Add function Language
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-16 16:44:04 +07:00
parent 1156e1ab29
commit a2e87aeb29
11 changed files with 1790 additions and 474 deletions

771
www/i18n.js Normal file
View File

@@ -0,0 +1,771 @@
/**
* Central i18n for LiDAR Manager — vi / en.
* Static DOM: data-i18n, data-i18n-placeholder, data-i18n-title, data-i18n-aria
* Dynamic JS: I18n.t("key") or I18n.t("key", { name: "..." })
*/
(() => {
const MESSAGES = {
vi: {
"app.title": "LiDAR Manager",
"app.robotName": "RobotApp",
"app.status.ready": "Sẵn sàng",
"app.status.reloaded": "Đã tải lại",
"app.status.backendError": "Không kết nối được backend",
"app.status.jsError": "Lỗi JavaScript",
"common.cancel": "Hủy",
"common.close": "Đóng",
"common.save": "Lưu",
"common.add": "Thêm",
"common.delete": "Xóa",
"common.apply": "Áp dụng",
"common.reload": "Tải lại",
"common.select": "Chọn",
"common.edit": "Sửa",
"common.enabled": "Bật",
"common.disabled": "Tắt",
"common.configure": "Cấu hình",
"common.error": "Lỗi: {msg}",
"common.none": "none",
"common.optional": "Tùy chọn",
"login.prompt": "Chọn cách đăng nhập:",
"login.tab.password": "Tên đăng nhập và mật khẩu",
"login.tab.pin": "Mã PIN",
"login.password.title": "Đăng nhập bằng tên và mật khẩu",
"login.password.help1": "Nhập tên đăng nhập và mật khẩu để truy cập robot.",
"login.password.help2": "Tài khoản do quản trị viên cấp hoặc xem trong tài liệu hướng dẫn robot.",
"login.password.help3": "Nếu chưa có tài khoản, vui lòng liên hệ quản trị viên robot.",
"login.field.username": "Tên đăng nhập:",
"login.field.password": "Mật khẩu:",
"login.placeholder.username": "Admin",
"login.submit": "Đăng nhập",
"login.submitting": "Đang đăng nhập…",
"login.pin.title": "Đăng nhập bằng mã PIN",
"login.pin.help1": "Người dùng được kích hoạt PIN có thể đăng nhập tại đây.",
"login.pin.help2": "Nếu chưa có mã PIN 4 chữ số, vui lòng liên hệ quản trị viên robot.",
"login.pin.helpNote": "Không có mã PIN cấu hình sẵn — quản trị viên phải gán PIN trước.",
"login.pin.aria.group": "Mã PIN 4 chữ số",
"login.pin.aria.keypad": "Bàn phím số",
"login.pin.aria.backspace": "Xóa",
"login.error.invalidPin": "Mã PIN không hợp lệ. Liên hệ quản trị viên.",
"login.error.invalidPinShort": "Mã PIN không hợp lệ",
"login.error.missingCredentials": "Nhập tên đăng nhập và mật khẩu",
"login.error.badCredentials": "Sai tên đăng nhập hoặc mật khẩu. Thử Admin / admin",
"login.error.serverUnreachable": "Không kết nối được server. Kiểm tra http://localhost:8080",
"login.error.failed": "Đăng nhập thất bại",
"nav.aria.main": "Điều hướng chính",
"nav.aria.submenu": "Menu phụ",
"nav.collapse": "Thu gọn menu",
"nav.expand": "Mở menu",
"nav.dashboards": "Dashboards",
"nav.setup": "Setup",
"nav.monitoring": "Monitoring",
"nav.system": "System",
"nav.help": "Help",
"nav.logout": "Log out",
"nav.dashboard": "Dashboard",
"nav.missions": "Missions",
"nav.maps": "Maps & layout",
"nav.monitoring-log": "System log",
"nav.integrations": "Tích hợp",
"nav.help-api": "API documentation",
"topbar.robotTitle": "Robot",
"topbar.controlAria": "Start / Pause robot",
"topbar.allOk": "ỔN ĐỊNH",
"topbar.error": "LỖI",
"topbar.paused": "TẠM DỪNG",
"topbar.running": "ĐANG CHẠY",
"topbar.waiting": "Đang chờ mission mới…",
"topbar.noMissionsQueue": "Không có mission trong queue…",
"topbar.reset": "RESET",
"topbar.changeUserData": "Lưu thông tin",
"topbar.changePassword": "Đổi mật khẩu",
"topbar.logout": "Đăng xuất",
"topbar.displayName": "Tên hiển thị",
"topbar.joystickTitle": "Điều khiển tay (Joystick)",
"topbar.joystickSpeed": "Tốc độ",
"topbar.joystickOff": "Tắt joystick",
"topbar.joystickAria": "Joystick",
"topbar.batteryTitle": "Pin",
"topbar.localeVi": "TIẾNG VIỆT",
"topbar.localeEn": "ENGLISH",
"topbar.localeOption.vi": "🇻🇳 Tiếng Việt",
"topbar.localeOption.en": "🇺🇸 English",
"topbar.userDefault": "USER",
"topbar.noControlPermission": "Không có quyền điều khiển",
"topbar.queueCount": "{n} mission trong queue",
"topbar.code": "Mã",
"topbar.module": "Module",
"topbar.joystickSpeed.slow": "Chậm",
"topbar.joystickSpeed.medium": "Trung bình",
"topbar.joystickSpeed.fast": "Nhanh",
"topbar.startHint": "Bấm để START robot",
"topbar.pauseHint": "Bấm để PAUSE robot",
"auth.profile.displayNameRequired": "Tên hiển thị không được trống",
"auth.profile.saveFailed": "Lưu thông tin thất bại",
"auth.changePassword.title": "Đổi mật khẩu",
"auth.changePassword.current": "Mật khẩu hiện tại",
"auth.changePassword.new": "Mật khẩu mới",
"auth.changePassword.confirm": "Xác nhận mật khẩu mới",
"auth.changePassword.mismatch": "Mật khẩu mới không khớp",
"auth.changePassword.failed": "Đổi mật khẩu thất bại",
"dashboard.title": "Dashboard",
"dashboard.subtitle": "Widget mission — chạy, xếp hàng và tạm dừng giống MiR Fleet.",
"dashboard.addWidget": "Thêm widget",
"dashboard.editLayout": "Sửa layout",
"dashboard.editDone": "Xong",
"dashboard.empty": "Chưa có widget. Bấm «Thêm widget» để bắt đầu.",
"dashboard.system.title": "Hệ thống",
"dashboard.system.subtitle": "Trạng thái backend và layout đang active.",
"dashboard.system.backend": "Backend",
"dashboard.system.layout": "Layout",
"dashboard.system.model": "Model robot",
"dashboard.system.sensors": "LiDAR / IMU",
"dashboard.system.sensorCount": "{lidars} LiDAR • {imus} IMU",
"dashboard.dialog.add.title": "Thêm widget",
"dashboard.dialog.add.type": "Loại widget",
"dashboard.dialog.edit.title": "Cấu hình widget",
"dashboard.dialog.edit.type": "Loại",
"dashboard.dialog.edit.delete": "Xóa widget",
"dashboard.widget.mission_button": "Nút mission",
"dashboard.widget.mission_group": "Nhóm mission",
"dashboard.widget.mission_queue": "Mission queue",
"dashboard.widget.pause_continue": "Tạm dừng / Tiếp tục",
"dashboard.widget.field.mission": "Mission",
"dashboard.widget.field.group": "Nhóm mission",
"dashboard.widget.field.title": "Tiêu đề widget (tùy chọn)",
"dashboard.widget.titlePlaceholder": "VD: Go to charging",
"dashboard.widget.pauseHint": "Tạm dừng / tiếp tục / hủy mission đang chạy trên robot.",
"dashboard.widget.selectMission": "Chọn mission…",
"dashboard.widget.configHint": "Cấu hình widget và chọn mission.",
"dashboard.widget.emptyGroup": "Không có mission trong nhóm «{group}».",
"dashboard.widget.queueEmpty": "Queue trống",
"dashboard.widget.clearQueue": "Xóa queue chờ",
"dashboard.widget.continue": "Tiếp tục",
"dashboard.widget.pause": "Tạm dừng",
"dashboard.widget.cancelMission": "Hủy mission",
"dashboard.widget.runner.paused": "Mission đang tạm dừng",
"dashboard.widget.runner.running": "Mission đang chạy",
"dashboard.widget.runner.idle": "Không có mission đang chạy",
"dashboard.widget.unsupported": "Widget không hỗ trợ.",
"dashboard.widget.deleteConfirm": "Xóa widget này?",
"config.layout.title": "Quản lý layout",
"config.layout.subtitle": "Nhiều cấu hình robot — mỗi layout có LiDAR và model riêng.",
"config.layout.save": "Lưu layout",
"config.layout.current": "Layout hiện tại",
"config.layout.newName": "Tên layout mới",
"config.layout.newNamePlaceholder": "VD: AGV kho A",
"config.layout.cloneCurrent": "Sao chép từ layout đang mở",
"config.layout.create": "Tạo layout",
"config.layout.editingHint": "Đang chỉnh: {name}{dirty}",
"config.layout.unsavedDirty": " • chưa lưu",
"config.layout.unsavedSwitchConfirm": "Layout hiện tại có thay đổi chưa lưu. Tiếp tục?",
"config.layout.deleteConfirm": "Xóa layout «{name}»? Hành động không hoàn tác.",
"config.lidar.title": "LiDARs",
"config.lidar.subtitle": "Đăng ký tên, IP, port và chỉnh pose theo robot frame.",
"config.lidar.field.name": "Tên",
"config.lidar.field.ip": "IP",
"config.lidar.field.port": "Port",
"config.lidar.placeholder.name": "Lidar trước",
"config.lidar.placeholder.ip": "192.168.0.10",
"config.lidar.empty": "Chưa có LiDAR",
"config.lidar.emptyHint": "Hãy thêm LiDAR ở form phía trên.",
"config.lidar.deleteConfirm": "Xóa LiDAR này?",
"config.imu.title": "IMU",
"config.imu.subtitle": "Cảm biến quán tính — frame, topic và pose trên robot.",
"config.imu.field.name": "Tên",
"config.imu.field.frame": "Frame ID",
"config.imu.field.topic": "Topic",
"config.imu.field.source": "Nguồn",
"config.imu.source.external": "Ngoài (ROS topic)",
"config.imu.source.lidarBuiltin": "Tích hợp LiDAR",
"config.imu.source.onboard": "Onboard robot",
"config.imu.field.rate": "Tần số (Hz)",
"config.imu.enabled": "Bật IMU",
"config.imu.add": "Thêm IMU",
"config.imu.placeholder.name": "IMU chính",
"config.imu.placeholder.frame": "imu_link",
"config.imu.placeholder.topic": "imu/data",
"config.imu.empty": "Chưa có IMU",
"config.imu.emptyHint": "Thêm IMU ở form phía trên.",
"config.imu.deleteConfirm": "Xóa IMU này?",
"config.robot.title": "Model robot",
"config.robot.subtitle": "Kinematic differential — bánh, động cơ và giới hạn vận tốc.",
"config.robot.model.diff": "Differential (2 bánh)",
"config.robot.model.bicycle": "Bicycle",
"config.canvas.title": "Bố trí trên robot",
"config.canvas.viewHint": "Cuộn chuột: zoom • Shift + kéo: di chuyển vùng nhìn",
"config.canvas.robotCenter": "Robot center:",
"config.canvas.selected": "Selected:",
"config.canvas.pose": "Pose:",
"config.pose.notSet": "chưa đặt pose",
"config.selected.lidar": "LiDAR: {name}",
"config.selected.imu": "IMU: {name}",
"config.motor.wheelRight": "Bánh phải",
"config.motor.wheelLeft": "Bánh trái",
"config.motor.wheelSteer": "Bánh trước (steer)",
"config.motor.wheelDrive": "Bánh sau (drive)",
"config.motor.vendor": "Hãng",
"config.motor.model": "Model",
"config.motor.joint": "Joint (ROS)",
"config.motor.ratio": "Tỷ số hộp số",
"config.motor.invert": "Đảo chiều quay",
"config.motor.invertSteer": "Đảo chiều",
"config.motor.custom": "Tùy chỉnh",
"config.motor.customMotor": "Motor tùy chỉnh",
"missions.title": "Missions",
"missions.subtitle": "Setup → Missions — danh sách nhiệm vụ robot.",
"missions.create": "Tạo mission",
"missions.empty": "Chưa có mission. Bấm Tạo mission để bắt đầu.",
"missions.queue.title": "Mission queue",
"missions.queue.subtitle": "Thêm mission bằng biểu tượng queue — robot chạy theo thứ tự từ trên xuống.",
"missions.queue.cancel": "Hủy chạy",
"missions.queue.clear": "Xóa queue",
"missions.queue.empty": "Queue trống. Bấm ▤ trên mission để thêm.",
"missions.editor.kicker": "Mission editor",
"missions.editor.unsaved": "Chưa lưu",
"missions.editor.saveAs": "Save as",
"missions.editor.save": "Save",
"missions.editor.flowHint": "Thực thi từ trên xuống dưới. Kéo biểu tượng ↔ để đổi thứ tự. Với Loop: kéo action vào vùng bên trong.",
"missions.editor.emptyActions": "Chọn action từ menu phía trên để bắt đầu.",
"missions.editor.backAria": "Quay lại danh sách",
"missions.editor.settingsAria": "Cài đặt mission",
"missions.editor.addActionAria": "Thêm action",
"missions.queue.status.pending": "Chờ",
"missions.queue.status.running": "Đang chạy",
"missions.queue.status.done": "Xong",
"missions.queue.status.error": "Lỗi",
"missions.queue.status.cancelled": "Đã hủy",
"missions.queue.ready": "Sẵn sàng",
"missions.queue.idleMessage": "Robot sẵn sàng — queue trống hoặc chờ mission mới.",
"missions.queue.moveUp": "Lên",
"missions.queue.moveDown": "Xuống",
"missions.queue.addAria": "Thêm vào mission queue",
"missions.deleteConfirm": "Xóa mission «{name}»?",
"missions.queue.clearConfirm": "Xóa các mission đang chờ trong queue?",
"missions.queue.cancelConfirm": "Hủy mission đang chạy? (thoát loop nếu đang lặp)",
"missions.dialog.create.title": "Tạo mission",
"missions.dialog.create.name": "Tên mission",
"missions.dialog.create.group": "Nhóm mission",
"missions.dialog.create.groupNew": "Hoặc nhóm mới",
"missions.dialog.create.desc": "Mô tả",
"missions.dialog.create.namePlaceholder": "VD: Go to charging station",
"missions.dialog.settings.title": "Cài đặt mission",
"missions.dialog.settings.name": "Tên",
"missions.dialog.settings.group": "Nhóm",
"missions.dialog.settings.desc": "Mô tả",
"missions.dialog.saveAs.title": "Save mission as",
"missions.dialog.saveAs.name": "Tên mission mới",
"missions.dialog.saveAs.submit": "Lưu bản sao",
"missions.dialog.actionConfig.title": "Cấu hình action",
"missions.dialog.queue.title": "Thêm vào mission queue",
"missions.group.Move": "Move",
"missions.group.Logic": "Logic",
"missions.group.IO": "I/O",
"missions.group.Cart": "Cart",
"missions.group.Misc": "Misc",
"missions.action.move_to_position": "Go to position",
"missions.action.move_to_marker": "Go to marker",
"missions.action.adjust_localization": "Adjust localization",
"missions.action.wait": "Wait",
"missions.action.set_speed": "Set speed",
"missions.action.if": "If",
"missions.action.loop": "Loop",
"missions.action.break": "Break",
"missions.action.continue": "Continue",
"missions.action.pause": "Pause",
"missions.action.set_digital_output": "Set digital output",
"missions.action.wait_digital_input": "Wait for digital input",
"missions.action.set_plc_register": "Set PLC register",
"missions.action.pick_cart": "Pick cart",
"missions.action.drop_cart": "Drop cart",
"missions.action.user_log": "User log",
"missions.action.play_sound": "Play sound",
"missions.error.nameRequired": "Tên mission không được trống.",
"missions.error.nameDuplicate": "Tên mission đã tồn tại.",
"missions.error.nameEmpty": "Tên không được trống.",
"missions.saveSuccess": "Đã lưu mission.",
"missions.editor.discardConfirm": "Bỏ thay đổi chưa lưu?",
"missions.queue.status.executing": "Đang chạy",
"missions.action.waitOnLevel": "Chờ mức ON",
"integrations.modbus.title": "Modbus trigger",
"integrations.modbus.subtitle": "System → Triggers — coil 10012000 gắn mission_id. Thiết bị remote bật coil (Modbus TCP :5502) → mission vào queue.",
"integrations.modbus.add": "Thêm trigger",
"integrations.modbus.empty": "Chưa có trigger Modbus.",
"integrations.modbus.coilsLabel": "Coil đã gán (bấm để mô phỏng rising edge)",
"integrations.rest.title": "REST API — MiR v2.0.0",
"integrations.rest.subtitle": "Hệ thống bên ngoài POST mission vào queue qua REST.",
"integrations.rest.baseUrl": "Base URL",
"integrations.rest.quickTest": "Thử nhanh",
"integrations.rest.postQueue": "POST queue",
"integrations.fleet.title": "MiRFleet — Lên lịch mission",
"integrations.fleet.subtitle": "Ưu tiên, gán robot, chạy ASAP hoặc theo thời gian.",
"integrations.fleet.addSchedule": "Thêm lịch",
"integrations.fleet.empty": "Chưa có lịch fleet.",
"integrations.noMissions": "— Chưa có mission —",
"integrations.defaultRobot": "Robot chính",
"integrations.fireTrigger": "Kích hoạt",
"integrations.coilsEmpty": "Chưa gán coil. Thêm trigger bên trên (10012000).",
"integrations.coilState": "coil hiện tại: {state}",
"integrations.confirm.deleteTrigger": "Xóa trigger Modbus này?",
"integrations.confirm.deleteSchedule": "Xóa lịch fleet này?",
"integrations.dialog.trigger.title": "Modbus trigger",
"integrations.dialog.trigger.name": "Tên trigger",
"integrations.dialog.trigger.coil": "Coil ID",
"integrations.dialog.trigger.mission": "Mission",
"integrations.dialog.schedule.title": "Lịch MiRFleet",
"integrations.dialog.schedule.name": "Tên lịch",
"integrations.dialog.schedule.robot": "Robot",
"integrations.dialog.schedule.priority": "Ưu tiên",
"integrations.dialog.schedule.mode": "Chế độ",
"integrations.dialog.schedule.asap": "ASAP",
"integrations.dialog.schedule.scheduled": "Lên lịch",
"integrations.dialog.schedule.startTime": "Thời gian bắt đầu",
"integrations.schedule.runNow": "Chạy ngay",
"monitoring.log.title": "System log",
"monitoring.log.subtitle": "Monitoring → System log — nhật ký hệ thống (đang phát triển).",
"monitoring.log.placeholder": "Tính năng monitoring sẽ hiển thị log robot, cảnh báo và lịch sử mission tại đây.",
"help.api.title": "API documentation",
"help.api.subtitle": "Help → API — tham chiếu REST MiR v2.0.0 cho tích hợp bên ngoài.",
"help.api.body1": "Xem chi tiết endpoint tại System → Tích hợp hoặc tài liệu /api/v2.0.0/.",
"help.api.body2": "Reference Guide MiR rev 1.9: docs/Reference guide.pdf",
},
en: {
"app.title": "LiDAR Manager",
"app.robotName": "RobotApp",
"app.status.ready": "Ready",
"app.status.reloaded": "Reloaded",
"app.status.backendError": "Cannot connect to backend",
"app.status.jsError": "JavaScript error",
"common.cancel": "Cancel",
"common.close": "Close",
"common.save": "Save",
"common.add": "Add",
"common.delete": "Delete",
"common.apply": "Apply",
"common.reload": "Reload",
"common.select": "Select",
"common.edit": "Edit",
"common.enabled": "On",
"common.disabled": "Off",
"common.configure": "Configure",
"common.error": "Error: {msg}",
"common.none": "none",
"common.optional": "Optional",
"login.prompt": "Choose sign-in method:",
"login.tab.password": "Username and password",
"login.tab.pin": "PIN code",
"login.password.title": "Sign in with username and password",
"login.password.help1": "Enter your username and password to access the robot.",
"login.password.help2": "Accounts are provided by an administrator or in the robot manual.",
"login.password.help3": "If you do not have an account, contact the robot administrator.",
"login.field.username": "Username:",
"login.field.password": "Password:",
"login.placeholder.username": "Admin",
"login.submit": "Sign in",
"login.submitting": "Signing in…",
"login.pin.title": "Sign in with PIN",
"login.pin.help1": "Users with PIN enabled can sign in here.",
"login.pin.help2": "If you do not have a 4-digit PIN, contact the robot administrator.",
"login.pin.helpNote": "No PIN is preconfigured — an administrator must assign one first.",
"login.pin.aria.group": "4-digit PIN",
"login.pin.aria.keypad": "Numeric keypad",
"login.pin.aria.backspace": "Delete",
"login.error.invalidPin": "Invalid PIN. Contact the administrator.",
"login.error.invalidPinShort": "Invalid PIN",
"login.error.missingCredentials": "Enter username and password",
"login.error.badCredentials": "Invalid username or password. Try Admin / admin",
"login.error.serverUnreachable": "Cannot reach server. Check http://localhost:8080",
"login.error.failed": "Sign-in failed",
"nav.aria.main": "Main navigation",
"nav.aria.submenu": "Submenu",
"nav.collapse": "Collapse menu",
"nav.expand": "Expand menu",
"nav.dashboards": "Dashboards",
"nav.setup": "Setup",
"nav.monitoring": "Monitoring",
"nav.system": "System",
"nav.help": "Help",
"nav.logout": "Log out",
"nav.dashboard": "Dashboard",
"nav.missions": "Missions",
"nav.maps": "Maps & layout",
"nav.monitoring-log": "System log",
"nav.integrations": "Integrations",
"nav.help-api": "API documentation",
"topbar.robotTitle": "Robot",
"topbar.controlAria": "Start / Pause robot",
"topbar.allOk": "ALL OK",
"topbar.error": "ERROR",
"topbar.paused": "PAUSED",
"topbar.running": "RUNNING",
"topbar.waiting": "Waiting for new missions…",
"topbar.noMissionsQueue": "No missions in queue…",
"topbar.reset": "RESET",
"topbar.changeUserData": "Change user data",
"topbar.changePassword": "Change password",
"topbar.logout": "Log out",
"topbar.displayName": "Display name",
"topbar.joystickTitle": "Manual control (Joystick)",
"topbar.joystickSpeed": "Speed",
"topbar.joystickOff": "Disengage joystick",
"topbar.joystickAria": "Joystick",
"topbar.batteryTitle": "Battery",
"topbar.localeVi": "TIẾNG VIỆT",
"topbar.localeEn": "ENGLISH",
"topbar.localeOption.vi": "🇻🇳 Tiếng Việt",
"topbar.localeOption.en": "🇺🇸 English",
"topbar.userDefault": "USER",
"topbar.noControlPermission": "No control permission",
"topbar.queueCount": "{n} mission(s) in queue",
"topbar.code": "Code",
"topbar.module": "Module",
"topbar.joystickSpeed.slow": "Slow",
"topbar.joystickSpeed.medium": "Medium",
"topbar.joystickSpeed.fast": "Fast",
"topbar.startHint": "Click to START the robot",
"topbar.pauseHint": "Click to PAUSE the robot",
"auth.profile.displayNameRequired": "Display name cannot be empty",
"auth.profile.saveFailed": "Failed to save profile",
"auth.changePassword.title": "Change password",
"auth.changePassword.current": "Current password",
"auth.changePassword.new": "New password",
"auth.changePassword.confirm": "Confirm new password",
"auth.changePassword.mismatch": "New passwords do not match",
"auth.changePassword.failed": "Failed to change password",
"dashboard.title": "Dashboard",
"dashboard.subtitle": "Mission widgets — run, queue and pause like MiR Fleet.",
"dashboard.addWidget": "Add widget",
"dashboard.editLayout": "Edit layout",
"dashboard.editDone": "Done",
"dashboard.empty": "No widgets yet. Click «Add widget» to start.",
"dashboard.system.title": "System",
"dashboard.system.subtitle": "Backend status and active layout.",
"dashboard.system.backend": "Backend",
"dashboard.system.layout": "Layout",
"dashboard.system.model": "Robot model",
"dashboard.system.sensors": "LiDAR / IMU",
"dashboard.system.sensorCount": "{lidars} LiDAR • {imus} IMU",
"dashboard.dialog.add.title": "Add widget",
"dashboard.dialog.add.type": "Widget type",
"dashboard.dialog.edit.title": "Configure widget",
"dashboard.dialog.edit.type": "Type",
"dashboard.dialog.edit.delete": "Delete widget",
"dashboard.widget.mission_button": "Mission button",
"dashboard.widget.mission_group": "Mission group",
"dashboard.widget.mission_queue": "Mission queue",
"dashboard.widget.pause_continue": "Pause / Continue",
"dashboard.widget.field.mission": "Mission",
"dashboard.widget.field.group": "Mission group",
"dashboard.widget.field.title": "Widget title (optional)",
"dashboard.widget.titlePlaceholder": "e.g. Go to charging",
"dashboard.widget.pauseHint": "Pause, continue or cancel the running mission on the robot.",
"dashboard.widget.selectMission": "Select mission…",
"dashboard.widget.configHint": "Configure the widget and select a mission.",
"dashboard.widget.emptyGroup": "No missions in group «{group}».",
"dashboard.widget.queueEmpty": "Queue empty",
"dashboard.widget.clearQueue": "Clear pending queue",
"dashboard.widget.continue": "Continue",
"dashboard.widget.pause": "Pause",
"dashboard.widget.cancelMission": "Cancel mission",
"dashboard.widget.runner.paused": "Mission paused",
"dashboard.widget.runner.running": "Mission running",
"dashboard.widget.runner.idle": "No mission running",
"dashboard.widget.unsupported": "Unsupported widget.",
"dashboard.widget.deleteConfirm": "Delete this widget?",
"config.layout.title": "Layout manager",
"config.layout.subtitle": "Multiple robot configurations — each layout has its own LiDAR and model.",
"config.layout.save": "Save layout",
"config.layout.current": "Current layout",
"config.layout.newName": "New layout name",
"config.layout.newNamePlaceholder": "e.g. Warehouse AGV A",
"config.layout.cloneCurrent": "Clone from open layout",
"config.layout.create": "Create layout",
"config.layout.editingHint": "Editing: {name}{dirty}",
"config.layout.unsavedDirty": " • unsaved",
"config.layout.unsavedSwitchConfirm": "Current layout has unsaved changes. Continue?",
"config.layout.deleteConfirm": "Delete layout «{name}»? This cannot be undone.",
"config.lidar.title": "LiDARs",
"config.lidar.subtitle": "Register name, IP, port and adjust pose in robot frame.",
"config.lidar.field.name": "Name",
"config.lidar.field.ip": "IP",
"config.lidar.field.port": "Port",
"config.lidar.placeholder.name": "Front lidar",
"config.lidar.placeholder.ip": "192.168.0.10",
"config.lidar.empty": "No LiDAR yet",
"config.lidar.emptyHint": "Add a LiDAR using the form above.",
"config.lidar.deleteConfirm": "Delete this LiDAR?",
"config.imu.title": "IMU",
"config.imu.subtitle": "Inertial sensor — frame, topic and pose on robot.",
"config.imu.field.name": "Name",
"config.imu.field.frame": "Frame ID",
"config.imu.field.topic": "Topic",
"config.imu.field.source": "Source",
"config.imu.source.external": "External (ROS topic)",
"config.imu.source.lidarBuiltin": "LiDAR integrated",
"config.imu.source.onboard": "Onboard robot",
"config.imu.field.rate": "Rate (Hz)",
"config.imu.enabled": "Enable IMU",
"config.imu.add": "Add IMU",
"config.imu.placeholder.name": "Main IMU",
"config.imu.placeholder.frame": "imu_link",
"config.imu.placeholder.topic": "imu/data",
"config.imu.empty": "No IMU yet",
"config.imu.emptyHint": "Add an IMU using the form above.",
"config.imu.deleteConfirm": "Delete this IMU?",
"config.robot.title": "Robot model",
"config.robot.subtitle": "Differential kinematics — wheels, motors and velocity limits.",
"config.robot.model.diff": "Differential (2 wheels)",
"config.robot.model.bicycle": "Bicycle",
"config.canvas.title": "Layout on robot",
"config.canvas.viewHint": "Mouse wheel: zoom • Shift + drag: pan view",
"config.canvas.robotCenter": "Robot center:",
"config.canvas.selected": "Selected:",
"config.canvas.pose": "Pose:",
"config.pose.notSet": "pose not set",
"config.selected.lidar": "LiDAR: {name}",
"config.selected.imu": "IMU: {name}",
"config.motor.wheelRight": "Right wheel",
"config.motor.wheelLeft": "Left wheel",
"config.motor.wheelSteer": "Front wheel (steer)",
"config.motor.wheelDrive": "Rear wheel (drive)",
"config.motor.vendor": "Vendor",
"config.motor.model": "Model",
"config.motor.joint": "Joint (ROS)",
"config.motor.ratio": "Gear ratio",
"config.motor.invert": "Invert rotation",
"config.motor.invertSteer": "Invert",
"config.motor.custom": "Custom",
"config.motor.customMotor": "Custom motor",
"missions.title": "Missions",
"missions.subtitle": "Setup → Missions — robot task list.",
"missions.create": "Create mission",
"missions.empty": "No missions yet. Click Create mission to start.",
"missions.queue.title": "Mission queue",
"missions.queue.subtitle": "Add missions via the queue icon — robot runs top to bottom.",
"missions.queue.cancel": "Cancel run",
"missions.queue.clear": "Clear queue",
"missions.queue.empty": "Queue empty. Click ▤ on a mission to add.",
"missions.editor.kicker": "Mission editor",
"missions.editor.unsaved": "Unsaved",
"missions.editor.saveAs": "Save as",
"missions.editor.save": "Save",
"missions.editor.flowHint": "Execute top to bottom. Drag ↔ to reorder. For Loop: drag actions inside.",
"missions.editor.emptyActions": "Pick an action from the menu above to start.",
"missions.editor.backAria": "Back to list",
"missions.editor.settingsAria": "Mission settings",
"missions.editor.addActionAria": "Add action",
"missions.queue.status.pending": "Pending",
"missions.queue.status.running": "Running",
"missions.queue.status.done": "Done",
"missions.queue.status.error": "Error",
"missions.queue.status.cancelled": "Cancelled",
"missions.queue.ready": "Ready",
"missions.queue.idleMessage": "Robot ready — queue empty or waiting for new mission.",
"missions.queue.moveUp": "Up",
"missions.queue.moveDown": "Down",
"missions.queue.addAria": "Add to mission queue",
"missions.deleteConfirm": "Delete mission «{name}»?",
"missions.queue.clearConfirm": "Clear pending missions in queue?",
"missions.queue.cancelConfirm": "Cancel running mission? (exits loop if looping)",
"missions.dialog.create.title": "Create mission",
"missions.dialog.create.name": "Mission name",
"missions.dialog.create.group": "Mission group",
"missions.dialog.create.groupNew": "Or new group",
"missions.dialog.create.desc": "Description",
"missions.dialog.create.namePlaceholder": "e.g. Go to charging station",
"missions.dialog.settings.title": "Mission settings",
"missions.dialog.settings.name": "Name",
"missions.dialog.settings.group": "Group",
"missions.dialog.settings.desc": "Description",
"missions.dialog.saveAs.title": "Save mission as",
"missions.dialog.saveAs.name": "New mission name",
"missions.dialog.saveAs.submit": "Save copy",
"missions.dialog.actionConfig.title": "Configure action",
"missions.dialog.queue.title": "Add to mission queue",
"missions.group.Move": "Move",
"missions.group.Logic": "Logic",
"missions.group.IO": "I/O",
"missions.group.Cart": "Cart",
"missions.group.Misc": "Misc",
"missions.action.move_to_position": "Go to position",
"missions.action.move_to_marker": "Go to marker",
"missions.action.adjust_localization": "Adjust localization",
"missions.action.wait": "Wait",
"missions.action.set_speed": "Set speed",
"missions.action.if": "If",
"missions.action.loop": "Loop",
"missions.action.break": "Break",
"missions.action.continue": "Continue",
"missions.action.pause": "Pause",
"missions.action.set_digital_output": "Set digital output",
"missions.action.wait_digital_input": "Wait for digital input",
"missions.action.set_plc_register": "Set PLC register",
"missions.action.pick_cart": "Pick cart",
"missions.action.drop_cart": "Drop cart",
"missions.action.user_log": "User log",
"missions.action.play_sound": "Play sound",
"missions.error.nameRequired": "Mission name cannot be empty.",
"missions.error.nameDuplicate": "Mission name already exists.",
"missions.error.nameEmpty": "Name cannot be empty.",
"missions.saveSuccess": "Mission saved.",
"missions.editor.discardConfirm": "Discard unsaved changes?",
"missions.queue.status.executing": "Running",
"missions.action.waitOnLevel": "Wait for ON level",
"integrations.modbus.title": "Modbus trigger",
"integrations.modbus.subtitle": "System → Triggers — coils 10012000 map to mission_id. Remote device sets coil (Modbus TCP :5502) → mission queued.",
"integrations.modbus.add": "Add trigger",
"integrations.modbus.empty": "No Modbus triggers yet.",
"integrations.modbus.coilsLabel": "Assigned coils (click to simulate rising edge)",
"integrations.rest.title": "REST API — MiR v2.0.0",
"integrations.rest.subtitle": "External systems POST missions to the queue via REST.",
"integrations.rest.baseUrl": "Base URL",
"integrations.rest.quickTest": "Quick test",
"integrations.rest.postQueue": "POST queue",
"integrations.fleet.title": "MiRFleet — Schedule missions",
"integrations.fleet.subtitle": "Priority, robot assignment, run ASAP or scheduled.",
"integrations.fleet.addSchedule": "Add schedule",
"integrations.fleet.empty": "No fleet schedules yet.",
"integrations.noMissions": "— No missions —",
"integrations.defaultRobot": "Main robot",
"integrations.fireTrigger": "Fire",
"integrations.coilsEmpty": "No coils assigned. Add a trigger above (10012000).",
"integrations.coilState": "coil state: {state}",
"integrations.confirm.deleteTrigger": "Delete this Modbus trigger?",
"integrations.confirm.deleteSchedule": "Delete this fleet schedule?",
"integrations.dialog.trigger.title": "Modbus trigger",
"integrations.dialog.trigger.name": "Trigger name",
"integrations.dialog.trigger.coil": "Coil ID",
"integrations.dialog.trigger.mission": "Mission",
"integrations.dialog.schedule.title": "MiRFleet schedule",
"integrations.dialog.schedule.name": "Schedule name",
"integrations.dialog.schedule.robot": "Robot",
"integrations.dialog.schedule.priority": "Priority",
"integrations.dialog.schedule.mode": "Mode",
"integrations.dialog.schedule.asap": "ASAP",
"integrations.dialog.schedule.scheduled": "Scheduled",
"integrations.dialog.schedule.startTime": "Start time",
"integrations.schedule.runNow": "Run now",
"monitoring.log.title": "System log",
"monitoring.log.subtitle": "Monitoring → System log — system log (coming soon).",
"monitoring.log.placeholder": "Monitoring will show robot logs, alerts and mission history here.",
"help.api.title": "API documentation",
"help.api.subtitle": "Help → API — MiR v2.0.0 REST reference for external integration.",
"help.api.body1": "See endpoint details under System → Integrations or /api/v2.0.0/ docs.",
"help.api.body2": "Reference Guide MiR rev 1.9: docs/Reference guide.pdf",
},
};
const LOCALE_META = {
vi: { flag: "🇻🇳", labelKey: "topbar.localeVi" },
en: { flag: "🇺🇸", labelKey: "topbar.localeEn" },
};
let locale = "vi";
function interpolate(str, vars) {
if (!vars) return str;
return String(str).replace(/\{(\w+)\}/g, (_, k) => (vars[k] != null ? String(vars[k]) : `{${k}}`));
}
function t(key, vars) {
const raw = MESSAGES[locale]?.[key] ?? MESSAGES.en[key] ?? key;
return interpolate(raw, vars);
}
function applyDOM() {
document.querySelectorAll("[data-i18n]").forEach((node) => {
const key = node.dataset.i18n;
if (key) node.textContent = t(key);
});
document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
const key = node.dataset.i18nPlaceholder;
if (key) node.placeholder = t(key);
});
document.querySelectorAll("[data-i18n-title]").forEach((node) => {
const key = node.dataset.i18nTitle;
if (key) node.title = t(key);
});
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
const key = node.dataset.i18nAria;
if (key) node.setAttribute("aria-label", t(key));
});
document.querySelectorAll("option[data-i18n]").forEach((node) => {
const key = node.dataset.i18n;
if (key) node.textContent = t(key);
});
const titleKey = document.documentElement.dataset.i18nTitle;
if (titleKey) document.title = t(titleKey);
}
function syncTopbarLocaleUI() {
const meta = LOCALE_META[locale];
if (!meta) return;
const flagEl = document.getElementById("mirLocaleFlag");
const labelEl = document.getElementById("mirLocaleLabel");
if (flagEl) flagEl.textContent = meta.flag;
if (labelEl) labelEl.textContent = t(meta.labelKey);
}
function setLocale(next, opts = {}) {
locale = MESSAGES[next] ? next : "vi";
try {
localStorage.setItem("lm_locale", locale);
} catch {
/* ignore */
}
document.documentElement.lang = locale;
applyDOM();
syncTopbarLocaleUI();
if (!opts.silent) {
window.dispatchEvent(new CustomEvent("lm:locale-change", { detail: { locale } }));
}
}
function loadLocale() {
try {
const saved = localStorage.getItem("lm_locale");
if (saved && MESSAGES[saved]) locale = saved;
} catch {
/* ignore */
}
setLocale(locale, { silent: true });
window.dispatchEvent(new CustomEvent("lm:locale-change", { detail: { locale } }));
}
window.I18n = {
t,
getLocale: () => locale,
setLocale,
applyDOM,
loadLocale,
LOCALE_META,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadLocale);
} else {
loadLocale();
}
})();