diff --git a/src/App.vue b/src/App.vue index 27dd5e7..fa0dd1e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4448,6 +4448,7 @@ const languageLabelKeys: Record = { de: 'language.german', cs: 'language.czech', es: 'language.spanish', + vi: 'language.vietnamese', }; const languageOptions = computed(() => supportedLocales.map(code => ({ diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 59b3eea..8ec62c2 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -629,6 +629,7 @@ const cs = { german: 'German', czech: 'Čeština', spanish: 'Español', + vietnamese: 'Tiếng Việt', switchTo: 'Přepnout do jazyka {language}', }, theme: { diff --git a/src/locales/de.ts b/src/locales/de.ts index 787e9a2..5434de1 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -629,6 +629,7 @@ const de = { german: 'Deutsch', czech: 'Tschechisch', spanish: 'Spanisch', + vietnamese: 'Vietnamesisch', switchTo: 'Zu {language} wechseln', }, theme: { diff --git a/src/locales/en.ts b/src/locales/en.ts index cddb6ff..11bf7bc 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -629,6 +629,7 @@ const en = { german: 'German', czech: 'Czech', spanish: 'Spanish', + vietnamese: 'Vietnamese', switchTo: 'Switch to {language}', }, theme: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 0f417fa..9cbe26a 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -641,6 +641,7 @@ const es = { german: 'German', czech: 'Czech', spanish: 'Español', + vietnamese: 'Vietnamese', switchTo: 'Cambiar a {language}', }, theme: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index a13ee3b..c91914c 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -629,6 +629,7 @@ const fr = { german: 'Allemand', czech: 'Tchèque', spanish: 'Espagnol', + vietnamese: 'Vietnamien', switchTo: 'Passer à {language}', }, theme: { diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 945e7b1..a799df4 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -628,6 +628,7 @@ const tr = { german: 'Almanca', czech: 'Çekçe', spanish: 'İspanyolca', + vietnamese: 'Vietnamca', switchTo: '{language} diline geç', }, theme: { diff --git a/src/locales/vi.ts b/src/locales/vi.ts new file mode 100644 index 0000000..a445fbc --- /dev/null +++ b/src/locales/vi.ts @@ -0,0 +1,647 @@ +const vi = { + app: { + title: 'ESPConnect', + sectionsLabel: 'Sections', + resourcesLabel: 'Tài nguyên', + }, + navigation: { + deviceInfo: 'Thông tin thiết bị', + partitions: 'Phân vùng', + apps: 'Ứng dụng', + nvs: 'Trình kiểm tra NVS', + spiffs: 'Công cụ SPIFFS', + littlefs: 'Công cụ LittleFS', + fatfs: 'Công cụ FATFS', + flash: 'Công cụ Flash', + console: 'Trình giám sát serial', + log: 'Sesion Log', + about: 'Giới thiệu', + makerTools: 'Maker Tools', + }, + resources: { + tutorial: 'Hướng dẫn', + buyCoffee: 'Buy Me a Coffee', + donatePaypal: 'Donate via PayPal', + getHelp: 'Nhận trợ giúp', + }, + makerTools: { + title: 'Maker Tools', + subtitle: 'Các tiện ích Maker từ The Last Outpost Workshop cho ESP32 và các dự án nhúng.', + thumbnailAlt: '{title} tutorial thumbnail', + support: { + title: 'Hỗ trợ dự án', + description: + 'Những tiện ích Maker này và ESPConnect đều miễn phí để sử dụng. Nếu chúng giúp ích cho bạn ở bàn làm việc, một ly cà phê sẽ hỗ trợ quá trình phát triển tiếp tục.', + action: 'Mua cho tôi một ly cà phê', + }, + actions: { + openTool: 'Mở công cụ', + watchTutorial: 'Xem hướng dẫn', + watchSpecific: 'Xem hướng dẫn {title}', + }, + items: { + espPinoutExplorer: { + title: 'ESP Pinout Explorer', + description: + 'Một tài liệu tham khảo tương tác ở bàn làm việc cho các bo mạch phát triển, mô-đun và chip packages. Tìm chân, chức năng, header labels và các ràng buộc về dây dẫn đã được ghi nhận trước khi kết nối phần cứng.', + sourceLabel: 'thelastoutpostworkshop/esp-pinout-explorer', + }, + espBoardVault: { + title: 'ESP Board Vault', + description: + 'ESP Board Vault is a free local-first desktop inventory app that helps ESP32 makers scan, identify, organize, and track their boards. Create board records with hardware details, partition maps, photos, projects, checklists, and backups. No account, no cloud backend, no telemetry - everything stays on your computer.', + sourceLabel: 'thelastoutpostworkshop/ESPVault', + }, + partitionBuilder: { + title: 'ESP32 Partition Builder', + description: + 'ESP32 Partition Builder giúp lên kế hoạch và tạo bố cục phân vùng tùy chỉnh cho các bo mạch ESP32.', + sourceLabel: 'thelastoutpostworkshop/ESP32PartitionBuilder', + }, + videoConversion: { + title: 'Video Conversion Studio', + description: + 'Video Conversion Studio chuyển đổi video assets cho màn hình nhúng. Sử dụng tệp video hoặc âm thanh thông thường và biến chúng thành đầu ra phù hợp với các dự án hiển thị ESP32.', + sourceLabel: 'thelastoutpostworkshop/video_conversion', + }, + gpioViewer: { + title: 'GPIOViewer', + description: + 'GPIOViewer cung cấp trực quan hóa thời gian thực về hoạt động chân GPIO trực tiếp trong trình duyệt web, giúp dễ dàng kiểm tra trạng thái chân, xác nhận hành vi bo mạch và khắc phục lỗi dây dẫn hoặc hoạt động tín hiệu.', + sourceLabel: 'thelastoutpostworkshop/gpio_viewer', + }, + arduinoMakerWorkshop: { + title: 'Arduino Maker Workshop', + description: + 'Arduino Maker Workshop là một tiện ích mở rộng của VS Code dành cho phát triển maker theo hướng Arduino, mang đến luồng làm việc tập trung trong trình soạn thảo cho các dự án dựa trên sketch và lặp lại theo hướng bo mạch.', + sourceLabel: 'Tiện ích mở rộng VS Code Marketplace', + }, + }, + }, + actions: { + connect: 'Kết nối', + disconnect: 'Ngắt kết nối', + }, + forms: { + baudRate: 'Tốc độ Baud', + }, + tooltips: { + higherBaud: 'Có thể sử dụng baudrate cao hơn', + }, + alerts: { + browserUnsupported: + 'Trình duyệt này không hỗ trợ Web Serial API. Hãy dùng Chrome, Edge hoặc một trình duyệt dựa trên Chromium khác.', + serialMonitorClosed: 'Trình giám sát nối tiếp đã đóng - nhấp Kết nối để quay lại chế độ bảo trì.', + }, + sessionLog: { + title: 'Nhật ký phiên', + actions: { + copy: 'Sao chép', + clear: 'Xóa', + }, + emptyState: 'Nhật ký sẽ xuất hiện ở đây sau khi có các hành động bắt đầu.', + copySuccess: 'Đã sao chép nhật ký phiên vào clipboard.', + copyError: 'Không thể sao chép nhật ký. Vui lòng thử lại.', + chipMetadata: 'Chip metadata: pkgVersion={pkgVersion}, chipRevision={chipRevision}', + chipId: 'ID chip: {chipId}', + }, + serialMonitor: { + title: 'Serial Monitor', + status: { + live: 'Trực tiếp', + stopped: 'Đã dừng', + }, + actions: { + start: 'Bắt đầu', + stop: 'Dừng', + pause: 'Tạm dừng', + resume: 'Tiếp tục', + copy: 'Sao chép', + clear: 'Xóa', + reset: 'Đặt lại', + send: 'Gửi', + }, + filterPlaceholder: 'Lọc đầu ra', + inputPlaceholder: 'Gửi văn bản (Ctrl+C)', + subtitle: + 'Console chạy ở 115200 bps tự động để có đầu ra ổn định. Quá trình flash sẽ dùng tốc độ baud đã chọn trong thanh công cụ.', + info: + 'Bắt đầu trình giám sát nối tiếp sẽ đặt lại bo mạch vào chế độ firmware bình thường để bạn xem đầu ra UART. Dừng trình giám sát nối tiếp sẽ tự động quay lại chế độ bootloader (stub) để bảo trì (flash, công cụ phân vùng, v.v.).', + emptyState: 'Đầu ra của trình giám sát sẽ xuất hiện ở đây sau khi bắt đầu.', + copySuccess: 'Đã sao chép đầu ra trình giám sát nối tiếp vào clipboard.', + copyError: 'Không thể sao chép đầu ra cuar serial monitor. Vui lòng thử lại.', + }, + serialPortPicker: { + title: 'Chọn cổng nối tiếp', + subtitle: 'Chọn cổng được kết nối với thiết bị ESP của bạn.', + recommended: 'Khuyến nghị', + unknownPort: 'Cổng không xác định', + noDetails: 'Không có chi tiết thiết bị nào', + usbId: 'USB ID {vendorId}:{productId}', + serialNumber: 'Số seri {serialNumber}', + actions: { + select: 'Chọn', + }, + }, + dialogs: { + connecting: 'Đang kết nối', + littlefsBackup: 'Sao lưu LittleFS', + littlefsLoading: 'Đang tải LittleFS', + littlefsSaving: 'Đang lưu LittleFS', + preparingBackup: 'Đang chuẩn bị bản sao lưu...', + writingLittlefs: 'Đang ghi hình ảnh LittleFS...', + littlefsRestoring: 'Đang khôi phục LittleFS', + fatfsBackup: 'Sao lưu FATFS', + spiffsLoading: 'Đang đọc SPIFFS...', + spiffsSaving: 'Đang lưu SPIFFS...', + spiffsRestoring: 'Đang khôi phục hình ảnh SPIFFS...', + fatfsLoading: 'Đang đọc FATFS...', + fatfsSaving: 'Đang lưu FATFS...', + fatfsRestoring: 'Đang khôi phục hình ảnh FATFS...', + cancel: 'Hủy', + confirm: 'Xác nhận', + continue: 'Tiếp tục', + confirmationTitle: 'Vui lòng xác nhận', + returningToMaintenance: 'Đang quay lại chế độ bảo trì...', + reEnteringBootloader: 'Đang nhập lại ROM bootloader...', + connectingToDevice: 'Đang kết nối với thiết bị ESP...', + openingSerialPort: 'Đang mở cổng nối tiếp...', + restoringBaud: 'Đang khôi phục baud về {baud} bps...', + handshakingBootloader: 'Đang bắt tay với ROM bootloader...', + readingFlashSize: 'Đang đọc kích thước Flash...', + preparingInformation: 'Đang chuẩn bị thông tin...', + readingPartitionTable: 'Đang đọc bảng phân vùng...', + reconnectingStub: 'Kết nối lại và đồng bộ với stub', + loadingStubFlasher: 'Đang tải stub flasher...', + gettingSecurityInfo: 'Đang lấy thông tin bảo mật...', + }, + disconnected: { + defaultTitle: 'Chưa có thiết bị nào được kết nối', + defaultSubtitle: 'Kết nối với ESP32 để tiếp tục.', + deviceInfo: 'Kết nối với ESP32 để xem thông tin thiết bị.', + nvs: 'Kết nối với ESP32 có phân vùng NVS để kiểm tra các cặp khóa/giá trị đã lưu.', + spiffs: 'Kết nối với ESP32 để duyệt và chỉnh sửa tệp SPIFFS.', + littlefs: 'Kết nối với ESP32 có phân vùng LittleFS để sử dụng các công cụ này.', + fatfs: 'Kết nối với ESP32 có phân vùng FATFS để sử dụng các công cụ này.', + apps: 'Kết nối với thiết bị để kiểm tra các khe ứng dụng OTA.', + flash: 'Kết nối với bảng mạch của bạn để flash firmware hoặc kiểm tra thanh ghi.', + }, + deviceInfo: { + summary: { + flashClock: 'Flash & Đồng hồ', + featureSet: 'Tập tính năng', + capabilities: '{count} tính năng', + noFeatures: 'Không có tính năng nào được báo cáo', + more: '+{count} thêm', + noOptionalCapabilities: 'Không có tính năng tùy chọn nào.', + }, + crystal: 'Crystal {crystal}', + unknown: 'Không xác định', + facts: { + groups: { + package: 'Gói & Phiên bản', + embeddedMemory: 'Bộ nhớ nhúng', + security: 'Bảo mật', + peripherals: 'Thiết bị ngoại vi', + connection: 'Kết nối', + documentation: 'Tài liệu', + }, + labels: { + chipVariant: 'Biến thể Chip', + packageFormFactor: 'Hình thức Gói', + revision: 'Phiên bản', + cpuCores: 'Số lõi CPU', + maxCpuFrequency: 'Tần số CPU tối đa', + embeddedFlash: 'Embedded Flash', + embeddedPsr: 'Embedded PSRAM', + flashId: 'Flash ID', + flashManufacturer: 'Nhà sản xuất Flash', + flashDevice: 'Thiết bị Flash', + flashVendorEfuse: 'Nhà cung cấp Flash (eFuse)', + psramVendorEfuse: 'Nhà cung cấp PSRAM (eFuse)', + efuseBlockVersion: 'Phiên bản khối eFuse', + flashEncryption: 'Mã hóa Flash', + flashEncryptionDetails: 'Chi tiết Mã hóa Flash', + flashEncryptionMode: 'Chế độ Mã hóa Flash', + secureBoot: 'Secure Boot', + secureBootType: 'Loại Secure Boot', + jtagProtection: 'Bảo vệ JTAG', + usbProtection: 'Bảo vệ USB', + downloadModeCaches: 'Bộ nhớ cache Chế độ Tải xuống', + securityNote: 'Ghi chú Bảo mật', + pwmLedc: 'PWM/LEDC', + usbBridge: 'USB Bridge', + connectionBaud: 'Baud Kết nối', + hardwareReference: 'Tham chiếu Phần cứng', + datasheet: 'Datasheet', + technicalReferenceManual: 'Sổ tay Tham chiếu Kỹ thuật', + errata: 'Errata', + hardwareDesignGuidelines: 'Hướng dẫn Thiết kế Phần cứng', + }, + pwmTooltip: + 'Tính năng PWM/LEDC dựa trên dòng chip, không phải trên dữ liệu thời gian thực được đọc từ thiết bị.', + }, + }, + about: { + title: 'Giới thiệu về ESPConnect', + description: { + intro: + 'ESPConnect là một bộ công cụ dựa trên trình duyệt cho các thiết bị ESP32. Nó kết hợp các tác vụ bảo trì phổ biến để bạn có thể kết nối, kiểm tra và cập nhật bo mạch mà không cần cài đặt các tiện ích trên máy tính.', + details: + 'Ứng dụng giao tiếp với thiết bị qua Web Serial API, sau đó hiển thị các công cụ cấp cao cho việc flash firmware, quản lý phân vùng, chỉnh sửa filesystem, quy trình sao lưu/khôi phục, cũng như nhật ký phiên và trình giám sát nối tiếp để khắc phục sự cố. Nó dựa trên', + reference: 'WebSerial ESPTool', + }, + filesystems: { + title: 'Công cụ Filesystem', + spiFFS: { + label: 'SPIFFS', + description: 'Bộ mô phỏng TypeScript để chuẩn bị thay đổi cục bộ trước khi ghi vào flash.', + }, + littleFS: { + label: 'LittleFS', + description: 'Dựa trên module littlefs-wasm để có độ tương đồng lưu trữ thật.', + }, + fatFS: { + label: 'FATFS', + description: 'Công cụ mới dựa trên wasm cho các phân vùng FAT, phản ánh bố cục thiết bị của bạn.', + }, + summary: + 'Tải lên bằng kéo-thả, xem trước/nghe các loại tệp phổ biến, bản tóm tắt diff và yêu cầu sao lưu bắt buộc trước khi lưu giúp quy trình an toàn hơn.', + }, + firmware: { + title: 'Firmware & Bảo trì', + flash: 'Flash các tệp nhị phân tùy chỉnh với theo dõi tiến độ, preset và nhắc nhở an toàn.', + inspect: 'Kiểm tra bảng phân vùng, khe OTA, mô tả thiết bị và mức sử dụng flash chỉ trong nháy mắt.', + monitor: 'Trình giám sát nối tiếp với phím tắt lệnh và nhật ký phiên để khắc phục sự cố.', + }, + safety: { + title: 'Quy trình ưu tiên an toàn', + body: + 'Mọi hành động phá hủy đều được chặn bởi lời nhắc xác nhận và bản sao lưu gần đây. ESPConnect giữ các thay đổi đã đưa vào staging trong bộ nhớ cho đến khi bạn chủ động ghi chúng vào flash, giúp dễ dàng xem lại sửa đổi hoặc khôi phục bằng cách đọc lại phân vùng.', + }, + privacy: { + title: 'Quyền riêng tư & Bảo mật', + body: + 'ESPConnect chạy hoàn toàn trong trình duyệt của bạn -- không có backend, tài khoản hay telemetry nào. Tệp firmware, bản sao lưu và chẩn đoán đều ở cục bộ và chỉ di chuyển khi bạn tự tải xuống. Luôn flash firmware từ các nguồn đáng tin cậy.', + }, + }, + apps: { + alerts: { + readingMetadata: 'Đang đọc siêu dữ liệu ứng dụng…', + noApplications: 'Không phát hiện thấy phân vùng ứng dụng nào.', + partitionTableUnavailable: 'Không thể hiển thị các phân vùng ứng dụng vì không thể đọc bảng phân vùng.', + }, + chips: { + active: 'Hoạt động', + invalid: 'Mã hóa/Không hợp lệ', + }, + subtitle: 'Offset {offset} — Kích thước {size}', + details: { + project: 'Dự án', + version: 'Phiên bản', + built: 'Xây dựng', + core: 'IDF / Core', + entry: 'Địa chỉ đầu vào', + segments: 'Segments', + }, + descriptorMissing: 'Không tìm thấy mô tả ứng dụng trong 64 KB đầu tiên.', + invalidImage: 'Tiêu đề hình ảnh không hợp lệ hoặc đã mã hóa.', + unknown: 'Không xác định', + facts: { + groups: { + package: 'Gói & Phiên bản', + embeddedMemory: 'Bộ nhớ nhúng', + security: 'Bảo mật', + peripherals: 'Thiết bị ngoại vi', + connection: 'Kết nối', + documentation: 'Tài liệu', + }, + labels: { + chipVariant: 'Biến thể Chip', + packageFormFactor: 'Hình thức Gói', + revision: 'Phiên bản', + cpuCores: 'Số lõi CPU', + maxCpuFrequency: 'Tần số CPU tối đa', + embeddedFlash: 'Embedded Flash', + embeddedPsr: 'Embedded PSRAM', + flashId: 'Flash ID', + flashManufacturer: 'Nhà sản xuất Flash', + flashDevice: 'Thiết bị Flash', + flashVendorEfuse: 'Nhà cung cấp Flash (eFuse)', + psramVendorEfuse: 'Nhà cung cấp PSRAM (eFuse)', + efuseBlockVersion: 'Phiên bản khối eFuse', + flashEncryption: 'Mã hóa Flash', + flashEncryptionDetails: 'Chi tiết Mã hóa Flash', + flashEncryptionMode: 'Chế độ Mã hóa Flash', + secureBoot: 'Secure Boot', + secureBootType: 'Loại Secure Boot', + jtagProtection: 'Bảo vệ JTAG', + usbProtection: 'Bảo vệ USB', + downloadModeCaches: 'Bộ nhớ cache Chế độ Tải xuống', + securityNote: 'Ghi chú Bảo mật', + pwmLedc: 'PWM/LEDC', + usbBridge: 'USB Bridge', + connectionBaud: 'Baud Kết nối', + hardwareReference: 'Tham chiếu Phần cứng', + datasheet: 'Datasheet', + technicalReferenceManual: 'Sổ tay Tham chiếu Kỹ thuật', + errata: 'Errata', + hardwareDesignGuidelines: 'Hướng dẫn Thiết kế Phần cứng', + }, + pwmTooltip: + 'Khả năng PWM/LEDC dựa trên họ chip, không phải trên dữ liệu thời gian thực được đọc từ thiết bị.', + }, + }, + flashFirmware: { + warning: + 'Các công cụ firmware chỉ dành cho người dùng nâng cao. Viết hoặc xóa flash có thể xóa vĩnh viễn dữ liệu hoặc làm cho vi điều khiển không thể khởi động lại. Kiểm tra kỹ cài đặt trước khi tiếp tục.', + backup: { + title: 'Sao lưu & Xóa Flash', + partitionLabel: 'Phân vùng', + downloadSelectedPartition: 'Tải xuống Phân vùng Đã chọn', + eraseSelectedPartition: 'Xóa Phân vùng Đã chọn', + downloadAllPartitions: 'Tải xuống Tất cả Phân vùng', + downloadFlashBackup: 'Tải xuống Sao lưu Flash', + startOffset: 'Offset bắt đầu', + lengthBytes: 'Độ dài (byte)', + downloadRegion: 'Tải xuống Vùng Flash', + eraseRegion: 'Xóa Vùng Flash', + eraseEntireFlash: 'Xóa Toàn bộ Flash', + eraseConfirmTitle: 'Xóa Toàn bộ Flash', + eraseConfirmMessage: 'Xóa toàn bộ flash? Thao tác này sẽ xóa tất cả dữ liệu và không thể hoàn tác.', + eraseConfirmButton: 'Xóa Flash', + erasePartitionConfirmTitle: 'Xóa Phân vùng', + erasePartitionConfirmMessage: 'Xóa phân vùng đã chọn ({label}) tại {offset} ({size})?', + eraseRegionConfirmTitle: 'Xóa Vùng Flash', + eraseRegionConfirmMessage: 'Xóa {label} tại {offset} với {length} byte?', + status: { + connectFirst: 'Kết nối với thiết bị trước.', + unsupported: 'Xóa toàn bộ flash không được bộ nạp này hỗ trợ.', + partitionRequired: 'Chọn một phân vùng để xóa.', + regionRequired: 'Nhập offset và độ dài để xóa.', + regionOutOfBounds: 'Vùng yêu cầu vượt quá kích thước flash đã phát hiện.', + cancelled: 'Đã hủy thao tác xóa flash.', + erasing: 'Đang xóa toàn bộ flash...', + erasingPartition: 'Đang xóa phân vùng...', + erasingRegion: 'Đang xóa vùng flash...', + complete: 'Đã xóa flash hoàn tất.', + partitionComplete: 'Đã xóa phân vùng.', + regionComplete: 'Đã xóa vùng flash.', + failed: 'Xóa không thành công: {error}', + }, + }, + firmware: { + title: 'Flash Firmware', + binaryLabel: 'Tệp nhị phân firmware (.bin)', + flashOffset: 'Offset flash', + recommendedOffsets: 'Offset đề xuất', + eraseBeforeFlash: 'Xóa toàn bộ flash trước khi ghi', + hint: + 'Quá trình flashing mặc định chạy ở 921,600 bps. Hạ thấp baud nếu thiết bị gặp khó khăn khi đồng bộ. Trình giám sát nối tiếp tự động chuyển sang 115,200 bps để ổn định.', + flashButton: 'Flash Firmware', + }, + registerAccess: { + title: 'Truy cập Thanh ghi', + quickSelectLabel: 'Chọn nhanh thanh ghi', + registerAddress: 'Địa chỉ thanh ghi', + valueLabel: 'Giá trị', + readButton: 'Đọc Thanh ghi', + writeButton: 'Ghi Thanh ghi', + referenceLink: 'Xem tài liệu tham khảo thanh ghi', + technicalReferenceLink: 'Mở tài liệu kỹ thuật', + lastReadValue: 'Giá trị đọc gần nhất:', + }, + integrity: { + title: 'Tính toàn vẹn Flash', + partitionLabel: 'Phân vùng', + helper: 'Chọn phân vùng sẽ tự điền các trường offset và độ dài bên dưới.', + startOffset: 'Offset bắt đầu', + lengthBytes: 'Độ dài (byte)', + computeButton: 'Tính MD5', + md5Label: 'Checksum MD5:', + }, + progress: { + flashTitle: 'Đang flash', + downloadTitle: 'Đang tải xuống flash', + preparingFlash: 'Đang chuẩn bị flash...', + preparingDownload: 'Đang chuẩn bị tải xuống...', + erasingFlash: 'Đang xóa flash...', + stop: 'Dừng', + }, + }, + partitions: { + cardTitle: 'Phân vùng', + cardTitleWithSize: 'Phân vùng — {size}', + usageSummary: 'Phân vùng sử dụng {size}', + empty: { + title: 'Chưa có dữ liệu phân vùng', + subtitle: 'Kết nối với ESP32 để tải bảng phân vùng của nó (ESP8266 không được hỗ trợ).', + subtitleConnected: 'Không phát hiện bảng phân vùng trên ESP32 này.', + }, + error: { + title: 'Không thể đọc bảng phân vùng', + message: + 'ESPConnect không thể đọc bảng phân vùng. Điều này thường có nghĩa là giao tiếp nối tiếp không đáng tin cậy trong trình duyệt này hoặc chip/transport không được hỗ trợ đầy đủ.', + detail: 'Chi tiết: {error}', + }, + alerts: { + unusedFlash: { + detected: 'Phát hiện flash chưa sử dụng - khoảng {amount} ({bytes} byte) có thể thu hồi.', + learn: 'Xem', + or: 'hoặc thử', + }, + customizePrompt: 'Bạn muốn tùy chỉnh bố cục này?', + customizeOr: 'hoặc mở', + resources: { + tutorial: 'hướng dẫn phân vùng', + builder: 'ESP32 partition builder', + }, + }, + table: { + label: 'Nhãn', + type: 'Loại', + subtype: 'Phân loại', + offset: 'Offset', + size: 'Kích thước', + }, + unnamed: 'Không có tên', + }, + filesystem: { + partitionTitle: 'Phân vùng {fs}', + partitionTitleWithSize: '{base} — {size}', + partitionSelectLabel: 'Tên phân vùng', + controls: { + read: 'Đọc', + backup: 'Sao lưu', + restore: 'Khôi phục Ảnh', + format: 'Định dạng', + save: 'Lưu vào Flash', + newFolder: 'Thư mục mới', + }, + backupReminder: + 'Tải xuống hình ảnh sao lưu trước (dùng nút "{backup}" một lần cho mỗi phiên). "{save}" sẽ có sẵn sau khi có bất kỳ bản sao lưu thành công nào trong kết nối này.', + saveHint: 'Các thay đổi được staging cục bộ cho đến khi bạn nhấn "{save}". Một bản sao lưu gần đây đảm bảo bạn có thể khôi phục nếu có vấn đề xảy ra.', + filesTitle: 'Tệp', + unsavedChanges: 'Có thay đổi chưa lưu', + usage: { + used: 'Đã dùng {percent}% ({used} / {capacity})', + free: 'Còn trống {free}', + }, + upload: { + selectFile: 'Chọn tệp', + uploadButton: 'Tải lên', + dropHint: 'Thả tệp để thêm', + dropHintFolder: 'Thả tệp hoặc thư mục để thêm', + }, + filter: { + search: 'Lọc tệp', + type: 'Loại tệp', + }, + noMatches: 'Không có tệp nào phù hợp với bộ lọc hiện tại.', + table: { + name: 'Tên', + size: 'Kích thước', + actions: 'Hành động', + }, + pagination: { + all: 'Tất cả', + itemsPerPage: 'Số mục trên mỗi trang:', + }, + fileCategory: { + all: 'Tất cả các loại', + text: 'Văn bản', + image: 'Hình ảnh', + audio: 'Âm thanh', + other: 'Khác', + }, + filterCount: { + noFiles: 'Không có tệp', + all: '{count} tệp', + partial: '{filtered} trên {total} tệp', + }, + emptyState: 'Không phát hiện tệp nào. Tải lên hoặc khôi phục hình ảnh {fs} để bắt đầu.', + loadCancelled: 'Đã hủy tải {fs}. {action} để lấy lại phân vùng.', + readOnly: '{fs} đang ở chế độ chỉ đọc. {detail}', + readOnlyDetail: 'Không thể lưu thay đổi.', + versionLabel: 'LittleFS v{version}', + dialog: { + newFolderTitle: 'Thư mục mới', + folderName: 'Tên thư mục', + cancel: 'Hủy', + create: 'Tạo', + }, + preview: { + listen: 'Nghe', + view: 'Xem', + }, + actions: { + download: 'Tải xuống {file}', + delete: 'Xóa {file}', + }, + }, + filesystemStatus: { + spiffs: 'Tải một phân vùng SPIFFS để bắt đầu.', + littlefs: 'Tải một phân vùng LittleFS để bắt đầu.', + fatfs: 'Tải một phân vùng FATFS để bắt đầu.', + }, + nvsInspector: { + title: 'Trình kiểm tra NVS', + experimentalTooltip: 'Tính năng thử nghiệm: đầu ra có thể chưa đầy đủ hoặc không chính xác.', + experimentalBadge: 'Thử nghiệm', + partitionSelectLabel: 'Tên phân vùng', + readButton: 'Đọc NVS', + versionLabel: 'NVS v{version}', + pageStatsTooltip: 'tổng: {total} · đang dùng: {inUse} · trống: {blank} · xấu: {bad}', + pagesCount: '{count} trang', + namespacesCount: '{count} không gian tên', + entriesCount: '{count} mục', + usage: { + label: 'Đã dùng {used} / {total}', + tooltip: + 'Bộ nhớ mục: {percent}% phân vùng · đã ghi: {written} · đã xóa: {erased} · trống: {free} · không hợp lệ: {illegal} · dung lượng mục: {capacity}', + }, + tabs: { + keys: 'Khóa', + pages: 'Trang', + }, + keys: { + emptyState: 'Đọc NVS để xem khóa.', + title: 'Khóa', + pageFilter: 'Trang p{page}', + filters: { + namespace: 'Không gian tên', + key: 'Khóa', + type: 'Loại', + value: 'Xem trước giá trị', + }, + table: { + namespace: 'Không gian tên', + key: 'Khóa', + type: 'Loại', + value: 'Giá trị', + length: 'Độ dài', + crc: 'CRC', + location: 'Vị trí', + issues: 'Vấn đề', + }, + noMatches: 'Không có mục nào khớp với bộ lọc hiện tại.', + }, + filteredCount: '{count} đang hiển thị', + warningsErrors: { + title: 'Cảnh báo / Lỗi', + errors: 'Lỗi', + warnings: 'Cảnh báo', + }, + status: { + blank: 'Trống', + ok: 'OK', + bad: 'BAD', + }, + pages: { + emptyState: 'Đọc NVS để xem các trang.', + title: 'Trang', + summary: 'tổng: {total} · đang dùng: {inUse} · trống: {blank} · xấu: {bad}', + table: { + page: 'Trang #', + state: 'Trạng thái', + seq: 'Seq', + status: 'Trạng thái', + map: 'Bản đồ', + errors: 'Lỗi', + }, + }, + pageTooltipTitle: 'Trang {index} ({state})', + pageTooltipWritten: 'Đã ghi: {written} / {total} ({percent}%)', + pageTooltipDistribution: 'Đã xóa: {erased}, Trống: {empty}, Không hợp lệ: {illegal}', + entryMapUnavailable: 'Bản đồ mục không khả dụng.', + pageTooltipErrors: 'Lỗi: {count}', + entryCellTitle: 'Mục #{entry} (từ {word} / bit {bitStart}-{bitEnd}): {state}', + filters: { + all: 'Tất cả', + }, + }, + language: { + english: 'English', + french: 'Français', + chinese: '简体中文', + turkish: 'Türkçe', + german: 'German', + czech: 'Czech', + spanish: 'Spanish', + vietnamese: 'Tiếng Việt', + switchTo: 'Switch to {language}', + }, + theme: { + light: 'sáng', + dark: 'tối', + switch: 'Chuyển sang giao diện {theme}', + }, + status: { + connected: 'Đã kết nối', + disconnected: 'Đã ngắt kết nối', + preparingBackup: 'Đang chuẩn bị bản sao lưu...', + writingLittlefs: 'Đang ghi hình ảnh LittleFS...', + }, +}; + +export default vi; diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 6256538..5ec0c83 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -627,6 +627,7 @@ const zh = { german: '德语', czech: '捷克语', spanish: '西班牙语', + vietnamese: '越南语', switchTo: '切换到{language}', }, theme: { diff --git a/src/plugins/i18n.test.ts b/src/plugins/i18n.test.ts index cea9940..2476416 100644 --- a/src/plugins/i18n.test.ts +++ b/src/plugins/i18n.test.ts @@ -55,6 +55,15 @@ describe('i18n locale selection', () => { expect(i18n.global.locale.value).toBe('es'); }); + it('uses vi when navigator languages include Vietnamese', async () => { + setNavigator(['vi-VN', 'en-US']); + setWindowWithLocalStorage(null); + + const { i18n } = await loadI18n(); + + expect(i18n.global.locale.value).toBe('vi'); + }); + it('falls back to en when navigator languages are unsupported', async () => { setNavigator(['ja-JP']); setWindowWithLocalStorage(null); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 789e7a0..3bc3500 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -6,10 +6,11 @@ import tr from '../locales/tr'; import de from '../locales/de'; import cs from '../locales/cs'; import es from '../locales/es'; -import { en as vuetifyEn, fr as vuetifyFr, zhHans as vuetifyZhHans, tr as vuetifyTr, de as vuetifyDe, cs as vuetifyCs, es as vuetifyEs } from 'vuetify/locale'; +import vi from '../locales/vi'; +import { en as vuetifyEn, fr as vuetifyFr, zhHans as vuetifyZhHans, tr as vuetifyTr, de as vuetifyDe, cs as vuetifyCs, es as vuetifyEs, vi as vuetifyVi } from 'vuetify/locale'; const STORAGE_KEY = 'espconnect-language'; -export const supportedLocales = ['en', 'fr', 'zh', 'tr', 'de', 'cs', 'es'] as const; +export const supportedLocales = ['en', 'fr', 'zh', 'tr', 'de', 'cs', 'es', 'vi'] as const; export type SupportedLocale = (typeof supportedLocales)[number]; function normalizeLocale(value: unknown): SupportedLocale { @@ -87,6 +88,10 @@ export const i18n = createI18n({ ...es, $vuetify: vuetifyEs, }, + vi: { + ...vi, + $vuetify: vuetifyVi, + }, }, });