diff --git a/src/ActuatorPanel.js b/src/ActuatorPanel.js index bed210a..ad20fe3 100644 --- a/src/ActuatorPanel.js +++ b/src/ActuatorPanel.js @@ -10,6 +10,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PauseIcon from '@mui/icons-material/Pause'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import SettingsIcon from '@mui/icons-material/Settings'; +import { useTranslation } from './i18n/LanguageContext'; const MAX_ACTUATOR_IDS = 256; @@ -50,6 +51,7 @@ const ActuatorPanel = () => { const [showIdSelector, setShowIdSelector] = useState(false); const nodeId = 0; + const { t } = useTranslation(); const activeActuatorIds = enabledActuatorIds .map((enabled, id) => enabled ? id : null) @@ -282,9 +284,9 @@ const ActuatorPanel = () => { const renderIdSelectorDialog = () => ( setShowIdSelector(false)}> - Select Actuator IDs + {t('act.select_ids_title')} - Select Actuator IDs: + {t('act.select_ids_label')} {Array(MAX_ACTUATOR_IDS).fill(0).map((_, id) => ( { @@ -316,11 +318,11 @@ const ActuatorPanel = () => { fullWidth > - Command Type Range Settings + {t('act.range_title')} - Configure default ranges for each command type. These settings can be applied to all actuators. + {t('act.range_instruction')} @@ -328,7 +330,7 @@ const ActuatorPanel = () => { {COMMAND_TYPE_LABELS[0]} - Unitless command range is fixed at -1 to 1 + {t('act.unitless_fixed')} @@ -339,7 +341,7 @@ const ActuatorPanel = () => { { { onClick={() => applyRangesToAllOfType(1)} fullWidth > - Apply + {t('act.apply')} @@ -379,7 +381,7 @@ const ActuatorPanel = () => { { { onClick={() => applyRangesToAllOfType(2)} fullWidth > - Apply + {t('act.apply')} @@ -419,7 +421,7 @@ const ActuatorPanel = () => { { { onClick={() => applyRangesToAllOfType(3)} fullWidth > - Apply + {t('act.apply')} @@ -458,13 +460,13 @@ const ActuatorPanel = () => { color="primary" variant="contained" > - Apply All Ranges + {t('act.apply_all')} @@ -553,7 +555,7 @@ const ActuatorPanel = () => { onClick={() => setShowIdSelector(true)} sx={{ textTransform: 'none' }} > - Actuator IDs ({activeActuatorIds.length}) + {t('act.ids', { count: activeActuatorIds.length })} @@ -566,14 +568,14 @@ const ActuatorPanel = () => { onClick={() => setShowSettingsModal(true)} sx={{ textTransform: 'none' }} > - Range Settings + {t('act.range_settings')} - Broadcast Rate: + {t('act.broadcast_rate')} { width: '50%' }}> - ID: {actuator.actuator_id} + {t('act.id')} {actuator.actuator_id} - Pos: {actuator.position !== null ? actuator.position.toFixed(3) : "NC"} + {t('act.pos')} {actuator.position !== null ? actuator.position.toFixed(3) : t('act.nc')} - Force: {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : "NC"} + {t('act.force')} {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : t('act.nc')} - Speed: {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : "NC"} + {t('act.speed')} {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : t('act.nc')} - RAT: {actuator.power_rating_pct !== null - ? actuator.power_rating_pct === 127 - ? "unknown" - : `${actuator.power_rating_pct.toFixed(1)} %` - : "NC"} + {t('act.rat')} {actuator.power_rating_pct !== null + ? actuator.power_rating_pct === 127 + ? t('act.unknown') + : `${actuator.power_rating_pct.toFixed(1)} %` + : t('act.nc')} @@ -668,10 +670,10 @@ const ActuatorPanel = () => { variant="outlined" sx={{ height: '30px', fontSize: '0.8rem' }} > - Unitless - Position - Force - Speed + {t('act.type_unitless')} + {t('act.type_position')} + {t('act.type_force')} + {t('act.type_speed')} @@ -700,7 +702,7 @@ const ActuatorPanel = () => { fullWidth size="small" > - Zero + {t('act.zero')} @@ -749,7 +751,7 @@ const ActuatorPanel = () => { }}> - cmd: [{ + {t('act.cmd')} [{ activeActuatorIds .map(id => { const type = commandTypes[id] || COMMAND_TYPES.UNITLESS; @@ -768,7 +770,7 @@ const ActuatorPanel = () => { startIcon={} onClick={handleZeroAll} > - Zero All + {t('act.zero_all')} diff --git a/src/App.js b/src/App.js index 1adaf6d..0b009cc 100644 --- a/src/App.js +++ b/src/App.js @@ -17,8 +17,10 @@ import './css/index.css'; import ConnectionIndicators from './ConnectionIndicators'; import DnsIcon from '@mui/icons-material/Dns'; import LanIcon from '@mui/icons-material/Lan'; +import LanguageIcon from '@mui/icons-material/Language'; import CompactSidebar from './CompactSidebar'; import DynamicNodeIdServer from './services/DynamicNodeIdServer'; +import { LanguageProvider, useTranslation } from './i18n/LanguageContext'; window.mavlinkSession = new MavlinkSession(); window.localNode = new dronecan.Node({name: "com.vimdrones.web_gui"}); @@ -32,6 +34,17 @@ localNode.on('uavcan.protocol.file.Read.Request', (transfer) => { }); const App = () => { + return ( + + + + + + ); +}; + +const AppContent = () => { + const { t, language, setLanguage } = useTranslation(); const [nodes, setNodes] = useState({}); const [nodesUpdateTimestamp, setNodesUpdateTimestamp] = useState(0); const [isConnected, setIsConnected] = useState(false); @@ -75,7 +88,7 @@ const App = () => { const handleConnectionStatusChange = (isConnected) => { setIsConnected(isConnected); showMessage( - isConnected ? 'Successfully connected to device' : 'Disconnected from device', + isConnected ? t('app.connected') : t('app.disconnected'), isConnected ? 'success' : 'info' ); }; @@ -87,7 +100,7 @@ const App = () => { if (window.localNode) { window.localNode.changeBus(newBus); - showMessage(`Switched to CAN bus ${newBus}`, 'info'); + showMessage(t('app.bus_switched', { bus: newBus }), 'info'); } }; @@ -119,16 +132,16 @@ const App = () => { if (window.dnaServer.getStatus().isActive) { window.dnaServer.stop(); setDnaServerActive(false); - showMessage('DNA server stopped', 'info'); + showMessage(t('app.dna_stopped'), 'info'); } else { const success = window.dnaServer.start(1, 125); setDnaServerActive(success); - showMessage(success ? 'DNA server started' : 'Failed to start DNA server', success ? 'success' : 'error'); + showMessage(success ? t('app.dna_started') : t('app.dna_failed'), success ? 'success' : 'error'); } }; return ( - + <> @@ -142,7 +155,7 @@ const App = () => { - DroneCAN Web Tools + {t('app.title')} @@ -175,10 +188,10 @@ const App = () => { backgroundColor: 'background.paper', }} > - Bus 1 - Bus 2 - Bus 3 - Bus 4 + {t('app.bus', { n: 1 })} + {t('app.bus', { n: 2 })} + {t('app.bus', { n: 3 })} + {t('app.bus', { n: 4 })} @@ -206,16 +219,27 @@ const App = () => { } } : {}} > - DNA + {t('app.dna')} - @@ -294,7 +318,7 @@ const App = () => { {snackbarMessage} - + ); }; diff --git a/src/BusMonitor.js b/src/BusMonitor.js index 5ec1bb3..dad4220 100644 --- a/src/BusMonitor.js +++ b/src/BusMonitor.js @@ -10,6 +10,7 @@ import PauseIcon from '@mui/icons-material/Pause'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import SaveIcon from '@mui/icons-material/Save'; import { toYaml } from './dronecan/message_format_utils'; +import { useTranslation } from './i18n/LanguageContext'; const BusMonitor = () => { const [transfers, setTransfers] = useState([]); @@ -18,6 +19,7 @@ const BusMonitor = () => { const [selectedTransfer, setSelectedTransfer] = useState(null); const [detailsOpen, setDetailsOpen] = useState(false); const [messageYaml, setMessageYaml] = useState(''); + const { t } = useTranslation(); const tableContainerRef = useRef(null); const maxTransfers = 1000; // Maximum number of transfers to store @@ -29,23 +31,23 @@ const BusMonitor = () => { let yamlText = ''; if (transfer.data && transfer.data.toObj) { const msgObj = transfer.data.toObj(); - yamlText = `### Message details\n`; - yamlText += `Direction: ${transfer.direction}\n`; - yamlText += `Time: ${transfer.timestamp}\n`; - yamlText += `CAN ID: ${transfer.frameId}\n`; - yamlText += `Source Node: ${transfer.sourceNodeId}\n`; - yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`; - yamlText += `Data Type: ${transfer.dataType}\n\n`; - yamlText += `### Message Payload\n`; + yamlText = `${t('bus.details_heading')}\n`; + yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`; + yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`; + yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`; + yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`; + yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`; + yamlText += `${t('bus.detail_data_type')} ${transfer.dataType}\n\n`; + yamlText += `${t('bus.payload_heading')}\n`; yamlText += toYaml(msgObj); } else { - yamlText = `No detailed payload data available for this transfer.\n\n`; - yamlText += `Direction: ${transfer.direction}\n`; - yamlText += `Time: ${transfer.timestamp}\n`; - yamlText += `CAN ID: ${transfer.frameId}\n`; - yamlText += `Hex Data: ${transfer.hexData}\n`; - yamlText += `Source Node: ${transfer.sourceNodeId}\n`; - yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`; + yamlText = `${t('bus.no_payload')}\n\n`; + yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`; + yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`; + yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`; + yamlText += `${t('bus.detail_hex_data')} ${transfer.hexData}\n`; + yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`; + yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`; } setMessageYaml(yamlText); @@ -139,7 +141,7 @@ const BusMonitor = () => { }; const exportToCSV = () => { - const headers = ['Direction', 'Timestamp', 'CAN ID (Hex)', 'Hex Data', 'Src Node ID', 'Dst Node ID', 'Data Type', 'Raw Data']; + const headers = [t('bus.csv_direction'), t('bus.csv_timestamp'), t('bus.csv_can_id'), t('bus.csv_hex_data'), t('bus.csv_src'), t('bus.csv_dst'), t('bus.csv_data_type'), t('bus.csv_raw')]; const csvRows = [ headers.join(','), ...transfers.map(transfer => { @@ -182,7 +184,7 @@ const BusMonitor = () => { - Bus Monitor + {t('bus.title')} { size="small" /> } - label={Auto Scroll} + label={{t('bus.auto_scroll')}} labelPlacement="start" /> { variant="outlined" sx={{ ml: 1 }} > - Export + {t('bus.export')} @@ -232,13 +234,13 @@ const BusMonitor = () => { - Dir - Time - CAN ID - Hex Data - Src - Dst - Data Type + {t('bus.col_dir')} + {t('bus.col_time')} + {t('bus.col_can_id')} + {t('bus.col_hex_data')} + {t('bus.col_src')} + {t('bus.col_dst')} + {t('bus.col_data_type')} @@ -297,7 +299,7 @@ const BusMonitor = () => { }} > - Showing {transfers.length} of max {maxTransfers} transfers + {t('bus.showing', { count: transfers.length, max: maxTransfers })} {isPaused && ( { gap: '4px' }} > - PAUSED + {t('bus.paused')} )} @@ -322,7 +324,7 @@ const BusMonitor = () => { fullWidth > - Message Details + {t('bus.message_details')} {selectedTransfer && ( {selectedTransfer.dataType} - {selectedTransfer.frameId} @@ -347,7 +349,7 @@ const BusMonitor = () => { diff --git a/src/ConfirmRestartModal.js b/src/ConfirmRestartModal.js index 7ee92d5..098d616 100644 --- a/src/ConfirmRestartModal.js +++ b/src/ConfirmRestartModal.js @@ -1,21 +1,23 @@ import React from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material'; +import { useTranslation } from './i18n/LanguageContext'; const ConfirmRestartModal = ({ open, onClose, onConfirm }) => { + const { t } = useTranslation(); return ( - Confirm Restart + {t('confirm.title')} - Are you sure you want to restart the node? + {t('confirm.message')} diff --git a/src/ConnectionSettingsModal.js b/src/ConnectionSettingsModal.js index 6e7f6a5..05e8a56 100644 --- a/src/ConnectionSettingsModal.js +++ b/src/ConnectionSettingsModal.js @@ -11,6 +11,7 @@ import CloseIcon from '@mui/icons-material/Close'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; import WebSerial from './web_serial'; +import { useTranslation } from './i18n/LanguageContext'; // Add this constant at the top of your file, outside the component const USB_DEVICE_NAMES = { @@ -115,10 +116,11 @@ const ConnectionSettingsModal = ({ // Add this state variable to track connection attempts in progress const [connectionInProgress, setConnectionInProgress] = useState(false); + const { t } = useTranslation(); // Create a function to identify and index duplicate devices const getPortDisplayName = (port, allPorts) => { - if (!port) return "No port selected"; + if (!port) return t('conn.no_port_selected'); // Try to extract the most user-friendly name possible if (port.info && port.info.product) { @@ -196,7 +198,7 @@ const ConnectionSettingsModal = ({ } // Fallback for ports without specific info - return "Serial Port"; + return t('conn.serial_port'); }; // Moved from App.js - Lists available ports @@ -249,7 +251,7 @@ const ConnectionSettingsModal = ({ setActiveConnection(null); setActiveSerialProtocol(null); onConnectionStatusChange(false); - showMessage('Serial connection closed', 'info'); + showMessage(t('conn.serial_closed'), 'info'); // Clear the forwarding interval if (forwardingInterval) { @@ -293,12 +295,12 @@ const ConnectionSettingsModal = ({ setActiveSerialProtocol(serialProtocol); setConnectionInProgress(false); onConnectionStatusChange(true); - showMessage(`Serial ${serialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'} connection established`, 'success'); + showMessage(serialProtocol === SERIAL_PROTOCOLS.SLCAN ? t('conn.serial_slcan_ok') : t('conn.serial_mavlink_ok'), 'success'); }) window.mavlinkSession.addWebSerialErrorHandler((error) => { console.error('Serial connection error:', error); - showMessage(`Serial connection failed: ${error.message || 'Could not connect to port'}`, 'error'); + showMessage(t('conn.serial_failed', { error: error.message || t('conn.could_not_connect') }), 'error'); // Reset in-progress state on error setConnectionInProgress(false); }); @@ -306,7 +308,7 @@ const ConnectionSettingsModal = ({ window.mavlinkSession.webSerialConnect(); } catch (error) { console.error('Serial connection error:', error); - showMessage(`Serial connection failed: ${error.message || 'Could not connect to port'}`, 'error'); + showMessage(t('conn.serial_failed', { error: error.message || t('conn.could_not_connect') }), 'error'); // Reset in-progress state on error setConnectionInProgress(false); } @@ -317,7 +319,7 @@ const ConnectionSettingsModal = ({ } } catch (error) { console.error('Error with serial connection:', error); - showMessage(`Serial error: ${error.message || 'Unknown error'}`, 'error'); + showMessage(t('conn.serial_error', { error: error.message || t('conn.unknown_error') }), 'error'); // Reset in-progress state on any error setConnectionInProgress(false); } @@ -327,7 +329,7 @@ const ConnectionSettingsModal = ({ const validateIpAddress = (input) => { // Check if empty if (!input) { - return 'IP address is required'; + return t('conn.ip_required'); } // Allow "localhost" @@ -342,7 +344,7 @@ const ConnectionSettingsModal = ({ for (const octet of octets) { const num = parseInt(octet, 10); if (isNaN(num) || num < 0 || num > 255 || octet !== num.toString()) { - return 'Each part must be a number between 0-255'; + return t('conn.ip_invalid_parts'); } } return ''; @@ -356,17 +358,17 @@ const ConnectionSettingsModal = ({ return ''; } - return 'Invalid IP address or hostname'; + return t('conn.ip_invalid'); }; const validatePort = (input) => { if (!input) { - return 'Port is required'; + return t('conn.port_required'); } const port = parseInt(input, 10); if (isNaN(port) || port < 1 || port > 65535) { - return 'Port must be between 1-65535'; + return t('conn.port_range'); } return ''; @@ -375,7 +377,7 @@ const ConnectionSettingsModal = ({ // Add validation for nodeId const validateNodeId = (value) => { const id = parseInt(value, 10); - return (isNaN(id) || id < 1 || id > 127) ? 'Node ID must be between 1-127' : ''; + return (isNaN(id) || id < 1 || id > 127) ? t('conn.node_id_range') : ''; }; // Update handler to propagate changes to parent @@ -417,7 +419,7 @@ const ConnectionSettingsModal = ({ setActiveConnection(null); setActiveSerialProtocol(null); onConnectionStatusChange(false); - showMessage('WebSocket connection closed', 'info'); + showMessage(t('conn.ws_closed'), 'info'); if (forwardingInterval) { clearInterval(forwardingInterval); setForwardingInterval(null); @@ -462,7 +464,7 @@ const ConnectionSettingsModal = ({ setActiveSerialProtocol(null); onConnectionStatusChange(true); setConnectionInProgress(false); - showMessage('WebSocket connection established', 'success'); + showMessage(t('conn.ws_connected'), 'success'); }); window.mavlinkSession.addWebSocketErrorHandler((error) => { @@ -474,11 +476,11 @@ const ConnectionSettingsModal = ({ setActiveConnection(null); onConnectionStatusChange(false); setConnectionInProgress(false); - let errorMsg = 'Connection failed'; + let errorMsg = t('conn.ws_failed'); if (error && error.message) { - errorMsg = `Connection failed: ${error.message}`; + errorMsg = t('conn.ws_failed_detail', { error: error.message }); } else if (typeof error === 'string') { - errorMsg = `Connection failed: ${error}`; + errorMsg = t('conn.ws_failed_detail', { error: error }); } showMessage(errorMsg, 'error'); }); @@ -490,7 +492,7 @@ const ConnectionSettingsModal = ({ } } catch (error) { console.error('Error with WebSocket connection:', error); - showMessage(`WebSocket error: ${error.message || 'Unknown error'}`, 'error'); + showMessage(t('conn.ws_error', { error: error.message || t('conn.unknown_error') }), 'error'); setConnectionInProgress(false); } }; @@ -521,11 +523,11 @@ const ConnectionSettingsModal = ({ > - Adapter Settings + {t('conn.title')} {activeConnection && ( - Serial Connection + {t('conn.serial_section')} {/* Reduce gap */} {/* Port and Baud Selection in same row */} {/* Port Selection - takes more space */} - Port + {t('conn.port')} setBaudRate(e.target.value)} - label="Baud Rate" + label={t('conn.baud_rate')} > {BAUD_RATES.map((rate) => ( @@ -601,14 +603,14 @@ const ConnectionSettingsModal = ({ - Serial Protocol + {t('conn.serial_protocol')} @@ -621,7 +623,7 @@ const ConnectionSettingsModal = ({ disabled={activeConnection !== null} size="small" > - Refresh + {t('conn.refresh')} @@ -657,11 +659,11 @@ const ConnectionSettingsModal = ({ width: '100%', bgcolor: activeConnection === 'websocket' ? 'rgba(0, 200, 83, 0.1)' : 'inherit' }}> - WebSocket Connection + {t('conn.ws_section')} {/* Reduce gap */} - {activeConnection === 'websocket' ? 'Disconnect' : - connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'} + {activeConnection === 'websocket' ? t('conn.disconnect') : + connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')} @@ -712,7 +714,7 @@ const ConnectionSettingsModal = ({ { const [serverEnabled, setServerEnabled] = useState(false); @@ -23,6 +24,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { const [server, setServer] = useState(null); const [operationInProgress, setOperationInProgress] = useState(false); const [refreshInterval, setRefreshInterval] = useState(null); + const { t } = useTranslation(); const handleAllocationUpdate = () => { console.log("Allocation update detected, refreshing list"); @@ -84,9 +86,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { }, 1000); // Check every 5 seconds setRefreshInterval(interval); - showMessage("DNA server started successfully", "success"); + showMessage(t('dna.started'), "success"); } else { - showMessage("Failed to start DNA server", "error"); + showMessage(t('dna.failed_start'), "error"); } } else { // Stop the server @@ -99,11 +101,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { setRefreshInterval(null); } - showMessage("DNA server stopped", "info"); + showMessage(t('dna.stopped'), "info"); } } catch (error) { console.error("Error toggling DNA server:", error); - showMessage(`Error: ${error.message}`, "error"); + showMessage(t('dna.error', { error: error.message }), "error"); } finally { setOperationInProgress(false); } @@ -124,7 +126,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { }; const validateNodeIdRange = () => { - return minNodeId >= maxNodeId ? "Min ID must be less than Max ID" : ""; + return minNodeId >= maxNodeId ? t('dna.invalid_range') : ""; }; const handleDeleteAllocation = (nodeId) => { @@ -133,15 +135,15 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { const success = server.deleteAllocation(nodeId); if (success) { fetchCurrentAllocations(); - showMessage(`Node ID ${nodeId} allocation revoked`, "info"); + showMessage(t('dna.revoked', { id: nodeId }), "info"); } else { - showMessage(`Failed to revoke allocation for node ID ${nodeId}`, "error"); + showMessage(t('dna.revoke_failed', { id: nodeId }), "error"); } }; const handleRefreshAllocations = () => { fetchCurrentAllocations(); - showMessage("Allocations refreshed", "info"); + showMessage(t('dna.refreshed'), "info"); }; // Modify the modal close handler to not stop the server @@ -208,11 +210,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { > - Dynamic Node ID Allocation Server + {t('dna.title')} {serverEnabled && ( { bgcolor: serverEnabled ? 'rgba(0, 200, 83, 0.1)' : 'inherit' }}> - Server Control + {t('dna.control')} @@ -262,7 +264,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { {/* Min Node ID */} { }} size="small" error={minNodeId >= maxNodeId} - helperText={minNodeId >= maxNodeId ? "Must be < Max" : ""} + helperText={minNodeId >= maxNodeId ? t('dna.must_lt_max') : ""} /> {/* Max Node ID */} { }} size="small" error={minNodeId >= maxNodeId} - helperText={minNodeId >= maxNodeId ? "Must be > Min" : ""} + helperText={minNodeId >= maxNodeId ? t('dna.must_gt_min') : ""} /> @@ -315,8 +317,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { } label={ - Persist Allocations - + {t('dna.persist')} + @@ -339,9 +341,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => { variant="body1" // Using body1 for smaller font size instead of subtitle1 sx={{ fontWeight: 500 }} // Adding some weight to make it still look like a title > - Allocated Node IDs ({allocatedNodes.length}) + {t('dna.allocated', { count: allocatedNodes.length })} - + { {allocatedNodes.length === 0 ? ( - No node IDs allocated + {t('dna.no_allocations')} ) : ( @@ -374,9 +376,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
- NID - UUID - Action + {t('dna.col_nid')} + {t('dna.col_uuid')} + {t('dna.col_action')} diff --git a/src/EditParamModal.js b/src/EditParamModal.js index 3252de4..fe3c765 100644 --- a/src/EditParamModal.js +++ b/src/EditParamModal.js @@ -9,6 +9,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import StopIcon from '@mui/icons-material/Stop'; // Add this import for the stop button import MusicNoteIcon from '@mui/icons-material/MusicNote'; import AM32_Rtttl from './am32_rtttl'; // Updated import to match class name +import { useTranslation } from './i18n/LanguageContext'; const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { // Add a new state for tracking whether a tune is currently playing @@ -21,6 +22,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { const [errorMessage, setErrorMessage] = useState(''); // For validation error messages const [isValid, setIsValid] = useState(true); // Add a new state variable to track validation status const [paramName, setParamName] = useState(""); // Add paramName to the component state + const { t } = useTranslation(); useEffect(() => { const localNode = window.localNode; @@ -84,7 +86,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { if (!isValidFormat) { // Warn user but continue with a default tune - setErrorMessage('Warning: Invalid RTTTL format! Using a default empty tune instead.'); + setErrorMessage(t('edit.rtttl_warning')); // Continue with a minimal valid RTTTL string const tuneToParse = "Empty:d=4,o=5,b=120:"; result = AM32_Rtttl.to_am32_startup_melody(tuneToParse); @@ -102,7 +104,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { console.log("Binary array values:", Array.from(result.data).slice(0, 30)); } catch (err) { console.error("Error converting RTTTL to binary:", err); - setErrorMessage(`Error saving tune: ${err.message || 'Unknown error'}`); + setErrorMessage(t('edit.error_saving', { error: err.message || t('edit.unknown') })); // Provide an empty binary string (all zeros) as fallback const emptyArray = new Uint8Array(128); valueToSave = String.fromCharCode.apply(null, emptyArray); @@ -126,30 +128,30 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { try { // First validate that the tune has the basic RTTTL format (name:defaults:notes) if (!tuneToPlay || !tuneToPlay.includes(':') || tuneToPlay.split(':').length !== 3) { - setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes'); + setErrorMessage(t('edit.rtttl_invalid')); return; } - + // Clear any previous error when successful setErrorMessage(''); - + // Stop any currently playing tune before starting a new one AM32_Rtttl.stopMelody(); - + // Play the new tune AM32_Rtttl.playMelody(tuneToPlay); setPreviewTune(tuneToPlay); setIsPlaying(true); - + // Set up an event listener to detect when audio context is closed or ends const estimatedDuration = estimateTuneDuration(tuneToPlay); setTimeout(() => { setIsPlaying(false); }, estimatedDuration + 500); // Add a small buffer - + } catch (err) { console.error("Error playing tune:", err); - setErrorMessage(`Error playing tune: ${err.message || 'Unknown error'}`); + setErrorMessage(t('edit.error_playing', { error: err.message || t('edit.unknown') })); setIsPlaying(false); } }; @@ -224,7 +226,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { const isValidFormat = stringValue.includes(':') && stringValue.split(':').length === 3; if (!isValidFormat) { - setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes'); + setErrorMessage(t('edit.rtttl_invalid')); return false; } @@ -275,13 +277,13 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { // Validate against min/max if they exist if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) { setIsValid(false); - setErrorMessage(`Value must be between ${min !== null ? min : '-∞'} and ${max !== null ? max : '∞'}`); + setErrorMessage(t('edit.value_range', { min: min !== null ? min : '-∞', max: max !== null ? max : '∞' })); } else { setIsValid(true); setErrorMessage(''); } } - + setValue(newValue); }; @@ -322,7 +324,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { // Validate against min/max if they exist if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) { setIsValid(false); - setErrorMessage(`Value must be between ${min !== null ? min : '-∞'} and ${max !== null ? max : '∞'}`); + setErrorMessage(t('edit.value_range', { min: min !== null ? min : '-∞', max: max !== null ? max : '∞' })); } else { setIsValid(true); setErrorMessage(''); @@ -340,14 +342,14 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { - Select Preset Tune + {t('edit.select_preset')} { {firmwareFile && ( Selected File: {firmwareFile.name} - {fileContent && ` (${fileContent.size} bytes)`} + {fileContent && ` (${fileContent.size} ${t('fw.bytes', { defaultValue: 'bytes' })})`} )} @@ -183,13 +185,13 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => { {updateStatus === 'error' && ( - {statusMessage || 'An error occurred during the update.'} + {statusMessage || t('fw.error_occurred')} )} {updateStatus === 'success' && ( - {statusMessage || 'Firmware update completed successfully!'} + {statusMessage || t('fw.success')} )} @@ -197,14 +199,14 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => { {updateStatus !== 'updating' ? ( <> - ) : ( @@ -213,7 +215,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => { disabled={updateProgress < 100} onClick={onClose} > - {updateProgress < 100 ? 'Updating...' : 'Close'} + {updateProgress < 100 ? t('fw.updating_ellipsis') : t('fw.close')} )} diff --git a/src/NodeLogs.js b/src/NodeLogs.js index 2bbaf3a..c8b0069 100644 --- a/src/NodeLogs.js +++ b/src/NodeLogs.js @@ -3,10 +3,12 @@ import { TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper import PauseIcon from '@mui/icons-material/Pause'; import CleaningServicesIcon from '@mui/icons-material/CleaningServices'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import { useTranslation } from './i18n/LanguageContext'; const NodeLogs = () => { const [logs, setLogs] = useState([]); const [paused, setPaused] = useState(false); + const { t } = useTranslation(); useEffect(() => { const localNode = window.localNode; @@ -57,7 +59,7 @@ const NodeLogs = () => { alignItems: 'center', height: 20 }} margin={1}> - Logs + {t('logs.title')} {
- NID - Time - Level - Source - Text + {t('logs.col_nid')} + {t('logs.col_time')} + {t('logs.col_level')} + {t('logs.col_source')} + {t('logs.col_text')} diff --git a/src/NodeParam.js b/src/NodeParam.js index 0ea38d1..1ced073 100644 --- a/src/NodeParam.js +++ b/src/NodeParam.js @@ -11,6 +11,7 @@ import FileUploadIcon from '@mui/icons-material/FileUpload'; import EditIcon from '@mui/icons-material/Edit'; import ParamEditorSelector from './ParamEditors/ParamEditorSelector'; import AM32_Rtttl from './am32_rtttl'; +import { useTranslation } from './i18n/LanguageContext'; const OPCODE_SAVE = 0; const OPCODE_ERASE = 1; @@ -21,6 +22,7 @@ const NodeParam = ({ nodeId, nodes }) => { const [paramsUpdateTimestamp, setParamsUpdateTimestamp] = useState(0); const [fetchingParams, setFetchingParams] = useState(false); const fetchTimeoutRef = useRef(null); + const { t } = useTranslation(); if (!nodeId) return null; const node = nodes[nodeId]; @@ -97,9 +99,9 @@ const NodeParam = ({ nodeId, nodes }) => { // Function to format boolean values visually const formatBooleanValue = (value) => { if (value === 'True') { - return ; + return ; } else if (value === 'False') { - return ; + return ; } return value; }; @@ -214,15 +216,15 @@ const NodeParam = ({ nodeId, nodes }) => { } else if (param.fields.value.msg.fields.boolean_value !== undefined) { paramTypeDisplay = 'boolean'; if (param.fields.value.msg.fields.boolean_value.value === 0) { - paramValueDisplay = 'Disabled'; + paramValueDisplay = t('param.disabled'); } else if (param.fields.value.msg.fields.boolean_value.value === 1) { - paramValueDisplay = 'Enabled'; + paramValueDisplay = t('param.enabled'); } if (paramDefaultValue === 0) { - paramDefaultValueDisplay = 'Disabled'; + paramDefaultValueDisplay = t('param.disabled'); } else { - paramDefaultValueDisplay = 'Enabled'; + paramDefaultValueDisplay = t('param.enabled'); } paramMinValueDisplay = ""; paramMaxValueDisplay = ""; @@ -306,7 +308,7 @@ const NodeParam = ({ nodeId, nodes }) => { - + @@ -322,13 +324,13 @@ const NodeParam = ({ nodeId, nodes }) => {
- Idx - Name - Type - Value - Default - Min - Max + {t('param.col_idx')} + {t('param.col_name')} + {t('param.col_type')} + {t('param.col_value')} + {t('param.col_default')} + {t('param.col_min')} + {t('param.col_max')} @@ -357,7 +359,7 @@ const NodeParam = ({ nodeId, nodes }) => { sx={{ width: 80, mr: 2, ml: 0.5 }} variant="caption" > - Parameters + {t('param.title')} @@ -398,7 +400,7 @@ const NodeParam = ({ nodeId, nodes }) => { onClick={handleDownloadParams} disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0} > - Download + {t('param.download')} diff --git a/src/NodeProperties.js b/src/NodeProperties.js index 06e1a3c..e46bd64 100644 --- a/src/NodeProperties.js +++ b/src/NodeProperties.js @@ -7,6 +7,7 @@ import CableIcon from '@mui/icons-material/Cable'; import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; import FirmwareUpdateModal from './FirmwareUpdateModal'; import ConfirmRestartModal from './ConfirmRestartModal'; +import { useTranslation } from './i18n/LanguageContext'; const VendorSpecificCodeDisplay = (code) => { code = Math.max(0, Math.floor(code) & 0xFFFF); @@ -19,6 +20,7 @@ const VendorSpecificCodeDisplay = (code) => { const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEditorEnable }) => { const [firmwareModalOpen, setFirmwareModalOpen] = useState(false); const [restartModalOpen, setRestartModalOpen] = useState(false); + const { t } = useTranslation(); useEffect(() => { const localNode = window.localNode; @@ -71,16 +73,16 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit > - Node Properties + {t('props.title')} - Multi Node Editor + {t('props.multi_editor')} { setMultiNodeEditorEnable(e.target.checked) }} /> - Node Controls + {t('props.controls')} diff --git a/src/PanelsMenu.js b/src/PanelsMenu.js index 8b3f4d2..96b13cc 100644 --- a/src/PanelsMenu.js +++ b/src/PanelsMenu.js @@ -1,10 +1,12 @@ import React, { useState } from 'react'; import { Box, Button, Menu, MenuItem, Divider } from '@mui/material'; import VideogameAssetIcon from '@mui/icons-material/VideogameAsset'; +import { useTranslation } from './i18n/LanguageContext'; const PanelsMenu = ({openWindow}) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); + const { t } = useTranslation(); const handleClick = (event) => { setAnchorEl(event.currentTarget); @@ -42,7 +44,7 @@ const PanelsMenu = ({openWindow}) => { color="default" startIcon={} > - Panels + {t('panels.title')} { onClose={handleClose} > - ESC + {t('panels.esc')} - Actuator + {t('panels.actuator')} diff --git a/src/SubscriberWindow.js b/src/SubscriberWindow.js index 84bb0f2..eb5e89f 100644 --- a/src/SubscriberWindow.js +++ b/src/SubscriberWindow.js @@ -7,6 +7,7 @@ import DronecanLogo from './image/dronecan_logo.png'; import { toYaml } from './dronecan/message_format_utils'; import theme from './theme'; +import { useTranslation } from './i18n/LanguageContext'; import './css/subscriber.css'; const SubscriberWindow = () => { @@ -19,9 +20,10 @@ const SubscriberWindow = () => { const [recordingSet, setRecordingSet] = useState([]); const [displayRecordText, setDisplayRecordText] = useState(""); const [recording, setRecording] = useState(true); + const { t } = useTranslation(); if (window.opener === null) { - return "Not Allowed To Open Directly"; + return t('sub.not_allowed'); } useEffect(() => { @@ -61,7 +63,7 @@ const SubscriberWindow = () => { } const msg = transfer.payload; const msgObj = msg.toObj(); - let destNodeText = "All"; + let destNodeText = t('sub.all'); if (transfer.destNodeId && transfer.destNodeId !== 0) { destNodeText = `${transfer.destNodeId}`; } @@ -148,12 +150,12 @@ const SubscriberWindow = () => { - RX: + {t('sub.rx')} {totalRX} - Rates(Hz): + {t('sub.rates')} {messageRate.toFixed(0)} - Max: + {t('sub.max')} diff --git a/src/ToolsMenu.js b/src/ToolsMenu.js index bae92fc..bdc1b3d 100644 --- a/src/ToolsMenu.js +++ b/src/ToolsMenu.js @@ -3,10 +3,12 @@ import { Box, Button, Menu, MenuItem, Divider } from '@mui/material'; import BuildIcon from '@mui/icons-material/Build'; import MessageIcon from '@mui/icons-material/Message'; import SettingsInputCompositeIcon from '@mui/icons-material/SettingsInputComposite'; +import { useTranslation } from './i18n/LanguageContext'; const ToolsMenu =({openWindow}) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); + const { t } = useTranslation(); const handleClick = (event) => { setAnchorEl(event.currentTarget); @@ -44,7 +46,7 @@ const ToolsMenu =({openWindow}) => { color="default" startIcon={} > - Tools + {t('tools.title')} { onClose={handleClose} > - Subscriber + {t('tools.subscriber')} - Bus Monitor + {t('tools.bus_monitor')} diff --git a/src/i18n/LanguageContext.js b/src/i18n/LanguageContext.js new file mode 100644 index 0000000..cb3f525 --- /dev/null +++ b/src/i18n/LanguageContext.js @@ -0,0 +1,67 @@ +import React, { createContext, useContext, useState, useCallback, useMemo } from "react"; +import { en, zh } from "./translations"; + +const allTranslations = { en, zh }; +const LanguageContext = createContext(); +const STORAGE_KEY = "language"; + +function getInitialLanguage() { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "zh" || stored === "en") return stored; + } catch (e) {} + return "en"; +} + +export function LanguageProvider({ children }) { + const [language, setLanguage] = useState(getInitialLanguage); + const strings = allTranslations[language]; + + const handleSetLanguage = useCallback((lang) => { + setLanguage(lang); + try { + localStorage.setItem(STORAGE_KEY, lang); + } catch (e) {} + }, []); + + const t = useCallback( + (key, params) => { + let str = strings[key] || en[key] || key; + if (params) { + Object.entries(params).forEach(([k, v]) => { + str = str.replace(new RegExp(`\\{${k}\\}`, "g"), v); + }); + } + return str; + }, + [strings] + ); + + const value = useMemo( + () => ({ language, setLanguage: handleSetLanguage, t }), + [language, handleSetLanguage, t] + ); + + return ( + + {children} + + ); +} + +export function useTranslation() { + const ctx = useContext(LanguageContext); + if (!ctx) { + const t = (key, params) => { + let str = en[key] || key; + if (params) { + Object.entries(params).forEach(([k, v]) => { + str = str.replace(new RegExp(`\\{${k}\\}`, "g"), v); + }); + } + return str; + }; + return { language: "en", setLanguage: () => {}, t }; + } + return ctx; +} diff --git a/src/i18n/translations.js b/src/i18n/translations.js new file mode 100644 index 0000000..72461d0 --- /dev/null +++ b/src/i18n/translations.js @@ -0,0 +1,605 @@ +export const en = { + // App.js + "app.title": "DroneCAN Web Tools", + "app.bus": "Bus {n}", + "app.dna": "DNA", + "app.adapter": "Adapter", + "app.connected": "Successfully connected to device", + "app.disconnected": "Disconnected from device", + "app.bus_switched": "Switched to CAN bus {bus}", + "app.dna_stopped": "DNA server stopped", + "app.dna_started": "DNA server started", + "app.dna_failed": "Failed to start DNA server", + + // ConnectionSettingsModal.js + "conn.title": "Adapter Settings", + "conn.connected_serial_slcan": "Connected via serial (SLCAN)", + "conn.connected_serial_mavlink": "Connected via serial (MAVLink)", + "conn.connected_ws": "Connected via {type}", + "conn.serial_section": "Serial Connection", + "conn.port": "Port", + "conn.no_ports": "No ports available", + "conn.baud_rate": "Baud Rate", + "conn.serial_protocol": "Serial Protocol", + "conn.protocol_mavlink": "MAVLink tunnel", + "conn.protocol_slcan": "SLCAN / LAWICEL", + "conn.refresh": "Refresh", + "conn.request": "Request", + "conn.disconnect": "Disconnect", + "conn.connecting": "Connecting...", + "conn.connect": "Connect", + "conn.ws_section": "WebSocket Connection", + "conn.host": "Host/IP Address", + "conn.ws_port": "Port", + "conn.node_id": "Node ID", + "conn.signing": "Mavlink Signing", + "conn.secret_key": "Secret Key", + "conn.show_secret": "Show secret", + "conn.hide_secret": "Hide secret", + "conn.serial_closed": "Serial connection closed", + "conn.serial_slcan_ok": "Serial SLCAN connection established", + "conn.serial_mavlink_ok": "Serial MAVLink connection established", + "conn.serial_failed": "Serial connection failed: {error}", + "conn.could_not_connect": "Could not connect to port", + "conn.serial_error": "Serial error: {error}", + "conn.unknown_error": "Unknown error", + "conn.ws_closed": "WebSocket connection closed", + "conn.ws_connected": "WebSocket connection established", + "conn.ws_failed": "Connection failed", + "conn.ws_failed_detail": "Connection failed: {error}", + "conn.ws_error": "WebSocket error: {error}", + "conn.ip_required": "IP address is required", + "conn.ip_invalid_parts": "Each part must be a number between 0-255", + "conn.ip_invalid": "Invalid IP address or hostname", + "conn.port_required": "Port is required", + "conn.port_range": "Port must be between 1-65535", + "conn.node_id_range": "Node ID must be between 1-127", + "conn.no_port_selected": "No port selected", + "conn.serial_port": "Serial Port", + + // NodeParam.js + "param.title": "Parameters", + "param.fetching": "Fetching...", + "param.fetch_all": "Fetch All", + "param.store_all": "Store All", + "param.erase_all": "Erase All", + "param.download": "Download", + "param.load": "Load", + "param.col_idx": "Idx", + "param.col_name": "Name", + "param.col_type": "Type", + "param.col_value": "Value", + "param.col_default": "Default", + "param.col_min": "Min", + "param.col_max": "Max", + "param.edit_param": "Edit Parameter", + "param.true": "True", + "param.false": "False", + "param.disabled": "Disabled", + "param.enabled": "Enabled", + + // EditParamModal.js + "edit.title": "Edit Parameter", + "edit.param_name": "Parameter Name", + "edit.unknown": "Unknown", + "edit.string_value": "String Value", + "edit.enable_disable": "Enable/Disable:", + "edit.new_value": "New Value", + "edit.value_range": "Value must be between {min} and {max}", + "edit.current_value": "Current Value", + "edit.current_rtttl": "Current RTTTL", + "edit.default_value": "Default Value", + "edit.min_value": "Min Value", + "edit.max_value": "Max Value", + "edit.error_parsing_melody": "Error parsing melody data", + "edit.select_preset": "Select Preset Tune", + "edit.choose_preset": "Choose a preset tune", + "edit.apply": "Apply", + "edit.rtttl_tune": "RTTTL Tune", + "edit.rtttl_placeholder": "Format: name:d=duration,o=octave,b=bpm:notes", + "edit.stop_tune": "Stop tune", + "edit.play_tune": "Play tune", + "edit.rtttl_instruction": "Enter RTTTL format tune or select a preset", + "edit.rtttl_guide_title": "RTTTL Format Guide", + "edit.rtttl_guide_duration": "d=duration (1=whole, 2=half, 4=quarter, 8=eighth, 16=16th note)", + "edit.rtttl_guide_octave": "o=octave (4-7 where 5 is default)", + "edit.rtttl_guide_tempo": "b=tempo (beats per minute)", + "edit.rtttl_guide_notes": "Notes are: c, c#, d, d#, e, f, f#, g, g#, a, a#, b or h", + "edit.rtttl_guide_example": "Example: Beep:d=4,o=5,b=120:c", + "edit.rtttl_warning": "Warning: Invalid RTTTL format! Using a default empty tune instead.", + "edit.rtttl_invalid": "Invalid RTTTL format! Format should be: name:defaults:notes", + "edit.error_saving": "Error saving tune: {error}", + "edit.error_playing": "Error playing tune: {error}", + "edit.cancel": "Cancel", + "edit.save": "Save", + "edit.true": "True", + "edit.false": "False", + + // BusMonitor.js + "bus.title": "Bus Monitor", + "bus.auto_scroll": "Auto Scroll", + "bus.export": "Export", + "bus.col_dir": "Dir", + "bus.col_time": "Time", + "bus.col_can_id": "CAN ID", + "bus.col_hex_data": "Hex Data", + "bus.col_src": "Src", + "bus.col_dst": "Dst", + "bus.col_data_type": "Data Type", + "bus.showing": "Showing {count} of max {max} transfers", + "bus.paused": "PAUSED", + "bus.message_details": "Message Details", + "bus.close": "Close", + "bus.broadcast": "Broadcast", + "bus.no_payload": "No detailed payload data available for this transfer.", + "bus.details_heading": "### Message details", + "bus.payload_heading": "### Message Payload", + "bus.detail_direction": "Direction:", + "bus.detail_time": "Time:", + "bus.detail_can_id": "CAN ID:", + "bus.detail_source": "Source Node:", + "bus.detail_dest": "Destination Node:", + "bus.detail_data_type": "Data Type:", + "bus.detail_hex_data": "Hex Data:", + "bus.csv_direction": "Direction", + "bus.csv_timestamp": "Timestamp", + "bus.csv_can_id": "CAN ID (Hex)", + "bus.csv_hex_data": "Hex Data", + "bus.csv_src": "Src Node ID", + "bus.csv_dst": "Dst Node ID", + "bus.csv_data_type": "Data Type", + "bus.csv_raw": "Raw Data", + + // SubscriberWindow.js + "sub.not_allowed": "Not Allowed To Open Directly", + "sub.rx": "RX:", + "sub.rates": "Rates(Hz):", + "sub.max": "Max:", + "sub.all": "All", + + // EscPanel.js + "esc.channels": "Channels:", + "esc.remove_propellers": "REMOVE PROPELLERS!", + "esc.send_safety": "Send Safety", + "esc.send_arming": "Send Arming", + "esc.broadcast_rate": "Broadcast Rate:", + "esc.index": "Index:", + "esc.error": "Err:", + "esc.temp": "Temp:", + "esc.volt": "Volt:", + "esc.curr": "Curr:", + "esc.rpm": "RPM:", + "esc.rat": "RAT:", + "esc.nc": "NC", + "esc.stop": "Stop", + "esc.cmd": "cmd:", + "esc.stop_all": "Stop All", + + // ActuatorPanel.js + "act.ids": "Actuator IDs ({count})", + "act.range_settings": "Range Settings", + "act.broadcast_rate": "Broadcast Rate:", + "act.select_ids_title": "Select Actuator IDs", + "act.select_ids_label": "Select Actuator IDs:", + "act.done": "Done", + "act.range_title": "Command Type Range Settings", + "act.range_instruction": "Configure default ranges for each command type. These settings can be applied to all actuators.", + "act.unitless_fixed": "Unitless command range is fixed at -1 to 1", + "act.min": "Min", + "act.max": "Max", + "act.apply": "Apply", + "act.apply_all": "Apply All Ranges", + "act.close": "Close", + "act.id": "ID:", + "act.pos": "Pos:", + "act.force": "Force:", + "act.speed": "Speed:", + "act.rat": "RAT:", + "act.nc": "NC", + "act.unknown": "unknown", + "act.type_unitless": "Unitless", + "act.type_position": "Position", + "act.type_force": "Force", + "act.type_speed": "Speed", + "act.type_unitless_label": "Unitless [-1, 1]", + "act.type_position_label": "Position (m/rad)", + "act.type_force_label": "Force (N/Nm)", + "act.type_speed_label": "Speed (m/s, rad/s)", + "act.zero": "Zero", + "act.cmd": "cmd:", + "act.zero_all": "Zero All", + + // DnaServerModal.js + "dna.title": "Dynamic Node ID Allocation Server", + "dna.active": "Server Active", + "dna.control": "Server Control", + "dna.processing": "Processing...", + "dna.stop": "Stop", + "dna.start": "Start", + "dna.min_node_id": "Min Node ID", + "dna.must_lt_max": "Must be < Max", + "dna.max_node_id": "Max Node ID", + "dna.must_gt_min": "Must be > Min", + "dna.persist": "Persist Allocations", + "dna.persist_tooltip": "When enabled, node ID allocations are stored and restored when the server restarts", + "dna.allocated": "Allocated Node IDs ({count})", + "dna.no_allocations": "No node IDs allocated", + "dna.col_nid": "NID", + "dna.col_uuid": "UUID", + "dna.col_action": "Action", + "dna.refresh_tooltip": "Refresh allocation list", + "dna.started": "DNA server started successfully", + "dna.failed_start": "Failed to start DNA server", + "dna.stopped": "DNA server stopped", + "dna.revoked": "Node ID {id} allocation revoked", + "dna.revoke_failed": "Failed to revoke allocation for node ID {id}", + "dna.refreshed": "Allocations refreshed", + "dna.error": "Error: {error}", + "dna.invalid_range": "Min ID must be less than Max ID", + + // NodeProperties.js + "props.title": "Node Properties", + "props.multi_editor": "Multi Node Editor", + "props.node_id": "Node ID", + "props.name": "Name", + "props.mode": "Mode", + "props.health": "Health", + "props.uptime": "Uptime", + "props.vendor_code": "Vendor Specific Status Code", + "props.sw_version": "Software Version", + "props.crc64": "CRC64", + "props.vcs_commit": "VCS Commit", + "props.hw_version": "Hardware Version", + "props.uid": "UID", + "props.certificate": "Cert. of authenticity", + "props.controls": "Node Controls", + "props.restart": "Restart", + "props.transport_stats": "Get Transport Stats", + "props.update_firmware": "Update Firmware", + + // NodeLogs.js + "logs.title": "Logs", + "logs.col_nid": "NID", + "logs.col_time": "Time", + "logs.col_level": "Level", + "logs.col_source": "Source", + "logs.col_text": "Text", + + // ToolsMenu.js + "tools.title": "Tools", + "tools.subscriber": "Subscriber", + "tools.bus_monitor": "Bus Monitor", + + // PanelsMenu.js + "panels.title": "Panels", + "panels.esc": "ESC", + "panels.actuator": "Actuator", + + // ConfirmRestartModal.js + "confirm.title": "Confirm Restart", + "confirm.message": "Are you sure you want to restart the node?", + "confirm.cancel": "Cancel", + "confirm.confirm": "Confirm", + + // FirmwareUpdateModal.js + "fw.title": "Firmware Update", + "fw.select_hint": "Please select the firmware file (.bin|.hex) to upload to node {id}.", + "fw.select_file": "Select Firmware File", + "fw.selected_file": "Selected File: {name} ({size} bytes)", + "fw.invalid_file": "Invalid file type. Please select a .bin or .hex firmware file.", + "fw.load_failed": "Failed to load firmware file", + "fw.starting": "Starting firmware update...", + "fw.updating": "Updating firmware: {progress}% ({offset}/{total} bytes)", + "fw.success": "Firmware update completed successfully!", + "fw.update_failed": "Update failed: code: {code} {message}", + "fw.start_failed": "Failed to start update: {error}", + "fw.node_unavailable": "Local node not available", + "fw.error_occurred": "An error occurred during the update.", + "fw.cancel": "Cancel", + "fw.update": "Update", + "fw.updating_ellipsis": "Updating...", + "fw.close": "Close", +}; + +export const zh = { + // App.js + "app.title": "DroneCAN Web Tools", + "app.bus": "总线 {n}", + "app.dna": "DNA", + "app.adapter": "适配器", + "app.connected": "设备连接成功", + "app.disconnected": "设备已断开", + "app.bus_switched": "已切换到 CAN 总线 {bus}", + "app.dna_stopped": "DNA 服务器已停止", + "app.dna_started": "DNA 服务器已启动", + "app.dna_failed": "DNA 服务器启动失败", + + // ConnectionSettingsModal.js + "conn.title": "适配器设置", + "conn.connected_serial_slcan": "已通过串口连接 (SLCAN)", + "conn.connected_serial_mavlink": "已通过串口连接 (MAVLink)", + "conn.connected_ws": "已通过 {type} 连接", + "conn.serial_section": "串口连接", + "conn.port": "端口", + "conn.no_ports": "无可用端口", + "conn.baud_rate": "波特率", + "conn.serial_protocol": "串口协议", + "conn.protocol_mavlink": "MAVLink 隧道", + "conn.protocol_slcan": "SLCAN / LAWICEL", + "conn.refresh": "刷新", + "conn.request": "请求", + "conn.disconnect": "断开", + "conn.connecting": "连接中...", + "conn.connect": "连接", + "conn.ws_section": "WebSocket 连接", + "conn.host": "主机/IP 地址", + "conn.ws_port": "端口", + "conn.node_id": "节点 ID", + "conn.signing": "MAVLink 签名", + "conn.secret_key": "密钥", + "conn.show_secret": "显示密钥", + "conn.hide_secret": "隐藏密钥", + "conn.serial_closed": "串口连接已关闭", + "conn.serial_slcan_ok": "SLCAN 串口连接已建立", + "conn.serial_mavlink_ok": "MAVLink 串口连接已建立", + "conn.serial_failed": "串口连接失败: {error}", + "conn.could_not_connect": "无法连接到端口", + "conn.serial_error": "串口错误: {error}", + "conn.unknown_error": "未知错误", + "conn.ws_closed": "WebSocket 连接已关闭", + "conn.ws_connected": "WebSocket 连接已建立", + "conn.ws_failed": "连接失败", + "conn.ws_failed_detail": "连接失败: {error}", + "conn.ws_error": "WebSocket 错误: {error}", + "conn.ip_required": "IP 地址为必填项", + "conn.ip_invalid_parts": "每段必须是 0-255 之间的数字", + "conn.ip_invalid": "无效的 IP 地址或主机名", + "conn.port_required": "端口为必填项", + "conn.port_range": "端口必须在 1-65535 之间", + "conn.node_id_range": "节点 ID 必须在 1-127 之间", + "conn.no_port_selected": "未选择端口", + "conn.serial_port": "串口", + + // NodeParam.js + "param.title": "参数", + "param.fetching": "获取中...", + "param.fetch_all": "获取全部", + "param.store_all": "保存全部", + "param.erase_all": "擦除全部", + "param.download": "下载", + "param.load": "加载", + "param.col_idx": "索引", + "param.col_name": "名称", + "param.col_type": "类型", + "param.col_value": "值", + "param.col_default": "默认值", + "param.col_min": "最小值", + "param.col_max": "最大值", + "param.edit_param": "编辑参数", + "param.true": "是", + "param.false": "否", + "param.disabled": "禁用", + "param.enabled": "启用", + + // EditParamModal.js + "edit.title": "编辑参数", + "edit.param_name": "参数名称", + "edit.unknown": "未知", + "edit.string_value": "字符串值", + "edit.enable_disable": "启用/禁用:", + "edit.new_value": "新值", + "edit.value_range": "值必须在 {min} 和 {max} 之间", + "edit.current_value": "当前值", + "edit.current_rtttl": "当前 RTTTL", + "edit.default_value": "默认值", + "edit.min_value": "最小值", + "edit.max_value": "最大值", + "edit.error_parsing_melody": "解析旋律数据出错", + "edit.select_preset": "选择预设铃声", + "edit.choose_preset": "选择一个预设铃声", + "edit.apply": "应用", + "edit.rtttl_tune": "RTTTL 铃声", + "edit.rtttl_placeholder": "格式: name:d=duration,o=octave,b=bpm:notes", + "edit.stop_tune": "停止播放", + "edit.play_tune": "播放铃声", + "edit.rtttl_instruction": "输入 RTTTL 格式铃声或选择预设", + "edit.rtttl_guide_title": "RTTTL 格式指南", + "edit.rtttl_guide_duration": "d=时值 (1=全音符, 2=二分, 4=四分, 8=八分, 16=十六分音符)", + "edit.rtttl_guide_octave": "o=八度 (4-7, 默认为 5)", + "edit.rtttl_guide_tempo": "b=速度 (每分钟节拍数)", + "edit.rtttl_guide_notes": "音符: c, c#, d, d#, e, f, f#, g, g#, a, a#, b 或 h", + "edit.rtttl_guide_example": "示例: Beep:d=4,o=5,b=120:c", + "edit.rtttl_warning": "警告: 无效的 RTTTL 格式! 使用默认空铃声代替。", + "edit.rtttl_invalid": "无效的 RTTTL 格式! 格式应为: name:defaults:notes", + "edit.error_saving": "保存铃声出错: {error}", + "edit.error_playing": "播放铃声出错: {error}", + "edit.cancel": "取消", + "edit.save": "保存", + "edit.true": "是", + "edit.false": "否", + + // BusMonitor.js + "bus.title": "总线监视器", + "bus.auto_scroll": "自动滚动", + "bus.export": "导出", + "bus.col_dir": "方向", + "bus.col_time": "时间", + "bus.col_can_id": "CAN ID", + "bus.col_hex_data": "十六进制数据", + "bus.col_src": "源", + "bus.col_dst": "目标", + "bus.col_data_type": "数据类型", + "bus.showing": "显示 {count} 条,最大 {max} 条", + "bus.paused": "已暂停", + "bus.message_details": "消息详情", + "bus.close": "关闭", + "bus.broadcast": "广播", + "bus.no_payload": "该传输无详细负载数据。", + "bus.details_heading": "### 消息详情", + "bus.payload_heading": "### 消息负载", + "bus.detail_direction": "方向:", + "bus.detail_time": "时间:", + "bus.detail_can_id": "CAN ID:", + "bus.detail_source": "源节点:", + "bus.detail_dest": "目标节点:", + "bus.detail_data_type": "数据类型:", + "bus.detail_hex_data": "十六进制数据:", + "bus.csv_direction": "方向", + "bus.csv_timestamp": "时间戳", + "bus.csv_can_id": "CAN ID (十六进制)", + "bus.csv_hex_data": "十六进制数据", + "bus.csv_src": "源节点 ID", + "bus.csv_dst": "目标节点 ID", + "bus.csv_data_type": "数据类型", + "bus.csv_raw": "原始数据", + + // SubscriberWindow.js + "sub.not_allowed": "不允许直接打开", + "sub.rx": "接收:", + "sub.rates": "频率(Hz):", + "sub.max": "最大:", + "sub.all": "全部", + + // EscPanel.js + "esc.channels": "通道数:", + "esc.remove_propellers": "请拆除螺旋桨!", + "esc.send_safety": "发送安全指令", + "esc.send_arming": "发送解锁指令", + "esc.broadcast_rate": "广播速率:", + "esc.index": "索引:", + "esc.error": "错误:", + "esc.temp": "温度:", + "esc.volt": "电压:", + "esc.curr": "电流:", + "esc.rpm": "转速:", + "esc.rat": "功率:", + "esc.nc": "无连接", + "esc.stop": "停止", + "esc.cmd": "指令:", + "esc.stop_all": "全部停止", + + // ActuatorPanel.js + "act.ids": "执行器 ID ({count})", + "act.range_settings": "范围设置", + "act.broadcast_rate": "广播速率:", + "act.select_ids_title": "选择执行器 ID", + "act.select_ids_label": "选择执行器 ID:", + "act.done": "完成", + "act.range_title": "指令类型范围设置", + "act.range_instruction": "配置每种指令类型的默认范围。这些设置可以应用到所有执行器。", + "act.unitless_fixed": "无量纲指令范围固定为 -1 到 1", + "act.min": "最小值", + "act.max": "最大值", + "act.apply": "应用", + "act.apply_all": "应用所有范围", + "act.close": "关闭", + "act.id": "ID:", + "act.pos": "位置:", + "act.force": "力:", + "act.speed": "速度:", + "act.rat": "功率:", + "act.nc": "无连接", + "act.unknown": "未知", + "act.type_unitless": "无量纲", + "act.type_position": "位置", + "act.type_force": "力", + "act.type_speed": "速度", + "act.type_unitless_label": "无量纲 [-1, 1]", + "act.type_position_label": "位置 (m/rad)", + "act.type_force_label": "力 (N/Nm)", + "act.type_speed_label": "速度 (m/s, rad/s)", + "act.zero": "归零", + "act.cmd": "指令:", + "act.zero_all": "全部归零", + + // DnaServerModal.js + "dna.title": "动态节点 ID 分配服务器", + "dna.active": "服务器运行中", + "dna.control": "服务器控制", + "dna.processing": "处理中...", + "dna.stop": "停止", + "dna.start": "启动", + "dna.min_node_id": "最小节点 ID", + "dna.must_lt_max": "必须小于最大值", + "dna.max_node_id": "最大节点 ID", + "dna.must_gt_min": "必须大于最小值", + "dna.persist": "持久化分配", + "dna.persist_tooltip": "启用后,节点 ID 分配将被保存,服务器重启时自动恢复", + "dna.allocated": "已分配节点 ID ({count})", + "dna.no_allocations": "暂无已分配的节点 ID", + "dna.col_nid": "节点ID", + "dna.col_uuid": "UUID", + "dna.col_action": "操作", + "dna.refresh_tooltip": "刷新分配列表", + "dna.started": "DNA 服务器启动成功", + "dna.failed_start": "DNA 服务器启动失败", + "dna.stopped": "DNA 服务器已停止", + "dna.revoked": "节点 ID {id} 分配已撤销", + "dna.revoke_failed": "撤销节点 ID {id} 分配失败", + "dna.refreshed": "分配列表已刷新", + "dna.error": "错误: {error}", + "dna.invalid_range": "最小 ID 必须小于最大 ID", + + // NodeProperties.js + "props.title": "节点属性", + "props.multi_editor": "多节点编辑器", + "props.node_id": "节点 ID", + "props.name": "名称", + "props.mode": "模式", + "props.health": "健康状态", + "props.uptime": "运行时间", + "props.vendor_code": "供应商状态码", + "props.sw_version": "软件版本", + "props.crc64": "CRC64", + "props.vcs_commit": "VCS 提交", + "props.hw_version": "硬件版本", + "props.uid": "UID", + "props.certificate": "认证证书", + "props.controls": "节点控制", + "props.restart": "重启", + "props.transport_stats": "获取传输统计", + "props.update_firmware": "更新固件", + + // NodeLogs.js + "logs.title": "日志", + "logs.col_nid": "节点ID", + "logs.col_time": "时间", + "logs.col_level": "级别", + "logs.col_source": "来源", + "logs.col_text": "内容", + + // ToolsMenu.js + "tools.title": "工具", + "tools.subscriber": "订阅器", + "tools.bus_monitor": "总线监视器", + + // PanelsMenu.js + "panels.title": "面板", + "panels.esc": "ESC", + "panels.actuator": "执行器", + + // ConfirmRestartModal.js + "confirm.title": "确认重启", + "confirm.message": "确定要重启该节点吗?", + "confirm.cancel": "取消", + "confirm.confirm": "确认", + + // FirmwareUpdateModal.js + "fw.title": "固件更新", + "fw.select_hint": "请选择要上传到节点 {id} 的固件文件 (.bin|.hex)。", + "fw.select_file": "选择固件文件", + "fw.selected_file": "已选文件: {name} ({size} 字节)", + "fw.invalid_file": "无效的文件类型。请选择 .bin 或 .hex 固件文件。", + "fw.load_failed": "加载固件文件失败", + "fw.starting": "正在开始固件更新...", + "fw.updating": "正在更新固件: {progress}% ({offset}/{total} 字节)", + "fw.success": "固件更新成功完成!", + "fw.update_failed": "更新失败: 代码: {code} {message}", + "fw.start_failed": "启动更新失败: {error}", + "fw.node_unavailable": "本地节点不可用", + "fw.error_occurred": "更新过程中发生错误。", + "fw.cancel": "取消", + "fw.update": "更新", + "fw.updating_ellipsis": "更新中...", + "fw.close": "关闭", +};