/** * Central i18n for Robot App — 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": "Robot App", "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", "common.yes": "Có", "common.no": "Không", "common.ok": "OK", "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.dashboardsList": "Dashboards", "nav.missions": "Missions", "nav.maps": "Maps", "nav.sounds": "Sounds", "nav.build-robot": "Build Robot", "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.list.title": "Dashboards", "dashboard.list.subtitle": "Tạo và chỉnh sửa dashboard cho robot.", "dashboard.list.create": "+ Tạo dashboard", "dashboard.list.clearFilters": "Xóa bộ lọc", "dashboard.list.filterLabel": "Lọc:", "dashboard.list.filterPlaceholder": "Nhập tên để lọc…", "dashboard.list.itemsFound": "{count} mục", "dashboard.list.pageOf": "Trang {page} / {total}", "dashboard.list.col.name": "Tên", "dashboard.list.col.createdBy": "Tạo bởi", "dashboard.list.col.functions": "Chức năng", "dashboard.list.empty": "Không có dashboard nào.", "dashboard.list.back": "← Danh sách", "dashboard.list.design": "Thiết kế", "dashboard.list.active": "Dashboard đang active", "dashboard.list.edit": "Sửa", "dashboard.list.delete": "Xóa", "dashboard.list.deleteConfirm": "Xóa dashboard «{name}»?", "dashboard.list.cannotDeleteDefault": "Không thể xóa Default Dashboard.", "dashboard.list.noEditPermission": "Bạn không có quyền chỉnh sửa dashboard này.", "dashboard.dialog.create.title": "Tạo dashboard", "dashboard.create.title": "Tạo dashboard", "dashboard.create.subtitle": "Tạo dashboard mới trên robot.", "dashboard.create.backToList": "← Quay lại danh sách", "dashboard.create.name": "Tên", "dashboard.create.namePlaceholder": "VD: John's Dashboard", "dashboard.create.permissions": "Chọn user groups được phép chỉnh sửa dashboard này.", "dashboard.create.permissionsBtn": "Quyền", "dashboard.create.permissionsTitle": "Quyền chỉnh sửa", "dashboard.create.submit": "Tạo dashboard", "dashboard.create.cancel": "Hủy", "dashboard.dialog.editDashboard.title": "Sửa dashboard", "dashboard.designer.empty": "Chưa có widget trên dashboard này.", "dashboard.designer.emptyEdit": "Chưa có widget. Chọn loại widget trên thanh Maps / Missions / Miscellaneous.", "dashboard.designer.dragHint": "Kéo thanh tiêu đề để di chuyển widget trên lưới", "dashboard.designer.configure": "Cấu hình widget", "dashboard.designer.resize": "Kéo để đổi kích thước", "dashboard.designer.save": "Lưu", "dashboard.designer.saved": "Đã lưu", "dashboard.menu.maps": "Maps", "dashboard.menu.missions": "Missions", "dashboard.menu.plc": "PLC Registers", "dashboard.menu.io": "I/O", "dashboard.menu.misc": "Miscellaneous", "dashboard.menu.comingSoon": "Widget nhóm này sẽ có trong bản cập nhật sau.", "dashboard.createdBy.system": "MiR", "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.mission_action_log": "Mission action log", "dashboard.widget.logout_button": "Nút đăng xuất", "dashboard.widget.map_locked": "Locked map", "dashboard.widget.map": "Map", "dashboard.widget.robot_summary": "Robot summary", "dashboard.widget.field.map": "Map", "dashboard.widget.mapActive": "Active map (robot)", "dashboard.widget.mapHint": "Chọn map cố định hoặc để «Active map» dùng map đang gắn với robot.", "dashboard.widget.mapLoading": "Đang tải map…", "dashboard.widget.mapEmpty": "Chưa có map. Distributor tạo map qua API /api/maps.", "dashboard.widget.mapNoImage": "Chưa có ảnh map — upload qua POST /api/maps/{id}/image", "dashboard.widget.mapImageError": "Không tải được ảnh map.", "dashboard.widget.actionLog.empty": "Chưa có action đang chạy.", "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", "maps.title": "Maps", "maps.subtitle": "Tạo và chỉnh sửa map.", "maps.create": "Tạo map", "maps.importSite": "Import site", "maps.clearFilters": "Xóa bộ lọc", "maps.filterLabel": "Lọc:", "maps.filterPlaceholder": "Nhập tên để lọc…", "maps.itemsFound": "{n} mục", "maps.pageOf": "Trang {page} / {total}", "maps.colName": "Tên", "maps.colCreatedBy": "Tạo bởi", "maps.colFunctions": "Chức năng", "maps.empty": "Chưa có map. Bấm Tạo map để bắt đầu.", "maps.emptyFilter": "Không có map khớp bộ lọc.", "maps.activeBadge": "ACTIVE", "maps.activeHint": "Map đang hoạt động: {name}", "maps.view": "Xem", "maps.importComingSoon": "Import site sẽ có trong phiên bản sau.", "maps.helpTitle": "Trợ giúp Maps", "maps.helpText": "Tạo map mới, upload ảnh PNG qua menu ⋮, sau đó kích hoạt map cho robot.", "maps.createDialog.title": "Tạo map", "maps.createDialog.name": "Tên *", "maps.createDialog.site": "Site *", "maps.createDialog.manageSite": "Tạo / Sửa site…", "maps.createDialog.submit": "Tạo map", "maps.createPage.title": "Tạo map", "maps.createPage.subtitle": "Tạo map mới.", "maps.createPage.goBack": "Quay lại", "maps.createPage.name": "Tên", "maps.createPage.namePlaceholder": "Nhập tên map…", "maps.createPage.nameHelp": "Tên hiển thị trong danh sách Maps.", "maps.createPage.site": "Site", "maps.createPage.siteHelp": "Site chứa map trong cơ sở.", "maps.createPage.siteManage": "Tạo / Sửa", "maps.createPage.submit": "Tạo map", "maps.createPage.cancel": "Hủy", "maps.createPage.helpText": "Nhập tên map và chọn site, sau đó bấm Tạo map để mở trình editor.", "maps.siteDialog.create": "Tạo site", "maps.siteDialog.edit": "Sửa site", "maps.siteDialog.name": "Tên *", "maps.siteForm.create": "Tạo site", "maps.siteForm.edit": "Sửa site", "maps.siteForm.name": "Tên *", "maps.sitesDialog.title": "Sites", "maps.sitesDialog.createSite": "Tạo site", "maps.sitesDialog.description": "Site là container chứa maps và dữ liệu cơ sở trên robot.", "maps.sitesDialog.empty": "Chưa có site.", "maps.sitesDialog.deleteConfirm": "Xóa site \"{name}\"?", "maps.deleteConfirm": "Xóa map \"{name}\"?", "maps.error.nameEmpty": "Tên map không được để trống.", "maps.error.noImage": "Map chưa có ảnh — upload PNG trước khi kích hoạt.", "maps.error.pngOnly": "Chỉ chấp nhận file PNG.", "maps.activateDialog.title": "Kích hoạt map?", "maps.activateDialog.text": "Đặt \"{name}\" làm map hoạt động của robot?", "maps.menu.title": "Upload, download and record maps", "maps.menu.uploadOverwrite": "Upload and overwrite", "maps.menu.uploadOverwriteDesc": "Thay map hiện tại bằng map upload.", "maps.menu.uploadAppend": "Upload and append", "maps.menu.uploadAppendDesc": "Upload map mới và ghép vào map hiện tại.", "maps.menu.download": "Download map", "maps.menu.downloadDesc": "Tải map hiện tại.", "maps.menu.recordOverwrite": "Record and overwrite", "maps.menu.recordOverwriteDesc": "Thay map hiện tại bằng bản ghi map mới.", "maps.menu.recordAppend": "Record and append", "maps.menu.recordAppendDesc": "Ghi map mới và ghép vào map hiện tại.", "maps.menu.comingSoon": "Sắp có", "maps.menu.recordHint": "Cần LiDAR", "maps.settings.title": "Cài đặt map", "maps.settings.name": "Tên", "maps.settings.description": "Mô tả", "maps.settings.resolution": "Resolution (m/px)", "maps.settings.originX": "Origin X", "maps.settings.originY": "Origin Y", "maps.settings.originYaw": "Origin yaw", "maps.editor.originLabelShort": "Gốc ({x}, {y})", "maps.editor.originTooltip": "Origin map: X={x} m, Y={y} m, yaw={yaw}°", "maps.uploadConfirm.title": "Ghi đè floor plan?", "maps.uploadConfirm.text": "Ảnh map hiện tại sẽ bị thay thế. Tiếp tục?", "maps.uploadConfirm.yes": "Ghi đè", "maps.uploadMeta.title": "Metadata map (ROS)", "maps.uploadMeta.hint": "Nhập origin, resolution và ngưỡng occupancy — hoặc import file .yaml.", "maps.uploadMeta.importYaml": "Import file YAML…", "maps.uploadMeta.negate": "Negate", "maps.uploadMeta.occupiedThresh": "Occupied thresh", "maps.uploadMeta.freeThresh": "Free thresh", "maps.uploadMeta.continue": "Tiếp tục — chọn PNG", "maps.uploadMeta.invalidResolution": "Resolution phải lớn hơn 0.", "maps.uploadMeta.invalidYaml": "Không đọc được file YAML.", "maps.editor.back": "Maps", "maps.editor.goBack": "Quay lại", "maps.editor.subtitle": "Chỉnh sửa và vẽ map.", "maps.editor.helpTitle": "Trợ giúp map editor", "maps.editor.helpText": "Ba lớp: View (pan/zoom màn hình) → Image (pixel floor plan, 20 px/m) → World (X,Y mét). Dùng Pan, zoom, Fit; di chuột để xem tọa độ.", "maps.editor.toolbarAria": "Mapping tools", "maps.editor.canvasTip": "Kéo map để di chuyển vùng nhìn hoặc dùng nút zoom in/out để phóng to/thu nhỏ.", "maps.editor.unsaved": "Chưa lưu", "maps.editor.unsavedLeave": "Có thay đổi chưa lưu. Rời editor?", "maps.editor.menu": "Menu", "maps.editor.undo": "Hoàn tác", "maps.editor.save": "Lưu", "maps.editor.settings": "Cài đặt", "maps.editor.tool.search": "Tìm kiếm", "maps.editor.tool.save": "Lưu map", "maps.editor.tool.pan": "Pan — di chuyển vùng nhìn", "maps.editor.tool.crosshair": "Crosshair", "maps.editor.tool.origin": "Hiển thị gốc tọa độ", "maps.editor.tool.center": "Căn giữa vùng nhìn", "maps.editor.tool.lidar": "Hiển thị LiDAR", "maps.editor.tool.waypoints": "Vị trí / waypoint", "maps.editor.fit": "Vừa khung", "maps.editor.zoomIn": "Phóng to", "maps.editor.zoomOut": "Thu nhỏ", "maps.editor.noData": "Chưa có floor plan — menu ⋮ để upload PNG.", "maps.editor.statusView": "zoom {zoom}% · pan ({panX}, {panY})", "maps.editor.statusImageIdle": "— px (di chuột trên map)", "maps.editor.statusImage": "({px}, {py}) px", "maps.editor.statusWorldIdle": "— m", "maps.editor.statusWorld": "X {x}, Y {y} m", "maps.editor.objectTypesNone": "Chưa chọn object-type", "maps.editor.objectType.wall": "Walls", "maps.editor.objectType.floor": "Floors", "maps.editor.objectType.position": "Positions", "maps.editor.objectType.forbidden": "Forbidden zones", "maps.editor.objectType.preferred": "Preferred zones", "maps.editor.objectType.unpreferred": "Unpreferred zones", "maps.editor.objectType.speed": "Speed zones", "maps.editor.objectType.sound": "Sound zones", "maps.editor.objectType.directional": "Directional zones", "maps.editor.objectType.directionalLine": "Directional lines", "maps.editor.objectType.planner": "Planner zones", "maps.editor.objectType.io": "I/O zones", "maps.editor.filter.walls": "Walls", "maps.editor.filter.floors": "Floors", "maps.editor.filter.positions": "Positions", "maps.editor.filter.forbidden": "Forbidden", "maps.editor.filter.preferred": "Preferred", "maps.editor.filter.unpreferred": "Unpreferred", "maps.editor.filter.speed": "Speed", "maps.editor.filter.sound": "Sound", "maps.editor.tool.drawLine": "Vẽ line — polyline (wall, drive zone line…)", "maps.editor.tool.drawShape": "Vẽ shape — polygon (floor, zones…)", "maps.editor.drawLine.title": "Vẽ line mới", "maps.editor.drawLine.lineWidth": "Độ rộng line (cm)", "maps.editor.drawLine.hint": "Chỉnh độ rộng line (cm), xác nhận, rồi click trên map để đặt các điểm.", "maps.editor.drawLine.confirm": "Xác nhận", "maps.editor.drawLine.lineWidthInvalid": "Nhập độ rộng line hợp lệ (wall ≥ 5 cm, zone line ≥ 10 cm).", "maps.editor.tool.draw": "Vẽ — line hoặc polygon (walls, zones…)", "maps.editor.tool.select": "Chọn object", "maps.editor.tool.erase": "Tẩy object", "maps.editor.tool.eraser": "Tẩy pixel — xóa noise trên floor plan (Walls/Floors layer)", "maps.editor.tool.eraseShape": "Xóa line/shape đã vẽ", "maps.editor.tool.eraseSelection": "Xóa vùng chọn — tẩy pixel trong khung", "maps.editor.tool.confirmDraw": "Xác nhận hình vẽ", "maps.editor.drawNeedMorePoints": "Cần thêm điểm trước khi xác nhận (line ≥ 2, polygon ≥ 3).", "maps.editor.position.title": "Position", "maps.editor.position.name": "Tên", "maps.editor.position.x": "X (m)", "maps.editor.position.y": "Y (m)", "maps.editor.position.yaw": "Hướng (°)", "maps.editor.position.hint": "Click map và kéo để đặt hướng, rồi xác nhận.", "maps.editor.position.invalid": "Nhập X, Y và hướng hợp lệ.", "maps.editor.speed.title": "Speed zone", "maps.editor.speed.limit": "Giới hạn tốc độ (m/s)", "maps.editor.speed.hint": "Robot giảm tốc trong vùng này (0.1–1.5 m/s).", "maps.editor.speed.invalid": "Nhập tốc độ hợp lệ (0.1–1.5 m/s).", "maps.editor.sound.title": "Sound zone", "maps.editor.sound.select": "Sound", "maps.editor.sound.noSound": "— Chọn sound —", "maps.editor.sound.manage": "Quản lý sounds tại Setup → Sounds", "maps.editor.sound.invalid": "Chọn một sound.", "maps.editor.directional.title": "Directional zone", "maps.editor.directional.direction": "Hướng", "maps.editor.directional.degOption": "{deg}°", "maps.editor.directional.shapeHint": "Robot không được di chuyển ngược hướng mũi tên (bước 45°).", "maps.editor.directional.lineWidth": "Độ rộng line (cm)", "maps.editor.directional.reversed": "Đảo hướng", "maps.editor.directional.lineHint": "Hướng theo line từ điểm đầu đến điểm cuối.", "maps.editor.directional.lineWidthInvalid": "Nhập độ rộng line hợp lệ (≥ 10 cm).", "maps.editor.planner.title": "Planner zone", "maps.editor.planner.noLocalization": "No localization (chỉ encoder)", "maps.editor.planner.lookAhead": "Look-ahead (thu hẹp field of view)", "maps.editor.planner.ignoreObstacles": "Ignore obstacles", "maps.editor.planner.pathDeviation": "Path deviation (m)", "maps.editor.planner.pathTimeout": "Path timeout (s)", "maps.editor.io.title": "I/O zone", "maps.editor.io.module": "I/O module", "maps.editor.io.plcRegister": "PLC register", "maps.editor.io.plcValue": "Giá trị", "maps.editor.io.plcMode": "PLC mode", "maps.editor.io.plcModeSet": "Set", "maps.editor.io.plcModeAdd": "Add", "maps.editor.io.plcModeSubtract": "Subtract", "maps.editor.io.hint": "Robot kích hoạt I/O khi vào vùng này.", "maps.editor.io.moduleRequired": "Nhập tên I/O module.", "maps.menu.save": "Lưu map", "sounds.title": "Sounds", "sounds.subtitle": "Setup → Sounds — upload và quản lý âm thanh robot cho sound zones.", "sounds.create": "Tạo sound", "sounds.createTitle": "Tạo sound", "sounds.editTitle": "Sửa sound", "sounds.empty": "Chưa có sound. Tạo mới để dùng trong sound zones.", "sounds.name": "Tên", "sounds.description": "Mô tả", "sounds.enabled": "Bật", "sounds.file": "File âm thanh", "sounds.noFile": "Chưa có file", "sounds.upload": "Upload file…", "sounds.play": "Phát", "sounds.playFailed": "Không phát được file.", "sounds.fileMeta": "{name} · {duration}", "sounds.nameRequired": "Nhập tên sound.", "sounds.deleteConfirm": "Xóa sound này?", "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": "Robot App", "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", "common.yes": "Yes", "common.no": "No", "common.ok": "OK", "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.dashboardsList": "Dashboards", "nav.missions": "Missions", "nav.maps": "Maps", "nav.sounds": "Sounds", "nav.build-robot": "Build Robot", "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.list.title": "Dashboards", "dashboard.list.subtitle": "Create and edit dashboards for the robot.", "dashboard.list.create": "+ Create dashboard", "dashboard.list.clearFilters": "Clear filters", "dashboard.list.filterLabel": "Filter:", "dashboard.list.filterPlaceholder": "Write name to filter by…", "dashboard.list.itemsFound": "{count} item(s) found", "dashboard.list.pageOf": "Page {page} of {total}", "dashboard.list.col.name": "Name", "dashboard.list.col.createdBy": "Created by", "dashboard.list.col.functions": "Functions", "dashboard.list.empty": "No dashboards found.", "dashboard.list.back": "← Back to list", "dashboard.list.design": "Design", "dashboard.list.active": "Active dashboard", "dashboard.list.edit": "Edit", "dashboard.list.delete": "Delete", "dashboard.list.deleteConfirm": "Delete dashboard «{name}»?", "dashboard.list.cannotDeleteDefault": "Cannot delete Default Dashboard.", "dashboard.list.noEditPermission": "You do not have permission to edit this dashboard.", "dashboard.dialog.create.title": "Create dashboard", "dashboard.create.title": "Create dashboard", "dashboard.create.subtitle": "Create a new dashboard in the robot.", "dashboard.create.backToList": "← Back to the list", "dashboard.create.name": "Name", "dashboard.create.namePlaceholder": "John's Dashboard", "dashboard.create.permissions": "Select user groups allowed to edit this dashboard.", "dashboard.create.permissionsBtn": "Permissions", "dashboard.create.permissionsTitle": "Permissions", "dashboard.create.submit": "Create dashboard", "dashboard.create.cancel": "Cancel", "dashboard.dialog.editDashboard.title": "Edit dashboard", "dashboard.designer.empty": "This dashboard has no widgets yet.", "dashboard.designer.emptyEdit": "No widgets yet. Pick a type from the Maps / Missions / Miscellaneous toolbar.", "dashboard.designer.dragHint": "Drag the header bar to move the widget on the grid", "dashboard.designer.configure": "Configure widget", "dashboard.designer.resize": "Drag to resize", "dashboard.designer.save": "Save", "dashboard.designer.saved": "Saved", "dashboard.menu.maps": "Maps", "dashboard.menu.missions": "Missions", "dashboard.menu.plc": "PLC Registers", "dashboard.menu.io": "I/O", "dashboard.menu.misc": "Miscellaneous", "dashboard.menu.comingSoon": "Widgets in this category are coming in a future release.", "dashboard.createdBy.system": "MiR", "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.mission_action_log": "Mission action log", "dashboard.widget.logout_button": "Log-out button", "dashboard.widget.map_locked": "Locked map", "dashboard.widget.map": "Map", "dashboard.widget.robot_summary": "Robot summary", "dashboard.widget.field.map": "Map", "dashboard.widget.mapActive": "Active map (robot)", "dashboard.widget.mapHint": "Pick a fixed map or leave «Active map» to follow the robot's current map.", "dashboard.widget.mapLoading": "Loading map…", "dashboard.widget.mapEmpty": "No maps yet. A Distributor can create maps via /api/maps.", "dashboard.widget.mapNoImage": "No map image yet — upload via POST /api/maps/{id}/image", "dashboard.widget.mapImageError": "Could not load the map image.", "dashboard.widget.actionLog.empty": "No running action to show.", "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", "maps.title": "Maps", "maps.subtitle": "Create and edit maps.", "maps.create": "Create map", "maps.importSite": "Import site", "maps.clearFilters": "Clear filters", "maps.filterLabel": "Filter:", "maps.filterPlaceholder": "Write name to filter by...", "maps.itemsFound": "{n} item(s) found", "maps.pageOf": "Page {page} of {total}", "maps.colName": "Name", "maps.colCreatedBy": "Created by", "maps.colFunctions": "Functions", "maps.empty": "No maps yet. Click Create map to get started.", "maps.emptyFilter": "No maps match the filter.", "maps.activeBadge": "ACTIVE", "maps.activeHint": "Active map: {name}", "maps.view": "View", "maps.importComingSoon": "Import site will be available in a future release.", "maps.helpTitle": "Maps help", "maps.helpText": "Create a new map, upload a PNG via the ⋮ menu, then activate the map for the robot.", "maps.createDialog.title": "Create map", "maps.createDialog.name": "Name *", "maps.createDialog.site": "Site *", "maps.createDialog.manageSite": "Create / Edit site…", "maps.createDialog.submit": "Create map", "maps.createPage.title": "Create map", "maps.createPage.subtitle": "Create a new map.", "maps.createPage.goBack": "Go back", "maps.createPage.name": "Name", "maps.createPage.namePlaceholder": "Enter the map's name...", "maps.createPage.nameHelp": "Display name shown in the Maps list.", "maps.createPage.site": "Site", "maps.createPage.siteHelp": "The facility site that contains this map.", "maps.createPage.siteManage": "Create / Edit", "maps.createPage.submit": "Create map", "maps.createPage.cancel": "Cancel", "maps.createPage.helpText": "Enter a map name and select a site, then click Create map to open the editor.", "maps.siteDialog.create": "Create site", "maps.siteDialog.edit": "Edit site", "maps.siteDialog.name": "Name *", "maps.siteForm.create": "Create site", "maps.siteForm.edit": "Edit site", "maps.siteForm.name": "Name *", "maps.sitesDialog.title": "Sites", "maps.sitesDialog.createSite": "Create site", "maps.sitesDialog.description": "A site is a container for maps and other facility data on the robot.", "maps.sitesDialog.empty": "No sites yet.", "maps.sitesDialog.deleteConfirm": "Delete site \"{name}\"?", "maps.deleteConfirm": "Delete map \"{name}\"?", "maps.error.nameEmpty": "Map name is required.", "maps.error.noImage": "Map has no image — upload a PNG before activating.", "maps.error.pngOnly": "Only PNG files are accepted.", "maps.activateDialog.title": "Activate map?", "maps.activateDialog.text": "Set \"{name}\" as the robot's active map?", "maps.menu.title": "Upload, download and record maps", "maps.menu.uploadOverwrite": "Upload and overwrite", "maps.menu.uploadOverwriteDesc": "Replace existing map with uploaded map.", "maps.menu.uploadAppend": "Upload and append", "maps.menu.uploadAppendDesc": "Upload a new map and append it to current map.", "maps.menu.download": "Download map", "maps.menu.downloadDesc": "Download the current map.", "maps.menu.recordOverwrite": "Record and overwrite", "maps.menu.recordOverwriteDesc": "Replace existing map with new recording of map.", "maps.menu.recordAppend": "Record and append", "maps.menu.recordAppendDesc": "Record a new map and append it to current map.", "maps.menu.comingSoon": "Coming soon", "maps.menu.recordHint": "Requires LiDAR", "maps.settings.title": "Map settings", "maps.settings.name": "Name", "maps.settings.description": "Description", "maps.settings.resolution": "Resolution (m/px)", "maps.settings.originX": "Origin X", "maps.settings.originY": "Origin Y", "maps.settings.originYaw": "Origin yaw", "maps.editor.originLabelShort": "Origin ({x}, {y})", "maps.editor.originTooltip": "Map origin: X={x} m, Y={y} m, yaw={yaw}°", "maps.uploadConfirm.title": "Overwrite floor plan?", "maps.uploadConfirm.text": "The current map image will be replaced. Continue?", "maps.uploadConfirm.yes": "Overwrite", "maps.uploadMeta.title": "Map metadata", "maps.uploadMeta.hint": "Enter origin, resolution, and occupancy thresholds — or import a .yaml file.", "maps.uploadMeta.importYaml": "Import YAML file…", "maps.uploadMeta.negate": "Negate", "maps.uploadMeta.occupiedThresh": "Occupied thresh", "maps.uploadMeta.freeThresh": "Free thresh", "maps.uploadMeta.continue": "Continue — choose PNG", "maps.uploadMeta.invalidResolution": "Resolution must be greater than 0.", "maps.uploadMeta.invalidYaml": "Could not read YAML file.", "maps.editor.back": "Maps", "maps.editor.goBack": "Go back", "maps.editor.subtitle": "Edit and draw the map.", "maps.editor.helpTitle": "Map editor help", "maps.editor.helpText": "Three layers: View (screen pan/zoom) → Image (floor plan pixels, 20 px/m) → World (X,Y in metres). Use Pan, zoom, Fit; hover for coordinates.", "maps.editor.toolbarAria": "Mapping tools", "maps.editor.canvasTip": "Drag the map to move your view or use the zoom-in and -out buttons to zoom.", "maps.editor.unsaved": "Unsaved", "maps.editor.unsavedLeave": "You have unsaved changes. Leave the editor?", "maps.editor.menu": "Menu", "maps.editor.undo": "Undo", "maps.editor.save": "Save", "maps.editor.settings": "Settings", "maps.editor.tool.search": "Search", "maps.editor.tool.save": "Save map", "maps.editor.tool.pan": "Pan — move view", "maps.editor.tool.crosshair": "Crosshair", "maps.editor.tool.origin": "Show map origin", "maps.editor.tool.center": "Center view", "maps.editor.tool.lidar": "LiDAR overlay", "maps.editor.tool.waypoints": "Positions", "maps.editor.fit": "Fit to view", "maps.editor.zoomIn": "Zoom in", "maps.editor.zoomOut": "Zoom out", "maps.editor.noData": "No floor plan — ⋮ menu to upload PNG.", "maps.editor.statusView": "zoom {zoom}% · pan ({panX}, {panY})", "maps.editor.statusImageIdle": "— px (hover map)", "maps.editor.statusImage": "({px}, {py}) px", "maps.editor.statusWorldIdle": "— m", "maps.editor.statusWorld": "X {x}, Y {y} m", "maps.editor.objectTypesNone": "No object-type selected", "maps.editor.objectType.wall": "Walls", "maps.editor.objectType.floor": "Floors", "maps.editor.objectType.position": "Positions", "maps.editor.objectType.forbidden": "Forbidden zones", "maps.editor.objectType.preferred": "Preferred zones", "maps.editor.objectType.unpreferred": "Unpreferred zones", "maps.editor.objectType.speed": "Speed zones", "maps.editor.objectType.sound": "Sound zones", "maps.editor.objectType.directional": "Directional zones", "maps.editor.objectType.directionalLine": "Directional lines", "maps.editor.objectType.planner": "Planner zones", "maps.editor.objectType.io": "I/O zones", "maps.editor.filter.walls": "Walls", "maps.editor.filter.floors": "Floors", "maps.editor.filter.positions": "Positions", "maps.editor.filter.forbidden": "Forbidden", "maps.editor.filter.preferred": "Preferred", "maps.editor.filter.unpreferred": "Unpreferred", "maps.editor.filter.speed": "Speed", "maps.editor.filter.sound": "Sound", "maps.editor.tool.drawLine": "Draw line — polyline (wall, drive zone line…)", "maps.editor.tool.drawShape": "Draw shape — polygon (floor, zones…)", "maps.editor.drawLine.title": "Draw a new line", "maps.editor.drawLine.lineWidth": "Line width (cm)", "maps.editor.drawLine.hint": "Set the line width in cm, confirm, then click on the map to place points.", "maps.editor.drawLine.confirm": "Confirm", "maps.editor.drawLine.lineWidthInvalid": "Enter a valid line width (wall ≥ 5 cm, zone line ≥ 10 cm).", "maps.editor.tool.draw": "Draw — line or polygon (walls, zones…)", "maps.editor.tool.select": "Select object", "maps.editor.tool.erase": "Eraser", "maps.editor.tool.eraser": "Pixel eraser — remove noise on floor plan (Walls/Floors layer)", "maps.editor.tool.eraseShape": "Erase shape or line", "maps.editor.tool.eraseSelection": "Erase by selection — clear pixels in rectangle", "maps.editor.tool.confirmDraw": "Confirm shape", "maps.editor.drawNeedMorePoints": "Add more points before confirming (line ≥ 2, polygon ≥ 3).", "maps.editor.position.title": "Position", "maps.editor.position.name": "Name", "maps.editor.position.x": "X (m)", "maps.editor.position.y": "Y (m)", "maps.editor.position.yaw": "Orientation (°)", "maps.editor.position.hint": "Click map and drag to set orientation, then confirm.", "maps.editor.position.invalid": "Enter valid X, Y and orientation.", "maps.editor.speed.title": "Speed zone", "maps.editor.speed.limit": "Speed limit (m/s)", "maps.editor.speed.hint": "Robot slows to this speed inside the zone (0.1–1.5 m/s).", "maps.editor.speed.invalid": "Enter a valid speed (0.1–1.5 m/s).", "maps.editor.sound.title": "Sound zone", "maps.editor.sound.select": "Sound", "maps.editor.sound.noSound": "— Select sound —", "maps.editor.sound.manage": "Manage sounds in Setup → Sounds", "maps.editor.sound.invalid": "Select a sound.", "maps.editor.directional.title": "Directional zone", "maps.editor.directional.direction": "Direction", "maps.editor.directional.degOption": "{deg}°", "maps.editor.directional.shapeHint": "Robot cannot move opposite to the arrow (45° steps).", "maps.editor.directional.lineWidth": "Line width (cm)", "maps.editor.directional.reversed": "Reverse direction", "maps.editor.directional.lineHint": "Direction follows the line from first to last point.", "maps.editor.directional.lineWidthInvalid": "Enter a valid line width (≥ 10 cm).", "maps.editor.planner.title": "Planner zone", "maps.editor.planner.noLocalization": "No localization (encoders only)", "maps.editor.planner.lookAhead": "Look-ahead (narrow field of view)", "maps.editor.planner.ignoreObstacles": "Ignore obstacles", "maps.editor.planner.pathDeviation": "Path deviation (m)", "maps.editor.planner.pathTimeout": "Path timeout (s)", "maps.editor.io.title": "I/O zone", "maps.editor.io.module": "I/O module", "maps.editor.io.plcRegister": "PLC register", "maps.editor.io.plcValue": "Value", "maps.editor.io.plcMode": "PLC mode", "maps.editor.io.plcModeSet": "Set", "maps.editor.io.plcModeAdd": "Add", "maps.editor.io.plcModeSubtract": "Subtract", "maps.editor.io.hint": "Robot activates I/O when entering the zone.", "maps.editor.io.moduleRequired": "Enter an I/O module name.", "maps.menu.save": "Save map", "sounds.title": "Sounds", "sounds.subtitle": "Setup → Sounds — upload and manage robot sounds for sound zones.", "sounds.create": "Create sound", "sounds.createTitle": "Create sound", "sounds.editTitle": "Edit sound", "sounds.empty": "No sounds yet. Create one to use in sound zones.", "sounds.name": "Name", "sounds.description": "Description", "sounds.enabled": "Enabled", "sounds.file": "Audio file", "sounds.noFile": "No file uploaded", "sounds.upload": "Upload file…", "sounds.play": "Play", "sounds.playFailed": "Could not play file.", "sounds.fileMeta": "{name} · {duration}", "sounds.nameRequired": "Enter a sound name.", "sounds.deleteConfirm": "Delete this sound?", "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(); } })();