feat: complete and polish Vietnamese locale

This commit is contained in:
Tony Tran 2026-06-18 17:44:27 +07:00
parent 73d1e521fa
commit a9645f6c47
2 changed files with 380 additions and 67 deletions

View file

@ -19109,7 +19109,7 @@ const LOCALES = {
mcp_no_servers: 'Chưa có máy chủ MCP nào được cấu hình.',
mcp_add_server: '+ Thêm máy chủ',
mcp_field_name: 'Tên máy chủ',
mcp_transport_label: 'Loại transport',
mcp_transport_label: 'Loại kết nối',
mcp_field_command: 'Lệnh',
mcp_field_args: 'Tham số (phân tách bằng dấu phẩy)',
mcp_field_url: 'URL',
@ -19174,7 +19174,7 @@ const LOCALES = {
html_too_large: 'HTML quá lớn để xem trực tiếp',
html_error: 'Không thể hiển thị xem trước HTML',
html_open_full: 'Mở toàn bộ trang',
html_sandbox_label: 'Xem trước HTML (sandbox)',
html_sandbox_label: 'Xem trước HTML (chế độ cách ly)',
thinking: 'Đang suy nghĩ',
expand_all: 'Mở rộng tất cả',
collapse_all: 'Thu gọn tất cả',
@ -19187,7 +19187,7 @@ const LOCALES = {
approval_desc_prefix: 'Phát hiện lệnh nguy hiểm',
approval_btn_once: 'Cho phép một lần',
approval_btn_once_title: 'Cho phép lệnh này một lần (Enter)',
approval_btn_session: 'Cho phép trong phiên',
approval_btn_session: 'Cho phép trong cuộc trò chuyện này',
approval_btn_session_title: 'Cho phép trong phiên trò chuyện này',
approval_btn_always: 'Luôn cho phép',
approval_btn_always_title: 'Luôn cho phép mẫu lệnh này',
@ -19223,10 +19223,10 @@ const LOCALES = {
model_unavailable: ' (không khả dụng)',
model_unavailable_title: 'Model này không còn trong danh sách provider hiện tại của bạn',
provider_mismatch_warning: (m,p)=>`"${m}" có thể không hoạt động với provider đã cấu hình (${p}). Vẫn gửi hoặc chạy \`hermes model\` trong terminal để chuyển đổi.`,
provider_mismatch_label: 'Provider không khớp',
provider_mismatch_label: 'Nhà cung cấp không khớp',
gateway_auth_label: 'Xác thực gateway thất bại',
model_not_found_label: 'Không tìm thấy model',
model_custom_label: 'ID model tùy chỉnh',
model_custom_label: 'ID model tự nhập',
model_custom_placeholder: 'ví dụ: openai/gpt-5.4',
model_search_placeholder: 'Tìm model…',
model_search_no_results: 'Không tìm thấy model nào',
@ -19246,10 +19246,10 @@ const LOCALES = {
cmd_compress: 'Nén thủ công ngữ cảnh cuộc trò chuyện (cách dùng: /compress [chủ đề])',
ctx_compress_hint: 'Nén ngữ cảnh để giải phóng dung lượng →',
ctx_compress_action: '⚠ Nén ngay để giải phóng ngữ cảnh',
cmd_compact_alias: 'Alias cũ cho /compress',
cmd_compact_alias: 'Bí danh cũ của /compress',
cmd_model: 'Chuyển model (ví dụ: /model gpt-4o)',
cmd_workspace: 'Chuyển workspace theo tên',
cmd_terminal: 'Mở terminal của workspace',
cmd_workspace: 'Chuyển không gian làm việc theo tên',
cmd_terminal: 'Mở terminal của không gian làm việc',
cmd_new: 'Bắt đầu phiên chat mới',
cmd_usage: 'Bật/tắt hiển thị token usage',
cmd_theme: 'Đổi giao diện (theme: system/dark/light, skin: default/ares/mono/graphite/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus/verdigris)',
@ -19307,12 +19307,12 @@ const LOCALES = {
goal_continuing: (turns, max_turns, reason) => `↻ Đang tiếp tục mục tiêu (${turns}/${max_turns}): ${reason}`,
cmd_interrupt: 'Hủy lượt hiện tại và gửi tin nhắn mới',
cmd_steer: 'Chèn điều chỉnh giữa lượt mà không ngắt agent',
cmd_queue_no_msg: 'Cách dùng: /queue <message>',
cmd_queue_no_msg: 'Cách dùng: /queue <tin nhắn>',
cmd_queue_not_busy: 'Không có tác vụ hoạt động — cứ gửi bình thường',
cmd_queue_confirm: 'Đã đưa tin nhắn vào hàng chờ',
cmd_interrupt_no_msg: 'Cách dùng: /interrupt <message>',
cmd_interrupt_no_msg: 'Cách dùng: /interrupt <tin nhắn>',
cmd_interrupt_confirm: 'Đã ngắt — đang gửi tin nhắn mới',
cmd_steer_no_msg: 'Cách dùng: /steer <message>',
cmd_steer_no_msg: 'Cách dùng: /steer <tin nhắn>',
cmd_steer_fallback: 'Steer không khả dụng — đã đưa vào hàng chờ lượt tiếp theo',
cmd_steer_delivered: 'Steer đã được gửi — agent sẽ thấy ở kết quả tool tiếp theo',
steer_leftover_queued: 'Steer đã được đưa vào hàng chờ lượt tiếp theo',
@ -19400,16 +19400,15 @@ const LOCALES = {
usage_personality_none:'không có',
// Session toolsets (#493)
session_toolsets:'Bộ công cụ phiên',
session_toolsets_desc:'Giới hạn công cụ khả dụng cho phiên này (để trống = dùng cấu hình global)',
session_toolsets_global:'Global (mặc định)',
session_toolsets_desc:'Giới hạn công cụ khả dụng cho phiên này (để trống = dùng cấu hình toàn cục)',
session_toolsets_global:'Toàn cục (mặc định)',
session_toolsets_custom:'Tùy chỉnh',
session_toolsets_placeholder:'tool1, tool2, …',
session_toolsets_apply:'Áp dụng',
session_toolsets_clear:'Xóa (dùng global)',
session_toolsets_clear:'Xóa (dùng cấu hình toàn cục)',
session_toolsets_applied:'Đã cập nhật bộ công cụ',
session_toolsets_cleared:'Đã xóa bộ công cụ — dùng cấu hình global',
session_toolsets_cleared:'Đã xóa bộ công cụ — dùng cấu hình toàn cục',
session_toolsets_failed:'Cập nhật bộ công cụ thất bại: ',
untitled:'Không tiêu đề',
no_personalities: 'Không tìm thấy personality nào (thêm vào ~/.hermes/personalities/)',
available_personalities: 'Các personality khả dụng:',
personality_switch_hint: '\n\nDùng `/personality <name>` để chuyển hoặc `/personality none` để xóa.',
@ -19418,12 +19417,12 @@ const LOCALES = {
personality_set: 'Personality: ',
failed_colon: 'Thất bại: ',
// ui.js
no_workspace: 'Không có workspace',
terminal_open_title: 'Mở terminal workspace',
no_workspace: 'Không có không gian làm việc',
terminal_open_title: 'Mở terminal của không gian làm việc',
terminal_no_workspace_title: 'Chọn workspace để mở terminal',
terminal_title: 'Terminal',
terminal_clear: 'Xóa',
terminal_copy_output: 'Sao chép output',
terminal_copy_output: 'Sao chép đầu ra',
terminal_restart: 'Khởi động lại',
terminal_collapse: 'Thu gọn',
terminal_expand: 'Mở rộng',
@ -19435,12 +19434,12 @@ const LOCALES = {
terminal_error: 'Lỗi terminal',
terminal_remote_backend_unsupported: 'Terminal nhúng chỉ hỗ trợ backend terminal local.',
workspace_empty_no_path: 'Chưa chọn workspace. Đặt workspace trong Settings → Workspace để duyệt file.',
workspace_empty_dir: 'Workspace này trống.',
workspace_empty_dir: 'Không gian làm việc này đang trống.',
workspace_show_hidden_files: 'Hiện file ẩn',
workspace_show_hidden_files_desc: 'Bao gồm .DS_Store, .git, node_modules và các file hệ thống / ẩn khác trong cây thư mục.',
workspace_hidden_files_visible: 'đang hiện file ẩn',
workspace_hidden_files_visible_title: 'File ẩn đang hiển thị — bấm để xem tùy chọn',
workspace_options: 'Tùy chọn workspace',
workspace_options: 'Tùy chọn không gian làm việc',
// workspace.js
unsaved_confirm: 'Bạn có thay đổi chưa lưu trong phần xem trước. Bỏ thay đổi và chuyển trang?',
save: 'Lưu',
@ -19465,7 +19464,7 @@ const LOCALES = {
rename_prompt: 'Tên mới:',
deleted: 'Đã xóa ',
delete_failed: 'Xóa thất bại: ',
reveal_in_finder: 'Hiện trong File Manager',
reveal_in_finder: 'Hiện trong trình quản lý tệp',
reveal_failed: 'Hiển thị thất bại: ',
copy_file_path: 'Sao chép đường dẫn file',
open_in_vscode: 'Mở bằng VS Code',
@ -19488,7 +19487,7 @@ const LOCALES = {
new_file: 'File mới',
new_folder: 'Thư mục mới',
workspace_root: 'thư mục gốc workspace',
new_file_prompt_title: 'Tên file mới trong {0}:',
new_file_prompt_title: 'Tên tệp mới trong {0}:',
new_folder_prompt_title: 'Tên thư mục mới trong {0}:',
project_name_prompt: 'Tên project:',
created: 'Đã tạo ',
@ -19513,7 +19512,7 @@ const LOCALES = {
session_pin_desc: 'Giữ cuộc trò chuyện này ở đầu danh sách',
session_unpin_desc: 'Bỏ khỏi danh sách ghim',
session_pin_failed: 'Ghim thất bại: ',
session_move_project: 'Chuyển sang project',
session_move_project: 'Chuyển sang dự án',
session_move_project_desc_has: 'Đổi project cho cuộc trò chuyện này',
session_move_project_desc_none: 'Gán project cho cuộc trò chuyện này',
session_archive: 'Lưu trữ cuộc trò chuyện',
@ -19553,8 +19552,8 @@ const LOCALES = {
settings_desc_chat_activity_display_mode: 'Compact Worklog gom nhóm hoạt động thành bản tóm tắt gọn. Transparent Stream giữ mỗi sự kiện thinking/tool thành một dòng theo thứ tự thời gian có thể mở rộng.',
first_token_time: 'Thời gian tới token đầu tiên',
done: 'Hoàn tất',
settings_option_compact_worklog: 'Compact Worklog',
settings_option_transparent_stream: 'Transparent Stream',
settings_option_compact_worklog: 'Nhật ký công việc rút gọn',
settings_option_transparent_stream: 'Luồng trong suốt',
settings_label_tab_visibility: 'Tab sidebar',
settings_desc_tab_visibility: 'Chọn tab nào xuất hiện trong sidebar và thanh rail. Kéo các chip để đổi thứ tự. Chat và Settings luôn hiển thị.',
@ -19589,7 +19588,7 @@ const LOCALES = {
settings_help_docs_link: 'Mở tài liệu',
settings_help_issue_label: 'Có vấn đề?',
settings_help_issue_desc: 'Tìm báo cáo hiện có hoặc mở issue mới trên GitHub.',
settings_help_issue_link: 'Mở GitHub Issues',
settings_help_issue_link: 'Mở trang GitHub Issues',
settings_title: 'Cài đặt',
settings_save_btn: 'Lưu cài đặt',
settings_label_model: 'Model mặc định',
@ -19623,13 +19622,13 @@ const LOCALES = {
settings_autosave_failed: 'Lưu thất bại',
settings_autosave_retry: 'Thử lại',
settings_label_language: 'Ngôn ngữ',
settings_label_quota_chip: 'Hiện quota provider trong composer',
settings_label_quota_chip: 'Hiện hạn mức nhà cung cấp trong khung soạn thảo',
settings_desc_quota_chip: 'Hiển thị chỉ báo quota còn lại (ví dụ: số dư credit OpenRouter) ở footer của composer. Mặc định tắt. Chỉ hiển thị trên màn hình rộng (≥1400px) khi bật để giữ composer gọn gàng trên laptop và desktop thông thường.',
settings_label_conversation_outline: 'Hiện mục lục cuộc trò chuyện',
settings_desc_conversation_outline: 'Hiển thị danh sách nhảy nhanh cho các câu hỏi của người dùng trong cuộc trò chuyện hiện tại trên desktop. Mặc định tắt.',
settings_label_hide_suggestions: 'Ẩn gợi ý chat mới',
settings_desc_hide_suggestions: 'Ẩn ba nút gợi ý mặc định trên màn hình chat mới để tránh bấm nhầm.',
settings_label_token_usage: 'Hiện token usage',
settings_label_token_usage: 'Hiện mức sử dụng token',
settings_label_token_speed: 'Hiện tốc độ token (TPS)',
settings_desc_token_speed: 'Hiển thị token mỗi giây trong header tin nhắn assistant khi đang stream và sau khi hoàn tất phản hồi. Mặc định tắt.',
settings_label_sidebar_density: 'Mật độ sidebar',
@ -19645,18 +19644,18 @@ const LOCALES = {
settings_label_password: 'Mật khẩu truy cập',
settings_label_pinned_limit: 'Giới hạn ghim cuộc trò chuyện',
settings_desc_pinned_limit: 'Số lượng tối đa cuộc trò chuyện hoạt động có thể được ghim trong sidebar. Mặc định là 3.',
settings_label_dashboard_mode: 'Hermes Dashboard chính thức',
settings_label_dashboard_mode: 'Bảng điều khiển Hermes chính thức',
settings_desc_dashboard_mode: 'Hiển thị liên kết nav-rail khi dashboard Hermes chính thức khả dụng. URL reverse-proxy công khai chỉ được lưu trong trình duyệt và không bao giờ bị server probe.',
settings_dashboard_mode_auto: 'Tự động phát hiện',
settings_dashboard_mode_always: 'Luôn hiển thị',
settings_dashboard_mode_never: 'Không bao giờ hiển thị',
settings_dashboard_save: 'Lưu cài đặt liên kết dashboard',
settings_label_gateway_status: 'Trạng thái Gateway',
settings_label_gateway_status: 'Trạng thái gateway',
settings_desc_gateway_status: 'Trạng thái gateway Hermes (Telegram, Discord, Slack, v.v.)',
gateway_not_configured: 'Gateway chưa được cấu hình',
gateway_metadata_stale: 'Metadata gateway đã cũ',
gateway_endpoint_unreachable: 'Không thể truy cập endpoint gateway',
gateway_not_running: 'Gateway chưa chạy',
gateway_not_running: 'Gateway hiện chưa chạy',
gateway_running_label: 'Đang chạy',
gateway_last_active: 'Hoạt động lần cuối: ',
gateway_session_count: (n) => `${n} ${n === 1 ? 'phiên' : 'phiên'}`,
@ -19689,7 +19688,7 @@ const LOCALES = {
kanban_search_tasks: 'Tìm kiếm tác vụ',
kanban_all_assignees: 'Tất cả assignee',
kanban_all_tenants: 'Tất cả tenant',
kanban_include_archived: 'Bao gồm archived',
kanban_include_archived: 'Bao gồm mục đã lưu trữ',
kanban_no_matching_tasks: 'Không có tác vụ phù hợp',
kanban_no_data: 'Không có dữ liệu Kanban',
kanban_work_queue_hint: 'Đây là hàng đợi công việc của Hermes Agent. Tạo hoặc phân loại tác vụ, gán người xử lý, chuyển sang Ready rồi để dispatcher nhận.',
@ -19728,9 +19727,9 @@ const LOCALES = {
kanban_new_task: 'Tác vụ mới',
kanban_edit_task: 'Chỉnh sửa tác vụ',
kanban_status_original_hint: 'Trạng thái thực tế: {0}. Hộp thoại này chỉ hỗ trợ chỉnh sửa Triage/Todo/Ready.',
kanban_run_dispatcher: 'Chạy dispatcher',
kanban_run_dispatcher: 'Chạy bộ điều phối',
kanban_run_dispatcher_confirm: 'Thao tác này sẽ nhận các tác vụ Ready trên bảng này và tạo subprocess worker (mỗi tác vụ một worker, tối đa 8 mỗi lần bấm). Tiếp tục?',
kanban_assignee_profiles_label: 'Profile Hermes',
kanban_assignee_profiles_label: 'Hồ sơ Hermes',
kanban_assignee_other_label: 'Khác (CLI lanes / profile đã xóa)',
kanban_assignee_unassigned: '— Chưa gán (sẽ không tự chạy) —',
kanban_ready_needs_assignee: 'Bạn đã chọn Unassigned + Ready. Dispatcher sẽ bỏ qua tác vụ này. Gửi lại để xác nhận hoặc chọn một profile.',
@ -19747,13 +19746,13 @@ const LOCALES = {
kanban_add_comment: 'Thêm bình luận',
kanban_only_mine: 'Chỉ của tôi',
kanban_bulk_action: 'Thao tác hàng loạt',
kanban_nudge_dispatcher: 'Xem trước dispatcher',
kanban_nudge_dispatcher: 'Xem trước bộ điều phối',
kanban_stats: 'Thống kê',
kanban_worker_log: 'Log worker',
kanban_block: 'Chặn',
kanban_unblock: 'Bỏ chặn',
kanban_back_to_board: 'Quay lại bảng',
kanban_lanes_by_profile: 'Lane theo profile',
kanban_lanes_by_profile: 'Cột theo hồ sơ',
kanban_new_board: 'Bảng mới…',
kanban_rename_board: 'Đổi tên bảng hiện tại…',
kanban_archive_board: 'Lưu trữ bảng hiện tại…',
@ -19762,7 +19761,7 @@ const LOCALES = {
kanban_board_name: 'Tên',
kanban_board_slug: 'Slug (chữ thường, dấu gạch nối)',
kanban_board_description: 'Mô tả (tùy chọn)',
kanban_board_icon: 'Icon (emoji, tùy chọn)',
kanban_board_icon: 'Biểu tượng (emoji, tùy chọn)',
kanban_board_color: 'Màu sắc (tùy chọn)',
kanban_board_name_required: 'Tên là bắt buộc',
kanban_board_slug_required: 'Slug là bắt buộc',
@ -19772,7 +19771,7 @@ const LOCALES = {
kanban_status_archived: 'Đã lưu trữ',
tab_todos: 'Todos',
tab_insights: 'Insights',
tab_dashboard: 'Hermes Dashboard',
tab_dashboard: 'Bảng điều khiển Hermes',
dashboard_loopback_warning: 'Dashboard chỉ cho phép loopback trên server. Hãy truy cập trực tiếp từ server hoặc khởi động lại với --host 0.0.0.0 (không an toàn).',
tab_logs: 'Logs',
tab_settings: 'Cài đặt',
@ -19864,14 +19863,14 @@ const LOCALES = {
session_child_toggle_hint: '{0} — các cuộc trò chuyện con được tạo từ phiên này. Bấm để hiện hoặc ẩn chúng.',
session_readonly_title_hint: 'Phiên import chỉ đọc — {0}',
session_lineage_segment_untitled: 'Segment chưa có tiêu đề',
session_lineage_segment_open: 'Mở lineage segment',
session_lineage_segment_open: 'Mở đoạn lineage',
new_profile: 'Profile mới',
transcript: 'Transcript',
download_transcript: 'Tải dưới dạng Markdown',
import: 'Import',
export_session_json: 'JSON',
export_session_json_tooltip: 'Xuất toàn bộ phiên dưới dạng JSON',
import_session_json_tooltip: 'Import phiên từ JSON',
import_session_json_tooltip: 'Nhập phiên từ JSON',
clear_conversation_btn_tooltip: 'Xóa toàn bộ tin nhắn trong cuộc trò chuyện này',
// Settings detail
settings_label_rtl: 'Bố cục chat từ phải sang trái',
@ -19932,19 +19931,19 @@ const LOCALES = {
password_env_var_locked_placeholder: 'Đã khóa: biến môi trường HERMES_WEBUI_PASSWORD đang được đặt',
disable_auth: 'Tắt xác thực',
sign_out: 'Đăng xuất',
settings_label_shutdown: 'Dừng server Hermes WebUI',
settings_label_shutdown: 'Dừng máy chủ Hermes WebUI',
settings_desc_shutdown_before_cmd: 'Dừng an toàn server WebUI local. Hữu ích nếu bạn khởi chạy bằng ',
settings_desc_shutdown_between_cmds: ' và muốn dừng mà không cần chuyển sang terminal. Bạn sẽ cần khởi chạy lại server (',
settings_desc_shutdown_after_cmd: ' hoặc ứng dụng native) trước khi WebUI có thể truy cập lại.',
settings_btn_shutdown: 'Dừng server',
settings_shutdown_confirm_title: 'Dừng Hermes WebUI',
settings_shutdown_confirm_message: 'Dừng server Hermes WebUI?',
settings_shutdown_confirm_message: 'Dừng máy chủ Hermes WebUI?',
settings_shutdown_confirm_btn: 'Dừng',
settings_shutdown_stopped_message: 'Server đã dừng. Bạn có thể đóng tab này.',
// Providers panel
providers_tab_title: 'Provider',
providers_section_title: 'Provider',
providers_section_meta: 'Quản lý API key cho các AI provider. Thay đổi có hiệu lực ngay lập tức.',
providers_section_title: 'Nhà cung cấp',
providers_section_meta: 'Quản lý API key cho các nhà cung cấp AI. Thay đổi có hiệu lực ngay lập tức.',
providers_status_configured: 'Đã cấu hình API key',
providers_status_not_configured: 'Chưa có API key',
providers_status_oauth: 'OAuth',
@ -19960,8 +19959,8 @@ const LOCALES = {
providers_remove: 'Xóa',
providers_saving: 'Đang lưu…',
providers_removing: 'Đang xóa…',
providers_enter_key: 'Vui lòng nhập API key',
providers_empty: 'Không tìm thấy provider có thể cấu hình.',
providers_enter_key: 'Vui lòng nhập khóa API',
providers_empty: 'Không tìm thấy nhà cung cấp nào có thể cấu hình.',
providers_key_updated: 'Đã lưu API key',
providers_key_removed: 'Đã xóa API key',
providers_key_placeholder_new: 'sk-...',
@ -19991,7 +19990,7 @@ const LOCALES = {
provider_quota_status_unsupported: 'không hỗ trợ',
provider_quota_used_meta: '{0} đã dùng',
provider_quota_resets_meta: 'reset lúc {0}',
provider_quota_credential_pool: 'Pool credential',
provider_quota_credential_pool: 'Nhóm thông tin xác thực',
provider_quota_credential_label: 'Credential {0}',
provider_quota_pool_plans: 'Gói: {0}',
provider_quota_pool_no_windows: 'Không có cửa sổ giới hạn tài khoản nào được báo cáo.',
@ -20023,9 +20022,9 @@ const LOCALES = {
onboarding_open: 'Mở Hermes',
onboarding_step_system_title: 'Kiểm tra hệ thống',
onboarding_step_system_desc: 'Xác minh Hermes Agent và khả năng truy cập config.',
onboarding_step_setup_title: 'Thiết lập provider',
onboarding_step_setup_title: 'Thiết lập nhà cung cấp',
onboarding_step_setup_desc: 'Lưu cấu hình provider tối thiểu cho Hermes.',
onboarding_step_workspace_title: 'Workspace + model',
onboarding_step_workspace_title: 'Không gian làm việc + model',
onboarding_step_workspace_desc: 'Chọn mặc định cho phiên mới và chat.',
onboarding_step_password_title: 'Mật khẩu tùy chọn',
onboarding_step_password_desc: 'Bảo vệ Web UI trước khi chia sẻ.',
@ -20039,7 +20038,7 @@ const LOCALES = {
onboarding_check_password: 'Mật khẩu',
onboarding_check_password_enabled: 'Đã bật',
onboarding_check_password_disabled: 'Chưa bật',
onboarding_check_provider: 'Cấu hình provider',
onboarding_check_provider: 'Cấu hình nhà cung cấp',
onboarding_check_provider_ready: 'Sẵn sàng chat',
onboarding_check_provider_partial: 'Đã lưu nhưng chưa hoàn chỉnh',
onboarding_check_provider_pending: 'Cần xác minh',
@ -20047,7 +20046,7 @@ const LOCALES = {
onboarding_env_file: '.env file:',
onboarding_unknown: 'Không xác định',
onboarding_current_provider: 'Thiết lập hiện tại:',
onboarding_missing_imports: 'Import còn thiếu:',
onboarding_missing_imports: 'Thiếu các import sau:',
onboarding_notice_setup_required: 'Hãy chọn cách thiết lập provider đơn giản tại đây. Các luồng OAuth nâng cao hiện vẫn nên thực hiện trong Hermes CLI.',
onboarding_notice_setup_already_ready: 'Đã phát hiện cấu hình provider Hermes đang hoạt động. Bạn có thể giữ nguyên hoặc thay thế tại đây.',
onboarding_oauth_provider_ready_title: 'Provider đã xác thực',
@ -20116,7 +20115,7 @@ const LOCALES = {
onboarding_error_probe_failed: 'Không thể xác minh Base URL đã cấu hình.',
onboarding_error_workspace_required: 'Workspace là bắt buộc.',
onboarding_error_model_required: 'Model là bắt buộc.',
onboarding_complete: 'Hoàn tất onboarding',
onboarding_complete: 'Hoàn tất thiết lập ban đầu',
// panel/runtime i18n
error_prefix: 'Lỗi: ',
@ -20165,12 +20164,12 @@ const LOCALES = {
cron_mode_label: 'Chế độ',
cron_script_job_banner: 'Chạy script theo lịch — stdout được gửi tới đích. Không có agent, prompt hoặc skill.',
cron_script_card_title: 'Script',
cron_script_output: 'Output script',
cron_script_output: 'Đầu ra script',
cron_script_path_label: 'Đường dẫn script',
cron_script_path_hint: 'Được resolve dưới ~/.hermes/scripts/ trừ khi là đường dẫn tuyệt đối. Hãy chỉnh sửa file script trên server để thay đổi hành vi.',
cron_script_badge_title: 'Script job (không có agent)',
cron_script_badge_title: 'Job script (không dùng agent)',
cron_workdir_label: 'Thư mục làm việc',
cron_view_full_output: 'Xem toàn bộ output',
cron_view_full_output: 'Xem toàn bộ đầu ra',
cron_all_runs: 'Tất cả lần chạy',
cron_hide_runs: 'Ẩn lần chạy',
cron_no_runs_yet: '(chưa có lần chạy nào)',
@ -20218,7 +20217,7 @@ const LOCALES = {
tasks_empty_sub: 'Chọn một job từ sidebar để xem chi tiết và các lần chạy hoặc tạo job mới.',
workspaces_empty_title: 'Chọn một space',
workspaces_empty_sub: 'Chọn một space từ sidebar để xem file và cài đặt hoặc thêm space mới.',
profiles_empty_title: 'Chọn một profile',
profiles_empty_title: 'Chọn một hồ sơ',
profiles_empty_sub: 'Chọn một profile agent từ sidebar để xem và chỉnh sửa cài đặt hoặc tạo profile mới.',
memory_notes_label: 'bộ nhớ (ghi chú)',
memory_saved: 'Đã lưu bộ nhớ',
@ -20243,8 +20242,8 @@ const LOCALES = {
no_soul_yet: 'Chưa định nghĩa soul.',
workspace_choose_path: 'Chọn đường dẫn workspace',
workspace_choose_path_meta: 'Thêm đường dẫn hợp lệ và chuyển cuộc trò chuyện này',
workspace_manage: 'Quản lý workspace',
workspace_manage_meta: 'Mở panel Spaces',
workspace_manage: 'Quản lý không gian làm việc',
workspace_manage_meta: 'Mở bảng Spaces',
workspace_use_title: 'Dùng trong phiên hiện tại',
workspace_use: 'Sử dụng',
workspace_add_path_placeholder: 'Thêm đường dẫn workspace (ví dụ: /home/user/my-project)',
@ -20257,7 +20256,7 @@ const LOCALES = {
workspace_remove_confirm_title: 'Xóa workspace',
workspace_remove_confirm_message: (path) => `Xóa "${path}"?`,
workspace_removed: 'Đã xóa workspace',
workspace_switch_prompt_title: 'Chuyển workspace',
workspace_switch_prompt_title: 'Chuyển không gian làm việc',
workspace_switch_prompt_message: 'Nhập đường dẫn workspace tuyệt đối để thêm và chuyển cuộc trò chuyện này sang đó.',
workspace_switch_prompt_confirm: 'Chuyển',
workspace_switch_prompt_placeholder: '/Users/you/project',
@ -20269,18 +20268,18 @@ const LOCALES = {
workspace_switched_to: (name) => `Đã chuyển sang ${name}`,
profiles_no_profiles: 'Không tìm thấy profile nào.',
profile_api_keys_configured: 'Đã cấu hình API key',
profile_gateway_running: 'Gateway đang chạy',
profile_gateway_running: 'Gateway đang hoạt động',
profile_gateway_stopped: 'Gateway đã dừng',
profile_active: 'ĐANG HOẠT ĐỘNG',
profile_no_configuration: 'Không có cấu hình',
profile_skill_count: (count) => `${count} kỹ năng`,
profile_use: 'Sử dụng',
profile_switch_title: 'Chuyển sang profile này',
profile_switch_title: 'Chuyển sang hồ sơ này',
profile_delete_title: 'Xóa profile này',
profile_default_label: '(mặc định)',
profile_name_placeholder: 'Tên profile (chữ thường, a-z 0-9 dấu gạch ngang)',
profile_clone_label: 'Nhân bản cấu hình từ profile đang hoạt động',
profile_model_label: 'Model / provider',
profile_model_label: 'Model / nhà cung cấp',
profile_model_hint: 'Chọn từ các provider và model đã cấu hình cho profile mới này.',
profile_model_use_default: 'Dùng mặc định của profile đang hoạt động',
profile_base_url_placeholder: 'Base URL (tùy chọn, ví dụ: http://localhost:11434)',
@ -20292,7 +20291,7 @@ const LOCALES = {
profile_name_rule: 'Chỉ chữ thường, số, dấu gạch ngang và gạch dưới',
profile_base_url_rule: 'Base URL phải bắt đầu bằng http:// hoặc https://',
profile_created: (name) => `Đã tạo profile: ${name}`,
profile_delete_confirm_title: (name) => `Xóa profile "${name}"?`,
profile_delete_confirm_title: (name) => `Xóa hồ sơ "${name}"?`,
profile_delete_confirm_message: 'Toàn bộ phiên, cấu hình, kỹ năng và bộ nhớ của profile này sẽ bị xóa vĩnh viễn. Hành động này không thể hoàn tác.',
profile_deleted: (name) => `Đã xóa profile: ${name}`,
active_conversation_none: 'Chưa chọn cuộc trò chuyện hoạt động.',
@ -20309,8 +20308,8 @@ const LOCALES = {
skill_name: 'Tên',
skill_category: 'Danh mục',
skill_category_placeholder: 'Tùy chọn, ví dụ: devops',
skill_content: 'Nội dung SKILL.md',
skill_content_placeholder: 'YAML frontmatter + nội dung markdown',
skill_content: 'Nội dung tệp SKILL.md',
skill_content_placeholder: 'YAML frontmatter + nội dung Markdown',
skill_rename_not_supported: 'Không hỗ trợ đổi tên kỹ năng. Hãy tạo kỹ năng mới và xóa kỹ năng cũ để đổi tên.',
skill_metadata: 'Metadata',
// cron form
@ -20321,7 +20320,7 @@ const LOCALES = {
cron_schedule_once_warning: "Các dạng thời lượng như '30m' chỉ chạy một lần và sẽ bị xóa sau khi chạy. Dùng 'every 30m' để giữ job lặp lại.",
cron_prompt_label: 'Prompt',
cron_deliver_label: 'Gửi output tới',
cron_deliver_local: 'Local (chỉ lưu output)',
cron_deliver_local: 'Cục bộ (chỉ lưu đầu ra)',
cron_deliver_custom: 'Đích gửi tùy chỉnh',
cron_profile_label: 'Profile',
cron_profile_server_default: 'mặc định của server',
@ -20344,6 +20343,208 @@ const LOCALES = {
profile_name_label: 'Tên',
profile_base_url_label: 'Base URL',
profile_api_key_label: 'API key',
approval_gateway_unsupported: 'Phê duyệt cần gateway mới hơn. Hãy nâng cấp Hermes gateway đang kết nối để bật tính năng này.',
approval_skip_all: '⚡ Bỏ qua tất cả trong phiên này',
approval_skip_all_title: 'Bỏ qua mọi yêu cầu phê duyệt cho phiên này',
checkpoint_title: 'Checkpoint',
checkpoint_empty: 'Không tìm thấy checkpoint nào cho workspace này.',
checkpoint_loading: 'Đang tải điểm khôi phục…',
checkpoint_error: 'Không thể tải checkpoint',
checkpoint_date: 'Ngày',
checkpoint_message: 'Tin nhắn',
checkpoint_files: 'Tệp',
checkpoint_view_diff: 'Xem diff',
checkpoint_restore: 'Khôi phục',
checkpoint_restore_confirm_title: 'Khôi phục điểm khôi phục?',
checkpoint_restore_confirm_message: (ckpt) => `Khôi phục workspace về checkpoint "${ckpt}"? Thao tác này sẽ ghi đè các tệp bằng bản đã lưu. Các tệp được thêm sau checkpoint này sẽ không bị xóa.`,
checkpoint_restored: 'Đã khôi phục checkpoint',
checkpoint_diff_title: 'Các thay đổi trong checkpoint',
checkpoint_diff_no_changes: 'Không tìm thấy khác biệt nào giữa checkpoint này và workspace hiện tại.',
checkpoint_diff_files_changed: (n) => `${n} tệp đã thay đổi`,
discard: 'Bỏ thay đổi',
clear: 'Xóa',
create: 'Tạo',
remove: 'Xóa bỏ',
cmd_yolo: 'Bật/tắt chế độ YOLO (bỏ qua phê duyệt)',
composer_send: 'Gửi tin nhắn',
composer_queue: 'Xếp hàng tin nhắn',
composer_interrupt: 'Ngắt và gửi',
composer_steer: 'Điều hướng phản hồi hiện tại',
composer_stop: 'Dừng tạo phản hồi',
composer_disabled_clarify: 'Hãy trả lời yêu cầu làm rõ',
composer_disabled_compression: 'Đang chờ nén hoàn tất',
composer_disabled_empty: 'Nhập tin nhắn để gửi',
composer_compression_will_queue: 'Nhập tin nhắn — hệ thống sẽ xếp hàng và gửi sau khi nén xong',
composer_mobile_workspace: 'Workspace',
composer_mobile_model: 'Model',
composer_mobile_reasoning: 'Lập luận',
composer_mobile_context: 'Ngữ cảnh',
context_block_name_default: 'Ngữ cảnh',
context_block_flush_aria: 'Các khối ngữ cảnh sẽ được gửi kèm khi bạn nhấn gửi',
csv_loading: 'Đang tải CSV',
csv_too_large: 'Tệp CSV quá lớn để hiển thị trực tiếp',
csv_no_data: 'Tệp CSV không đủ dữ liệu để hiển thị thành bảng',
csv_error: 'Không thể tải tệp CSV',
csv_header_note: 'Hàng đầu tiên được hiển thị làm tiêu đề bảng',
excalidraw_loading: 'Đang tải sơ đồ',
excalidraw_too_large: 'Tệp Excalidraw quá lớn để hiển thị trực tiếp',
excalidraw_invalid: 'Định dạng tệp Excalidraw không hợp lệ',
excalidraw_error: 'Không thể tải tệp Excalidraw',
excalidraw_label: 'Sơ đồ',
excalidraw_download: 'Tải xuống',
excalidraw_empty: 'Sơ đồ trống',
excalidraw_render_error: 'Không thể render sơ đồ',
excalidraw_simplified: 'Bản xem trước SVG đã được giản lược — không hoàn toàn giống canvas Excalidraw',
gateway_start: 'Bắt đầu',
gateway_stop: 'Dừng',
gateway_restart: 'Khởi động lại',
gateway_start_success: 'Đã khởi động gateway.',
gateway_restart_success: 'Đã khởi động lại gateway.',
gateway_stop_success: 'Đã dừng gateway.',
gateway_start_failed: 'Khởi động gateway thất bại',
gateway_stop_failed: 'Dừng gateway thất bại',
gateway_restart_failed: 'Khởi động lại gateway thất bại',
gateway_running: 'Đang chạy',
gateway_session: 'phiên',
gateway_sessions: 'phiên',
gateway_status_load_failed: 'Không thể tải trạng thái gateway',
insights_model_health_title: 'So sánh sức khỏe model',
insights_model_health_provider: 'Nhà cung cấp',
insights_model_health_replacement: 'Gợi ý thay thế',
insights_model_health_cost_per_m: 'Chi phí / 1M',
media_audio_label: 'Âm thanh',
media_svg_label: 'Sơ đồ',
media_video_label: 'Video',
media_download: 'Tải xuống',
model_show_all_models: 'Hiển thị tất cả {0} model',
outline_title: 'Dàn ý',
outline_empty: 'Chưa có câu hỏi nào.',
outline_loading: 'Đang tải…',
pdf_truncated: 'Đang hiển thị {0} trên {1} trang đầu — hãy tải xuống để xem toàn bộ tài liệu',
session_select_mode: 'Chọn',
session_select_mode_desc: 'Chọn các cuộc trò chuyện để quản lý hàng loạt',
session_select_all: 'Chọn tất cả',
session_deselect_all: 'Bỏ chọn tất cả',
session_selected_count: '{0} mục đã chọn',
session_batch_archive: 'Lưu trữ',
session_batch_delete: 'Xóa',
session_batch_move: 'Chuyển vào dự án',
session_no_selection: 'Chưa chọn cuộc trò chuyện nào',
session_batch_delete_confirm: 'Xóa {0} cuộc trò chuyện?',
session_batch_archive_confirm: 'Lưu trữ {0} cuộc trò chuyện?',
session_batch_delete_worktree_confirm: 'Xóa {0} cuộc trò chuyện? {1} cuộc trò chuyện dùng worktree sẽ giữ nguyên thư mục worktree trên đĩa.',
session_batch_archive_worktree_confirm: 'Lưu trữ {0} cuộc trò chuyện? {1} cuộc trò chuyện dùng worktree sẽ giữ nguyên thư mục worktree trên đĩa.',
session_toolsets_profile_defaults: 'Mặc định của profile đang hoạt động',
session_toolsets_use_profile_defaults: 'Dùng mặc định của profile đang hoạt động',
session_toolsets_configured_servers: 'Máy chủ MCP đã cấu hình',
session_toolsets_loading_servers: 'Đang tải các máy chủ đã cấu hình...',
session_toolsets_no_configured_servers: 'Chưa có máy chủ MCP nào được cấu hình',
session_worktree_remove: 'Gỡ worktree',
session_worktree_remove_desc: (path) => `Xóa git worktree tại ${path} khỏi đĩa`,
session_worktree_remove_not_exists: (path) => `Worktree tại ${path} không còn tồn tại trên đĩa.`,
session_worktree_remove_confirm: (path) => `Gỡ git worktree khỏi đĩa?\n\nĐường dẫn: ${path}\n\nThao tác này sẽ xóa toàn bộ thư mục worktree. Dữ liệu phiên vẫn được giữ trong WebUI.`,
session_worktree_remove_confirm_label: 'Gỡ bỏ',
session_worktree_removed: 'Đã gỡ worktree.',
session_worktree_remove_failed: 'Gỡ worktree thất bại: ',
session_worktree_remove_status_failed: 'Không thể đọc trạng thái worktree: ',
session_worktree_remove_locked_by_stream: 'Không thể gỡ — một phiên streaming đang sử dụng worktree này.',
session_worktree_remove_locked_by_terminal: 'Không thể gỡ — một terminal đang hoạt động đang sử dụng worktree này.',
session_worktree_remove_unsafe_blocked: 'Hãy xử lý các thay đổi cục bộ hoặc commit chưa push trước khi gỡ worktree này.',
session_worktree_remove_dirty_warning: 'CẢNH BÁO: Worktree này có thay đổi chưa commit và sẽ bị mất.',
session_worktree_remove_untracked_warning: (count) => `${count} tệp chưa theo dõi sẽ bị xóa vĩnh viễn.`,
session_worktree_remove_ahead_warning: (ahead) => `${ahead} commit chưa push sẽ bị mất.`,
settings_heading_title: 'Trung tâm điều khiển',
settings_heading_subtitle: 'Tùy chọn, công cụ hội thoại và điều khiển hệ thống.',
settings_section_conversation_title: 'Hội thoại',
settings_section_appearance_title: 'Giao diện',
settings_section_appearance_meta: 'Theme, màu nhấn và phong cách hiển thị.',
settings_section_preferences_title: 'Tùy chọn',
settings_section_preferences_meta: 'Thiết lập mặc định và hành vi giao diện của Hermes Web UI.',
settings_section_system_title: 'Hệ thống',
settings_section_system_meta: 'Phiên bản instance và các điều khiển truy cập.',
settings_search_placeholder: 'Tìm trong cài đặt…',
settings_search_no_results: 'Không tìm thấy cài đặt nào.',
settings_check_now: 'Kiểm tra ngay',
settings_checking: 'Đang kiểm tra…',
settings_up_to_date: 'Đã cập nhật ✓',
settings_updates_available: '{count} bản cập nhật khả dụng',
settings_updates_disabled: 'Đã tắt kiểm tra cập nhật',
settings_update_check_failed: 'Kiểm tra cập nhật thất bại',
settings_update_no_git: 'Không thể kiểm tra cập nhật (không phải bản checkout git)',
settings_label_workspace_panel_open: 'Giữ panel workspace mở theo mặc định',
settings_desc_workspace_panel_open: 'Khi bật, panel workspace / trình duyệt tệp sẽ tự mở với mỗi phiên mới. Bạn vẫn có thể đóng thủ công bất cứ lúc nào.',
settings_label_workspace_todos_tab: 'Hiển thị tab Todos trong bảng workspace',
settings_desc_workspace_todos_tab: 'Khi bật, một tab Todos sẽ xuất hiện trong panel workspace. Panel Todos ở sidebar vẫn có sẵn.',
settings_label_session_jump_buttons: 'Hiển thị nút nhảy trong phiên',
settings_desc_session_jump_buttons: 'Hiển thị các nút Đầu và Cuối nổi khi đọc lịch sử phiên dài.',
settings_label_terminal_auto_expand: 'Tự mở rộng terminal khi có output',
settings_desc_terminal_auto_expand: 'Tự động mở rộng panel terminal đang thu gọn khi một lệnh đang chạy phát sinh output mới.',
settings_label_virtualize_transcript: 'Ảo hóa transcript dài (thử nghiệm)',
settings_desc_virtualize_transcript: 'Thử nghiệm. Chỉ render phần đang hiển thị của transcript rất dài (trên 80 tin nhắn) để tăng tốc cuộn. Mặc định tắt vì có thể gây rung cuộn ở các phiên dài có nhiều khối tool-call lớn. Bật để thử; để tắt nếu bạn muốn render toàn bộ transcript một lần (và để Find/Ctrl+F của trình duyệt tìm được toàn bộ cuộc trò chuyện).',
settings_main_advanced_button_aria: 'Tùy chọn nâng cao cho model chính',
settings_main_advanced_title: 'Tùy chọn model chính',
settings_main_advanced_subtitle: 'Cấu hình nâng cao cho model chat mặc định.',
settings_main_advanced_saved: 'Đã lưu tùy chọn model chính',
settings_main_advanced_save_failed: 'Lưu tùy chọn model chính thất bại',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'phân tích ảnh / ảnh chụp màn hình',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'tóm tắt ngữ cảnh',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'trích xuất và tóm tắt trang web',
settings_aux_task_session_search: 'Tìm phiên',
settings_aux_task_session_search_desc: 'truy hồi hội thoại cũ',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'phê duyệt lệnh thông minh',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'suy luận công cụ MCP',
settings_aux_task_title_generation: 'Sinh tiêu đề',
settings_aux_task_title_generation_desc: 'tiêu đề phiên',
settings_aux_task_skills_hub: 'Kho kỹ năng',
settings_aux_task_skills_hub_desc: 'tìm / cài kỹ năng',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'rà soát việc dùng kỹ năng',
settings_aux_task_kanban_decomposer: 'Bộ phân rã Kanban',
settings_aux_task_kanban_decomposer_desc: 'phân rã công việc',
settings_aux_task_profile_describer: 'Bộ mô tả profile',
settings_aux_task_profile_describer_desc: 'tóm tắt profile',
settings_aux_task_triage_specifier: 'Bộ đặc tả phân loại',
settings_aux_task_triage_specifier_desc: 'đặc tả phân loại issue / task',
settings_aux_advanced_button_title: 'Tùy chọn nâng cao',
settings_aux_advanced_button_aria: 'Tùy chọn nâng cao cho {task}',
settings_aux_advanced_title: 'Tùy chọn cho {task}',
settings_aux_advanced_subtitle: 'Cấu hình nâng cao cho auxiliary.',
settings_aux_advanced_save: 'Lưu tùy chọn',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Ghi đè endpoint nhà cung cấp (tùy chọn).',
settings_aux_advanced_timeout: 'Thời gian chờ (giây)',
settings_aux_advanced_timeout_desc: 'Thời gian chờ request cho auxiliary task này. Để trống để dùng mặc định của Hermes.',
settings_aux_advanced_download_timeout: 'Thời gian chờ tải xuống (giây)',
settings_aux_advanced_download_timeout_desc: 'Chỉ áp dụng cho các tác vụ tải media / nội dung, ví dụ vision. Để trống để dùng mặc định.',
settings_aux_advanced_max_concurrency: 'Số lượng chạy đồng thời tối đa',
settings_aux_advanced_max_concurrency_desc: 'Giới hạn đồng thời cho từng tác vụ (tùy chọn). Để trống để dùng mặc định.',
settings_aux_advanced_extra_body: 'Phần JSON bổ sung cho body',
settings_aux_advanced_extra_body_desc: 'Đối tượng JSON tùy chọn sẽ được gộp vào body request model.',
settings_aux_advanced_api_key: 'Ghi đè API key',
settings_aux_advanced_api_key_set_hint: 'API key đã được thiết lập. Để trống để giữ nguyên, hoặc dùng nút xóa để bỏ.',
settings_aux_advanced_api_key_empty_hint: 'Để trống để dùng credential mặc định của provider.',
settings_aux_advanced_api_key_clear: 'Xóa ghi đè API key hiện có',
settings_aux_advanced_saved: 'Đã lưu tùy chọn auxiliary',
settings_aux_advanced_extra_body_invalid_json: 'Extra body phải là JSON hợp lệ',
settings_aux_advanced_extra_body_object_required: 'Extra body phải là một đối tượng JSON',
settings_aux_advanced_save_failed: 'Lưu tùy chọn auxiliary thất bại',
symlink_link_to: 'Liên kết mềm → {target}',
text_pasted: 'Đã đính kèm văn bản đã dán dưới dạng ',
wiki_browse: 'Duyệt wiki',
wiki_search_placeholder: 'Tìm trang…',
wiki_no_pages: 'Không tìm thấy trang wiki nào',
wiki_not_configured: 'Wiki chưa được cấu hình',
yolo_no_session: 'Không có phiên hoạt động',
yolo_enabled: '⚡ Chế độ YOLO BẬT — phê duyệt sẽ bị bỏ qua trong phiên này',
yolo_disabled: 'Chế độ YOLO TẮT',
yolo_pill_label: 'YOLO',
yolo_pill_title_active: 'Chế độ YOLO đang bật — bấm để tắt',
},
};

View file

@ -0,0 +1,112 @@
from collections import Counter
from pathlib import Path
import re
REPO = Path(__file__).resolve().parent.parent
def read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def test_vietnamese_locale_block_exists():
src = read(REPO / "static" / "i18n.js")
assert "\n _lang: 'vi'," in src
assert "_label: 'Tiếng Việt'" in src
assert "_speech: 'vi-VN'" in src
def extract_locale_block(src: str, locale_key: str) -> str:
start_match = re.search(rf"\b{re.escape(locale_key)}\s*:\s*\{{", src)
assert start_match, f"{locale_key} locale block not found"
brace_start = start_match.end() - 1
depth = 0
in_single = False
in_double = False
in_backtick = False
escape = False
for i in range(brace_start, len(src)):
ch = src[i]
if escape:
escape = False
continue
if in_single:
if ch == "\\":
escape = True
elif ch == "'":
in_single = False
continue
if in_double:
if ch == "\\":
escape = True
elif ch == '"':
in_double = False
continue
if in_backtick:
if ch == "\\":
escape = True
elif ch == "`":
in_backtick = False
continue
if ch == "'":
in_single = True
continue
if ch == '"':
in_double = True
continue
if ch == "`":
in_backtick = True
continue
if ch == "{":
depth += 1
continue
if ch == "}":
depth -= 1
if depth == 0:
return src[brace_start + 1 : i]
raise AssertionError(f"{locale_key} locale block braces are not balanced")
def test_vietnamese_locale_includes_representative_translations():
src = read(REPO / "static" / "i18n.js")
vi_block = extract_locale_block(src, "vi")
expected = [
"settings_heading_title: 'Trung tâm điều khiển'",
"settings_heading_subtitle: 'Tùy chọn, công cụ hội thoại và điều khiển hệ thống.'",
"approval_skip_all: '⚡ Bỏ qua tất cả trong phiên này'",
"checkpoint_title: 'Checkpoint'",
"composer_send: 'Gửi tin nhắn'",
"gateway_restart: 'Khởi động lại'",
"wiki_browse: 'Duyệt wiki'",
"yolo_pill_title_active: 'Chế độ YOLO đang bật — bấm để tắt'",
]
for entry in expected:
assert entry in vi_block
def test_vietnamese_locale_covers_english_keys():
src = read(REPO / "static" / "i18n.js")
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
en_keys = set(key_pattern.findall(extract_locale_block(src, "en")))
vi_keys = set(key_pattern.findall(extract_locale_block(src, "vi")))
missing = sorted(en_keys - vi_keys)
assert not missing, f"Vietnamese locale missing keys: {missing}"
def test_vietnamese_locale_has_no_duplicate_keys():
src = read(REPO / "static" / "i18n.js")
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
keys = key_pattern.findall(extract_locale_block(src, "vi"))
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
assert not duplicates, f"Vietnamese locale has duplicate keys: {duplicates}"