/** * 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 1001–2000 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 (1001–2000).", "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 1001–2000 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 (1001–2000).", "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(); } })();