diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4e8966..eaba2cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.621] — 2026-06-24 — Release WB (Vietnamese UI language) + +### Added + +- **Vietnamese (Tiếng Việt) is now a selectable UI language.** Adds a full `vi` locale to the language picker, including the login page. Thanks @thanhtantran. (#4241) + ## [v0.51.620] — 2026-06-24 — Release WA (fix /api/sessions CPU spike + slowness during streaming) ### Fixed diff --git a/api/routes.py b/api/routes.py index dbd1bbcb..207a34e0 100644 --- a/api/routes.py +++ b/api/routes.py @@ -7052,6 +7052,15 @@ _LOGIN_LOCALE = { "invalid_pw": "Nieprawid\u0142owe has\u0142o", "conn_failed": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", }, + "vi": { + "lang": "vi", + "title": "\u0110\u0103ng nh\u1eadp", + "subtitle": "Nh\u1eadp m\u1eadt kh\u1ea9u c\u1ee7a b\u1ea1n \u0111\u1ec3 ti\u1ebfp t\u1ee5c", + "placeholder": "M\u1eadt kh\u1ea9u", + "btn": "\u0110\u0103ng nh\u1eadp", + "invalid_pw": "M\u1eadt kh\u1ea9u kh\u00f4ng h\u1ee3p l\u1ec7", + "conn_failed": "K\u1ebft n\u1ed1i th\u1ea5t b\u1ea1i", + }, } diff --git a/static/i18n.js b/static/i18n.js index 66617e2f..cc450645 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -20011,6 +20011,1580 @@ const LOCALES = { checkpoint_diff_no_changes: 'Nie znaleziono różnic między tym punktem kontrolnym a obecnym obszarem roboczym.', checkpoint_diff_files_changed: (n) => n === 1 ? '1 plik zmieniony' : `${n} zmienionych plików`, }, + vi: { + offline_title: 'Mất kết nối', + offline_browser_detail: 'Trình duyệt báo rằng thiết bị này đang ngoại tuyến.', + offline_network_detail: 'Không thể kết nối tới Hermes từ trình duyệt lúc này.', + offline_autorefresh: 'Trang sẽ tự động làm mới khi Hermes khả dụng trở lại.', + offline_check_now: 'Kiểm tra ngay', + offline_checking: 'Đang kiểm tra…', + offline_stream_waiting: 'Mất kết nối. Đang chờ làm mới…', + _lang: 'vi', + _label: 'Tiếng Việt', + _speech: 'vi-VN', + // boot.js + cancelling: 'Đang hủy…', + cancel_failed: 'Hủy thất bại: ', + mic_denied: 'Quyền truy cập microphone bị từ chối. Kiểm tra quyền trình duyệt.', + mic_no_speech: 'Không phát hiện giọng nói. Hãy thử lại.', + mic_network: 'Nhận diện giọng nói không khả dụng.', + mic_error: 'Lỗi nhập giọng nói: ', + // Voice + voice_dictate: 'Đọc chính tả', + voice_dictate_active: 'Dừng đọc chính tả', + voice_recording_active: 'Dừng ghi âm', + voice_mode_toggle: 'Chế độ giọng nói', + voice_mode_toggle_active: 'Thoát chế độ giọng nói', + // Turn-based voice mode (#1333) + voice_listening: 'Đang nghe…', + voice_speaking: 'Đang nói…', + voice_thinking: 'Đang suy nghĩ…', + voice_error: 'Trình duyệt không hỗ trợ giọng nói', + voice_mode_active: 'Đã bật chế độ giọng nói', + voice_mode_off: 'Đã tắt chế độ giọng nói', + session_imported: 'Đã nhập phiên trò chuyện', + import_failed: 'Nhập thất bại: ', + import_invalid_json: 'JSON không hợp lệ', + image_pasted: 'Đã dán ảnh: ', + // messages.js + edit_message: 'Chỉnh sửa tin nhắn', + regenerate: 'Tạo lại phản hồi', + copy: 'Sao chép', + copied: 'Đã sao chép!', + copy_failed: 'Sao chép thất bại', + selected_text_reply: 'Trả lời với đoạn đã chọn', + selected_text_reply_title: 'Thêm đoạn chat đã chọn làm ngữ cảnh trích dẫn', + selected_text_reply_appended: 'Đã thêm đoạn đã chọn vào ô nhập', + saved_prompts_empty: 'Chưa có prompt đã lưu.', + saved_prompts_save_current: 'Lưu nội dung hiện tại thành prompt', + saved_prompts_saved: 'Đã lưu prompt', + saved_prompts_delete: 'Xóa prompt', + saved_prompts_empty_input: 'Hãy nhập prompt trước khi lưu', + + diff_loading: 'Đang tải diff', + diff_error: 'Không thể tải file patch', + diff_too_large: 'File patch quá lớn để hiển thị trực tiếp', + tree_view: 'Cây thư mục', + raw_view: 'Dữ liệu thô', + parse_failed_note: 'phân tích thất bại', + you: 'Bạn', + mcp_servers_title: 'Máy chủ MCP', + mcp_servers_desc: 'Xem các máy chủ MCP được cấu hình trong config.yaml.', + 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 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', + mcp_field_timeout: 'Thời gian chờ (giây)', + mcp_save: 'Lưu', + mcp_cancel: 'Hủy', + mcp_name_required: 'Tên máy chủ là bắt buộc.', + mcp_url_required: 'URL là bắt buộc cho transport HTTP.', + mcp_command_required: 'Lệnh là bắt buộc cho transport stdio.', + mcp_saved: 'Đã lưu máy chủ MCP.', + mcp_save_failed: 'Lưu máy chủ MCP thất bại.', + mcp_delete_confirm_title: 'Xóa máy chủ MCP', + mcp_delete_confirm_message: 'Xóa máy chủ MCP "{0}"? Hành động này không thể hoàn tác.', + mcp_deleted: 'Đã xóa máy chủ MCP.', + mcp_delete_failed: 'Xóa máy chủ MCP thất bại.', + mcp_load_failed: 'Tải danh sách máy chủ MCP thất bại.', + mcp_restart_hint: 'Hiện tại các thay đổi máy chủ chỉ ở chế độ chỉ đọc tại đây. Hãy sửa config.yaml và khởi động lại Hermes để áp dụng thay đổi.', + mcp_toggle_followup: 'Các điều khiển bật/tắt tạm thời chưa được hỗ trợ cho đến khi cơ chế reload MCP được xác định rõ.', + mcp_status_active: 'Đang hoạt động', + mcp_status_configured: 'Đã cấu hình', + mcp_status_disabled: 'Đã tắt', + mcp_status_invalid_config: 'Cấu hình không hợp lệ', + mcp_status_unknown: 'Không xác định', + mcp_tool_count: '{0} công cụ', + mcp_enabled_yes: 'Đã bật', + mcp_enabled_no: 'Đã tắt', + mcp_enable_server: 'Bật máy chủ MCP này', + mcp_disable_server: 'Tắt máy chủ MCP này', + mcp_enabled_toast: (name) => `Đã bật máy chủ MCP "${name}".`, + mcp_disabled_toast: (name) => `Đã tắt máy chủ MCP "${name}".`, + mcp_toggle_failed: 'Cập nhật máy chủ MCP thất bại.', + mcp_tools_title: 'Công cụ MCP', + mcp_tools_desc: 'Tìm kiếm các công cụ đã biết trên các máy chủ MCP đang hoạt động.', + mcp_tools_search_placeholder: 'Tìm công cụ theo tên, máy chủ hoặc mô tả…', + mcp_tools_no_tools: 'Không có công cụ MCP nào khả dụng từ runtime hiện tại.', + mcp_tools_no_matches: 'Không có công cụ MCP nào khớp với tìm kiếm.', + mcp_tools_load_failed: 'Tải công cụ MCP thất bại.', + mcp_tools_schema_empty: 'Không có tham số schema.', + mcp_tools_runtime_note: 'Danh sách công cụ chỉ sử dụng dữ liệu runtime MCP đang hoạt động đã biết; WebUI sẽ không tự khởi động hoặc kiểm tra máy chủ.', + mcp_tools_summary_no_matches: (query,total) => `Không có công cụ MCP nào khớp với “${query}” (${total} công cụ MCP tổng cộng).`, + mcp_tools_summary_none: 'Không có công cụ MCP để hiển thị.', + mcp_tools_summary_matching: (query) => ` khớp với “${query}”`, + mcp_tools_summary_total_note: (total) => ` (${total} công cụ MCP tổng cộng)`, + mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Hiển thị ${start}-${end} trên ${filtered} công cụ MCP${searchNote}${totalNote}. Trang ${page}/${pages}.`, + mcp_tools_page_size_prefix: 'Hiển thị', + mcp_tools_page_size_suffix: 'mỗi trang', + mcp_tools_per_page_aria: 'Số công cụ MCP mỗi trang', + mcp_tools_inactive_configured_servers: (servers) => `Đã cấu hình nhưng không hoạt động trong runtime WebUI này: ${servers}.`, + mcp_tools_pagination_label: 'Phân trang công cụ MCP', + mcp_tools_previous_page: '‹ Trước', + mcp_tools_previous_page_aria: 'Trang công cụ MCP trước', + mcp_tools_next_page: 'Tiếp ›', + mcp_tools_next_page_aria: 'Trang công cụ MCP tiếp theo', + // PDF preview (#480) + pdf_loading: 'Đang tải PDF {0}…', + pdf_too_large: 'PDF quá lớn để xem trực tiếp', + pdf_no_pages: 'PDF không có trang nào', + pdf_error: 'Không thể hiển thị xem trước PDF', + pdf_download: 'Tải PDF', + // HTML sandbox preview (#482) + html_loading: 'Đang tải xem trước HTML…', + 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 (chế độ cách ly)', + thinking: 'Đang suy nghĩ', + expand_all: 'Mở rộng tất cả', + collapse_all: 'Thu gọn tất cả', + edit_failed: 'Chỉnh sửa thất bại: ', + regen_failed: 'Tạo lại thất bại: ', + reconnect_active: 'Một phản hồi vẫn đang được tạo. Tải lại khi sẵn sàng?', + reconnect_finished: 'Một phản hồi đang diễn ra khi bạn rời đi lần trước. Tin nhắn có thể đã được cập nhật.', + // approval card + approval_heading: 'Yêu cầu phê duyệt', + 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 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', + approval_btn_deny: 'Từ chối', + approval_btn_deny_title: 'Từ chối — không chạy lệnh này', + approval_responding: 'Đang phản hồi\u2026', + clarify_heading: 'Cần làm rõ', + clarify_hint: 'Chọn một tùy chọn hoặc nhập câu trả lời của bạn bên dưới.', + clarify_other: 'Khác', + clarify_send: 'Gửi', + clarify_input_placeholder: 'Nhập phản hồi của bạn…', + clarify_responding: 'Đang phản hồi\u2026', + session_attention_approval: (n) => n === 1 ? 'Phê duyệt' : `${n} phê duyệt`, + session_attention_clarify: (n) => n === 1 ? 'Câu hỏi' : `${n} câu hỏi`, + session_attention_generic: (n) => n === 1 ? 'Chú ý' : `${n} mục`, + session_attention_approval_title: 'Đang chờ quyết định cấp quyền', + session_attention_clarify_title: 'Đang chờ câu trả lời của bạn', + session_attention_generic_title: 'Đang chờ thao tác từ người dùng', + untitled: 'Không tiêu đề', + n_messages: (n) => `${n} tin nhắn`, + load_older_messages: '↑ Cuộn lên hoặc bấm để tải tin nhắn cũ hơn', + session_jump_start: 'Đầu', + session_jump_start_label: 'Nhảy tới đầu phiên', + session_jump_end: 'Cuối', + session_jump_end_label: 'Nhảy tới cuối phiên', + session_new_message: 'Tin nhắn mới', + session_new_message_label: 'Có tin nhắn mới, nhảy xuống cuối', + jump_to_question: 'tới phản hồi', + jump_to_question_label: 'Nhảy tới đầu phản hồi này', + queued_label: 'Gửi sau phản hồi', + queued_count: (n) => n === 1 ? '1 đang chờ' : `${n} đang chờ`, + queued_cancel: 'Hủy tin nhắn đang chờ', + 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: '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ự 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', + model_group_configured: 'Đã cấu hình', + ws_search_placeholder: 'Tìm workspace…', + ws_no_results: 'Không tìm thấy workspace nào', + workspace_new_worktree_conversation: 'Cuộc trò chuyện mới trong worktree', + workspace_new_worktree_conversation_meta: 'Tạo git worktree độc lập cho workspace này.', + workspace_worktree_created: 'Đã tạo cuộc trò chuyện worktree', + workspace_worktree_failed: 'Tạo worktree thất bại: ', + session_worktree_badge: 'Worktree', + model_scope_advisory: 'Áp dụng cho cuộc trò chuyện này từ tin nhắn tiếp theo.', + model_scope_toast: 'Áp dụng cho cuộc trò chuyện này từ tin nhắn tiếp theo.', + // commands.js + cmd_clear: 'Xóa tin nhắn cuộc trò chuyện', + cmd_help: 'Hiển thị các lệnh slash khả dụng', + 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: 'Bí danh cũ của /compress', + cmd_model: 'Chuyển model (ví dụ: /model gpt-4o)', + 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)', + cmd_personality: 'Đổi personality của agent', + cmd_skills: 'Liệt kê các kỹ năng Hermes khả dụng', + cmd_use: 'Ép dùng một skill cho tin nhắn tiếp theo', + available_commands: 'Các lệnh khả dụng:', + type_slash: 'Gõ / để xem lệnh', + conversation_cleared: 'Đã xóa cuộc trò chuyện', + command_label: 'Lệnh', + context_compaction_label: 'Nén ngữ cảnh', + retrieval_context_label: 'Ngữ cảnh đã lập chỉ mục', + retrieval_context_preview: 'Các tin nhắn trước được lưu và có thể truy xuất bằng công cụ ngữ cảnh', + preserved_task_list_label: 'Danh sách tác vụ được giữ lại', + reference_only_label: 'Chỉ tham khảo', + model_usage: 'Cách dùng: /model ', + no_model_match: 'Không có model nào khớp "', + model_did_you_mean: (m) => ` — có phải bạn muốn "${m}"?`, + switched_to: 'Đã chuyển sang ', + workspace_usage: 'Cách dùng: /workspace ', + no_workspace_match: 'Không có workspace nào khớp "', + switched_workspace: 'Đã chuyển workspace sang: ', + workspace_switch_failed: 'Chuyển workspace thất bại: ', + new_session: 'Đã tạo phiên mới', + new_session_creating: 'Đang tạo cuộc trò chuyện mới…', + compressing: 'Đang yêu cầu nén ngữ cảnh...', + compress_running_label: 'Đang nén', + compress_complete_label: 'Nén hoàn tất', + auto_compress_label: 'Tự động nén', + compress_failed_label: 'Nén thất bại', + focus_label: 'Tiêu điểm', + token_usage_on: 'Đã bật token usage', + token_usage_off: 'Đã tắt token usage', + usage_cache_hit_detail: 'Cache: trúng {0}% ({1} đọc / {2} ghi)', + usage_cached_percent: '{0}% được cache', + theme_usage: 'Cách dùng: /theme ', + theme_set: 'Giao diện: ', + no_active_session: 'Không có phiên hoạt động', + cmd_queue: 'Đưa tin nhắn vào hàng chờ cho lượt tiếp theo', + cmd_goal: 'Đặt hoặc xem mục tiêu lâu dài', + goal_evaluating_progress: 'Đang đánh giá tiến độ mục tiêu…', + goal_working_toward: 'Đang thực hiện mục tiêu…', + goal_continuing_toast: 'Đang tiếp tục mục tiêu…', + goal_status_none: 'Không có mục tiêu nào đang hoạt động. Đặt bằng /goal .', + goal_status_active: (turns, max_turns, goal) => `⊙ Mục tiêu (đang hoạt động, ${turns}/${max_turns} lượt): ${goal}`, + goal_status_paused: (turns, max_turns, reason, goal) => `⏸ Mục tiêu (tạm dừng, ${turns}/${max_turns}${reason ? `, ${reason}` : ''}): ${goal}`, + goal_status_done: (turns, max_turns, goal) => `✓ Hoàn thành mục tiêu (${turns}/${max_turns}): ${goal}`, + goal_set: (turns, goal) => `⊙ Đã đặt mục tiêu (${turns} lượt): ${goal}`, + goal_paused: (goal) => `⏸ Đã tạm dừng mục tiêu: ${goal}`, + goal_resumed: (goal) => `▶ Đã tiếp tục mục tiêu: ${goal}`, + goal_cleared: 'Đã xóa mục tiêu.', + goal_no_goal: 'Không có mục tiêu hoạt động.', + goal_achieved: (reason) => `✓ Đã đạt mục tiêu: ${reason}`, + goal_paused_budget_exhausted: (turns, max_turns) => `⏸ Đã tạm dừng mục tiêu — đã dùng ${turns}/${max_turns} lượt. Dùng /goal resume để tiếp tục hoặc /goal clear để dừng.`, + 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 ', + 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 ', + cmd_interrupt_confirm: 'Đã ngắt — đang gửi tin nhắn mới', + cmd_steer_no_msg: 'Cách dùng: /steer ', + 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', + busy_steer_fallback: 'Steer không khả dụng — đã đưa vào hàng chờ lượt tiếp theo', + busy_interrupt_confirm: 'Đã ngắt — đang gửi tin nhắn mới', + settings_label_busy_input_mode: 'Chế độ nhập khi bận', + settings_desc_busy_input_mode: 'Điều khiển hành vi khi bạn gửi tin nhắn trong lúc agent đang chạy. Queue sẽ chờ; Interrupt sẽ hủy và bắt đầu mới; Steer sẽ chèn điều chỉnh giữa lượt mà không ngắt (sẽ fallback sang queue nếu agent hoặc stream không khả dụng).', + settings_label_fade_text_effect: 'Hiệu ứng mờ chữ', + settings_desc_fade_text_effect: 'Làm mờ dần các từ mới stream vào khi assistant đang phản hồi. Tương tự OpenWebUI; mặc định tắt để đạt hiệu năng tối đa.', + settings_busy_input_mode_queue: 'Xếp hàng phản hồi tiếp theo', + settings_busy_input_mode_interrupt: 'Ngắt lượt hiện tại', + settings_busy_input_mode_steer: 'Steer (điều chỉnh giữa lượt)', + + slash_skill_badge:'Kỹ năng', + slash_skill_desc:'Gọi kỹ năng này', + cmd_stop:'Dừng phản hồi hiện tại', + cmd_title:'Lấy hoặc đặt tiêu đề phiên', + cmd_retry:'Gửi lại tin nhắn cuối', + cmd_undo:'Xóa lượt trao đổi cuối', + cmd_btw:'Đặt câu hỏi phụ (tạm thời)', + cmd_btw_usage:'/btw — đặt câu hỏi phụ bằng ngữ cảnh phiên', + cmd_background:'Chạy prompt nền', + cmd_background_usage:'/background — chạy song song không chặn', + cmd_branch:'Fork cuộc trò chuyện này thành phiên mới', + cmd_branch_usage:'/branch [name] — fork cuộc trò chuyện (có thể đặt tên)', + branch_forked:'Đã fork sang phiên mới', + branch_failed:'Fork thất bại: ', + fork_from_here:'Fork từ đây', + forked_from:'Fork từ', + subagent_children:'Phiên subagent', + btw_asking:'Đang hỏi câu hỏi phụ...', + btw_label:'Câu hỏi phụ — không lưu vào lịch sử', + btw_done:'Đã trả lời câu hỏi phụ', + btw_no_answer:'Không nhận được câu trả lời.', + btw_failed:'Câu hỏi phụ thất bại: ', + bg_running:'Đang chạy nền...', + bg_complete:'Tác vụ nền hoàn tất', + bg_label:'Kết quả nền:', + bg_no_answer:'(không có câu trả lời)', + bg_failed:'Tác vụ nền thất bại: ', + undo_exchange:'Hoàn tác lượt trao đổi cuối', + cmd_status:'Hiển thị thông tin phiên', + cmd_voice:'Bật/tắt nhập microphone', + stream_stopped:'Đã dừng phản hồi.', + no_active_task:'Không có tác vụ hoạt động để dừng.', + cancel_unavailable:'Không thể hủy.', + retry_failed:'Gửi lại thất bại: ', + undo_failed:'Hoàn tác thất bại: ', + undid_n_messages:'Đã xóa', + undid_messages_suffix:'tin nhắn.', + status_heading:'Trạng thái phiên', + status_session_id:'ID phiên', + status_title:'Tiêu đề', + status_model:'Model', + status_provider:'Provider', + status_workspace:'Workspace', + status_personality:'Personality', + status_messages:'Tin nhắn', + status_agent_running:'Agent đang chạy', + status_profile: 'Hồ sơ', + status_hermes_home: 'Thư mục Hermes', + status_started: 'Bắt đầu', + status_updated: 'Cập nhật', + status_tokens: 'Token', + status_ephemeral: 'Ảnh chụp tạm thời — không lưu vào lịch sử transcript.', + status_no_tokens: 'Chưa dùng token', + status_unknown: 'Không xác định', + status_yes:'Có', + status_no:'Không', + status_load_failed:'Tải trạng thái thất bại: ', + title_current:'Tiêu đề hiện tại', + title_change_hint:'Dùng `/title ` để đổi tên.', + title_set:'Đã đặt tiêu đề thành', + cmd_webui_only_session:'Lệnh này không khả dụng cho các phiên được import từ CLI.', + cmd_voice_use_mic:'Bấm nút microphone trong composer.', + usage_heading:'Sử dụng token', + usage_default_model:'mặc định', + usage_unknown:'không xác định', + usage_input_tokens:'Token đầu vào', + usage_output_tokens:'Token đầu ra', + usage_total:'Tổng token', + usage_estimated_cost:'Chi phí ước tính', + usage_settings_tip:'Lưu ý: chi phí chỉ mang tính ước lượng.', + usage_load_failed:'Tải thống kê sử dụng thất bại: ', + 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 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 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 toàn cục', + session_toolsets_failed:'Cập nhật bộ công cụ thất bại: ', + 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 ` để chuyển hoặc `/personality none` để xóa.', + personalities_load_failed: 'Tải personality thất bại', + personality_cleared: 'Đã xóa personality', + personality_set: 'Personality: ', + failed_colon: 'Thất bại: ', + // ui.js + 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 đầu ra', + terminal_restart: 'Khởi động lại', + terminal_collapse: 'Thu gọn', + terminal_expand: 'Mở rộng', + terminal_close: 'Đóng', + terminal_input_placeholder: 'Chạy lệnh...', + terminal_start_failed: 'Khởi động terminal thất bại: ', + terminal_input_failed: 'Nhập terminal thất bại: ', + terminal_copy_failed: 'Sao chép thất bại: ', + 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: '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 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', + edit: 'Chỉnh sửa', + save_title: 'Lưu thay đổi', + edit_title: 'Chỉnh sửa file này', + saved: 'Đã lưu', + save_failed: 'Lưu thất bại: ', + image_load_failed: 'Không thể tải ảnh', + file_open_failed: 'Không thể mở file', + downloading: (name) => `Đang tải ${name}\u2026`, + mic_insecure_origin: 'Nhập giọng nói cần kết nối bảo mật. Hãy mở Hermes qua HTTPS hoặc từ localhost để dùng microphone.', + double_click_rename: 'Nhấp đúp để đổi tên', + session_rename_failed_no_row: 'Không thể bắt đầu đổi tên — không tìm thấy hàng.', + renamed_to: 'Đã đổi tên thành ', + rename_failed: 'Đổi tên thất bại: ', + moved_to: 'Đã chuyển tới ', + move_failed: 'Chuyển thất bại: ', + delete_title: 'Xóa', + delete_confirm: (name) => `Xóa ${name}?`, + delete_dir_confirm: (name) => `Xóa thư mục "${name}" và toàn bộ nội dung?`, + rename_title: 'Đổi tên', + rename_prompt: 'Tên mới:', + deleted: 'Đã xóa ', + delete_failed: 'Xóa thất bại: ', + 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', + open_in_vscode_failed: 'Mở bằng VS Code thất bại: ', + download_folder: 'Tải thư mục', + path_copied: 'Đã sao chép đường dẫn file vào clipboard', + path_copy_failed: 'Sao chép đường dẫn thất bại: ', + session_rename: 'Đổi tên cuộc trò chuyện', + session_rename_desc: 'Chỉnh sửa tiêu đề cuộc trò chuyện này', + session_title_regenerate: 'Tạo lại tiêu đề', + session_title_regenerate_desc: 'Yêu cầu Hermes tạo tiêu đề mới từ cuộc trò chuyện', + session_title_regenerating: 'Đang tạo lại tiêu đề…', + session_title_regenerated: (title) => `Đã tạo lại tiêu đề: ${title}`, + session_title_regenerate_failed: 'Tạo lại tiêu đề thất bại: ', + session_copy_link: 'Sao chép liên kết cuộc trò chuyện', + session_copy_link_desc: 'Sao chép liên kết trực tiếp tới cuộc trò chuyện này', + session_link_copied: 'Đã sao chép liên kết cuộc trò chuyện vào clipboard', + session_link_copy_failed: 'Sao chép liên kết cuộc trò chuyện thất bại: ', + new_file_prompt: 'Tên file mới (ví dụ: notes.md):', + 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 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 ', + create_failed: 'Tạo thất bại: ', + new_folder_prompt: 'Tên thư mục mới:', + folder_created: 'Đã tạo thư mục ', + folder_create_failed: 'Tạo thư mục thất bại: ', + workspace_auto_create_folder: 'Tạo thư mục nếu chưa tồn tại', + folder_add_as_space_btn: 'Thêm làm Space', + folder_add_as_space_msg: 'Thêm thư mục này làm space mới trong danh sách workspace?', + folder_add_as_space_title: 'Thêm làm Space?', + remove_title: 'Xóa', + empty_dir: '(trống)', + upload_failed: 'Tải lên thất bại: ', + upload_too_large: (maxMb, fileMb) => `File quá lớn (${fileMb} MB). Kích thước tối đa là ${maxMb} MB.`, + uploading: 'Đang tải lên…', + uploaded: 'Đã tải lên', + all_uploads_failed: (n) => `Toàn bộ ${n} lần tải lên đều thất bại`, + archive_extracted: (n, c) => `Đã giải nén ${n} file từ ${c} archive`, + session_pin: 'Ghim cuộc trò chuyện', + session_unpin: 'Bỏ ghim cuộc trò chuyện', + 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 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', + session_hide_external: 'Ẩn khỏi danh sách', + session_restore: 'Khôi phục cuộc trò chuyện', + session_archive_desc: 'Ẩn cuộc trò chuyện này cho tới khi hiện archived', + session_archive_worktree_desc: 'Ẩn cuộc trò chuyện này; giữ worktree trên đĩa', + session_hide_external_desc: 'Ẩn phiên import này khỏi WebUI mà không xóa lịch sử nguồn.', + session_restore_desc: 'Đưa cuộc trò chuyện này trở lại danh sách chính', + session_archived: 'Đã lưu trữ phiên', + session_archived_worktree: 'Đã lưu trữ phiên. Worktree vẫn còn trên đĩa.', + session_hidden: 'Đã ẩn phiên khỏi danh sách', + session_restored: 'Đã khôi phục phiên', + session_archive_failed: 'Lưu trữ thất bại: ', + session_duplicate: 'Nhân bản cuộc trò chuyện', + session_duplicate_desc: 'Tạo bản sao với cùng workspace và model', + session_duplicated: 'Đã nhân bản phiên', + session_duplicate_failed: 'Nhân bản thất bại: ', + session_stop_response: 'Dừng phản hồi', + session_stop_response_desc: 'Hủy phản hồi đang chạy cho cuộc trò chuyện này', + session_delete: 'Xóa cuộc trò chuyện', + session_delete_desc: 'Xóa vĩnh viễn cuộc trò chuyện này', + session_delete_confirm: 'Xóa cuộc trò chuyện này?', + session_delete_worktree_desc: 'Chỉ xóa cuộc trò chuyện WebUI; giữ worktree trên đĩa', + session_delete_worktree_confirm: (path) => `Xóa cuộc trò chuyện này? Worktree tại ${path} sẽ vẫn còn trên đĩa.`, + session_deleted: 'Đã xóa cuộc trò chuyện', + session_deleted_worktree: 'Đã xóa cuộc trò chuyện. Worktree vẫn còn trên đĩa.', + + settings_label_session_endless_scroll: 'Tải tin nhắn cũ khi cuộn lên', + + settings_desc_session_endless_scroll: 'Khi bật, các tin nhắn cũ sẽ tự động tải khi bạn cuộn lên trên. Khi tắt, dùng nút tải tin nhắn cũ.', + settings_label_auto_scroll_follow: 'Tự động cuộn theo nội dung mới', + settings_desc_auto_scroll_follow: 'Khi bật, khung nhìn sẽ tự cuộn xuống cuối khi token mới được stream vào. Khi tắt, bạn sẽ tự điều khiển vị trí cuộn.', + settings_label_render_user_markdown: 'Render Markdown trong tin nhắn người dùng', + settings_label_show_titlebar_profile: 'Hiển thị bộ chuyển profile trên thanh tiêu đề', + settings_desc_show_titlebar_profile: 'Khi bật, nút chuyển profile sẽ xuất hiện ở góc trên bên trái thanh tiêu đề ứng dụng để bạn đổi profile từ bất kỳ tab nào. Mặc định tắt; chân khung soạn thảo luôn có bộ chuyển profile dù cài đặt này thế nào.', + settings_desc_render_user_markdown: 'Khi bật, chữ đậm, chữ nghiêng, liên kết và các Markdown khác trong tin nhắn của bạn sẽ được render. Mặc định tắt; khối code fenced và công thức toán luôn được render bất kể cài đặt này.', + settings_label_structured_code: 'Khối code JSON/YAML', + settings_option_structured_code_auto: 'Tự động: dạng cây cho khối dài', + settings_option_structured_code_on: 'Mặc định dạng cây', + settings_option_structured_code_off: 'Mặc định dạng thô', + settings_label_structured_code_auto_lines: 'Ngưỡng số dòng tự động', + settings_desc_structured_code: 'Khi chọn Tự động, các khối JSON/YAML có ít nhất số dòng này sẽ mở ở dạng cây. Dạng cây luôn mở trình xem có cấu trúc; Dạng thô giữ văn bản được tô sáng cú pháp. Bạn vẫn có thể chuyển Dạng cây/Dạng thô trên từng khối.', + settings_label_worklog_details_expanded_default: 'Tự động mở chi tiết Worklog', + settings_desc_worklog_details_expanded_default: 'Khi bật, chi tiết Worklog mới sẽ được mở sẵn để hiển thị tool, suy nghĩ và thẻ tiến trình mà không cần bấm thêm. Khi tắt, Worklog sẽ mặc định thu gọn; các lựa chọn mở/thu gọn thủ công theo từng lượt vẫn được ưu tiên.', + settings_label_chat_activity_display_mode: 'Hiển thị hoạt động', + 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', + processed_elapsed: _i18nProcessedElapsedVi, + worklog_thinking: 'Đang suy nghĩ', + tool_target_skill_suffix: 'kỹ năng', + tool_action_label: _i18nToolActionLabelVi, + tool_worklog_summary: _i18nToolWorklogSummaryVi, + tool_summary_join: _i18nToolSummaryJoinVi, + done: 'Hoàn tất', + 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ị.', + settings_label_composer_controls: 'Điều khiển chân khung chat', + settings_desc_composer_controls: 'Bật/tắt các điều khiển hiển thị ở chân khung chat. Không hỗ trợ đổi thứ tự.', + settings_label_composer_situational_controls: 'Điều khiển chân khung theo ngữ cảnh', + settings_desc_composer_situational_controls: 'Các điều khiển này không phải lúc nào cũng xuất hiện, tùy theo kích thước màn hình, chế độ hoặc trạng thái chạy. Bạn vẫn có thể tắt chúng nếu cần.', + composer_control_attach: 'Đính kèm', + composer_control_saved_prompts: 'Prompt đã lưu', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Lập luận', + composer_control_context: 'Ngữ cảnh', + composer_control_voice_mode: 'Chế độ giọng nói', + composer_control_toolsets: 'Toolset', + composer_control_status: 'Trạng thái', + composer_control_yolo: 'YOLO', + composer_control_quota_chip: 'Chip hạn mức', + composer_control_bg_badge: 'Huy hiệu nền', + composer_control_mobile_config: 'Cấu hình di động', + open_in_browser: 'Mở trong trình duyệt', + settings_dropdown_conversation: 'Cuộc trò chuyện', + settings_dropdown_appearance: 'Giao diện', + settings_dropdown_preferences: 'Tùy chọn', + settings_dropdown_providers: 'Provider', + settings_dropdown_system: 'Hệ thống', + settings_tab_conversation: 'Cuộc trò chuyện', + settings_tab_appearance: 'Giao diện', + settings_tab_preferences: 'Tùy chọn', + settings_tab_plugins: 'Plugin', + settings_tab_extensions: 'Tiện ích mở rộng', + settings_plugins_title: 'Plugin', + plugins_enable_toggle: 'Bật', + settings_plugins_meta: 'Xem các plugin Hermes đã cài đặt và lifecycle hook mà chúng đăng ký. Bảng này chỉ đọc.', + settings_plugins_empty: 'Hiện không có plugin Hermes nào hiển thị. Hãy cài đặt hoặc bật plugin từ Hermes CLI/config để thấy chúng ở đây.', + plugins_unnamed: 'Plugin chưa đặt tên', + plugins_no_description: 'Không có mô tả.', + plugins_no_hooks: 'Không có lifecycle hook nào được đăng ký', + plugins_registered_hooks: 'Lifecycle hook đã đăng ký', + plugins_enabled: 'Đã bật', + plugins_disabled: 'Đã tắt', + plugins_active_provider: 'Đang hoạt động (provider)', + plugins_provider_no_hooks: 'Plugin provider — không có hook hiển thị agent', + plugins_load_failed: 'Tải plugin thất bại: ', + settings_tab_system: 'Hệ thống', + settings_tab_help: 'Trợ giúp', + settings_help_meta: 'Tài nguyên và hỗ trợ cho Hermes WebUI.', + settings_help_docs_label: 'Tài liệu', + settings_help_docs_desc: 'Hướng dẫn, tham chiếu cấu hình và README đầy đủ của Hermes WebUI.', + 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ở trang GitHub Issues', + settings_title: 'Cài đặt', + settings_save_btn: 'Lưu cài đặt', + settings_label_model: 'Model mặc định', + settings_desc_model: 'Được dùng cho các cuộc trò chuyện mới. Các cuộc trò chuyện hiện có vẫn giữ model đã chọn.', + settings_label_auxiliary_models: 'Model phụ trợ', + settings_desc_auxiliary_models: 'Điều hướng tác vụ phụ cho vision, nén, tạo tiêu đề, v.v. "Auto" sẽ dùng model chat chính.', + settings_btn_reset_aux_models: 'Đặt lại tất cả về auto', + settings_btn_apply_aux_models: 'Áp dụng thay đổi', + settings_aux_provider_auto: 'dùng model chính', + settings_aux_model_auto: 'auto (dùng mặc định của provider)', + settings_aux_model_custom: 'Model tùy chỉnh…', + settings_aux_model_custom_prompt: 'Nhập ID model:', + settings_aux_loading: 'Đang tải model phụ trợ…', + settings_aux_load_failed: 'Không thể tải cài đặt model phụ trợ. Hãy chắc chắn API agent khả dụng.', + settings_aux_reset_confirm_title: 'Đặt lại model phụ trợ?', + settings_aux_reset_confirm_msg: 'Thao tác này sẽ đặt tất cả tác vụ phụ trợ về auto (dùng model chính).', + settings_aux_reset_done: 'Đã đặt lại model phụ trợ về auto', + settings_aux_save_failed: 'Lưu model phụ trợ thất bại', + settings_aux_saved: 'Đã cập nhật model phụ trợ', + settings_aux_no_changes: 'Không có thay đổi để áp dụng', + settings_label_send_key: 'Phím gửi', + settings_label_theme: 'Theme', + settings_label_skin: 'Skin', + settings_label_font_size: 'Cỡ chữ', + font_size_small: 'Nhỏ', + font_size_default: 'Mặc định', + font_size_large: 'Lớn', + font_size_xlarge: 'Rất lớn', + settings_autosave_saving: 'Đang lưu…', + settings_autosave_saved: 'Đã lưu', + 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 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 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', + cmd_reasoning: 'Bật/tắt hiển thị suy nghĩ (show/hide), đặt mức effort hoặc kiểm tra trạng thái hiện tại', + settings_label_external_sessions: 'Hiện phiên không phải WebUI', + settings_label_cron_sessions: 'Hiện phiên cron', + settings_label_previous_messaging_sessions: 'Hiện các phiên nhắn tin trước đó', + settings_label_sync_insights: 'Đồng bộ sang insights', + settings_label_check_updates: 'Kiểm tra cập nhật', + settings_label_ignore_agent_updates: 'Bỏ qua cập nhật Agent', + settings_label_whats_new_summary: "Tóm tắt What's New bằng AI", + settings_label_bot_name: 'Tên assistant mặc định', + 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: '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_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 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'}`, + gateway_load_failed: 'Tải trạng thái gateway thất bại', + settings_saved: 'Đã lưu cài đặt', + settings_save_failed: 'Lưu thất bại: ', + settings_load_failed: 'Tải cài đặt thất bại: ', + settings_saved_pw: 'Đã lưu cài đặt — bảo vệ bằng mật khẩu đã bật và trình duyệt này vẫn đăng nhập', + settings_saved_pw_updated: 'Đã lưu cài đặt — mật khẩu đã được cập nhật', + // login page (used server-side via /api/i18n/login endpoint) + login_title: 'Đăng nhập', + login_subtitle: 'Nhập mật khẩu để tiếp tục', + login_placeholder: 'Mật khẩu', + login_btn: 'Đăng nhập', + login_invalid_pw: 'Mật khẩu không hợp lệ', + login_conn_failed: 'Kết nối thất bại', + dialog_confirm_title: 'Xác nhận thao tác', + dialog_prompt_title: 'Nhập giá trị', + dialog_confirm_btn: 'Xác nhận', + // Sidebar & Tabs + tab_chat: 'Chat', + tab_tasks: 'Tác vụ', + tab_skills: 'Kỹ năng', + tab_memory: 'Bộ nhớ', + tab_workspaces: 'Spaces', + tab_profiles: 'Hồ sơ', + tab_kanban: 'Kanban', + kanban_board: 'Bảng', + kanban_visible_tasks: '{0} tác vụ hiển thị', + 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 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.', + kanban_unavailable: 'Kanban không khả dụng', + kanban_read_only: 'Chế độ chỉ đọc', + kanban_empty: 'Trống', + kanban_task: 'Tác vụ', + kanban_no_description: 'Không có mô tả', + kanban_refresh: 'Làm mới', + kanban_status_triage: 'Phân loại', + kanban_status_todo: 'Todo', + kanban_status_ready: 'Sẵn sàng', + kanban_status_running: 'Đang chạy', + kanban_status_blocked: 'Bị chặn', + kanban_status_done: 'Hoàn tất', + kanban_comments_count: 'Bình luận ({0})', + kanban_events_count: 'Sự kiện ({0})', + kanban_links: 'Liên kết', + kanban_parents: 'Cha', + kanban_children: 'Con', + kanban_runs_count: 'Lần chạy ({0})', + kanban_no_comments: 'Không có bình luận', + kanban_no_events: 'Không có sự kiện', + kanban_no_runs: 'Không có lần chạy', + kanban_title: 'Tiêu đề', + kanban_description: 'Mô tả', + kanban_description_placeholder: 'Tùy chọn — điều gì cần thực hiện, tiêu chí chấp nhận, liên kết', + kanban_status: 'Trạng thái', + kanban_assignee: 'Người phụ trách', + kanban_assignee_placeholder: 'Tùy chọn — để trống cho bất kỳ worker nào', + kanban_tenant: 'Tenant', + kanban_tenant_placeholder: 'Tùy chọn — slug project hoặc team', + kanban_priority: 'Độ ưu tiên', + kanban_priority_hint: 'Số cao hơn sẽ chạy trước. Mặc định là 0.', + kanban_title_required: 'Tiêu đề là bắt buộc.', + kanban_new_task: 'Tác vụ mới', + kanban_view_lanes: 'Lane theo hồ sơ', + kanban_view_consolidated: 'Chế độ xem tổng hợp', + kanban_view_lanes_saved: 'Chế độ Kanban: lane theo hồ sơ', + kanban_view_consolidated_saved: 'Chế độ Kanban: tổng hợp', + kanban_view_update_failed: 'Cập nhật chế độ Kanban thất bạ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 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: '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.', + kanban_dispatch_preview_prefix: 'Xem trước:', + kanban_dispatch_run_prefix: 'Đã dispatch:', + kanban_dispatch_spawned: 'đã tạo', + kanban_dispatch_promoted: 'đã nâng cấp', + kanban_dispatch_reclaimed: 'đã nhận lại', + kanban_dispatch_skipped_unassigned: 'đã bỏ qua (chưa gán)', + kanban_dispatch_skipped_nonspawnable: 'đã bỏ qua (profile không xác định)', + kanban_dispatch_auto_blocked: 'tự động chặn', + kanban_dispatch_timed_out: 'hết thời gian', + kanban_dispatch_crashed: 'bị crash', + kanban_add_comment: 'Thêm bình luận', + kanban_workspace_kind: 'Loại workspace', + kanban_workspace_path: 'Đường dẫn workspace', + kanban_workspace_path_placeholder: 'Đường dẫn tới workspace', + kanban_workspace_scratch: 'Tạm thời', + kanban_workspace_worktree: 'Worktree', + kanban_workspace_dir: 'Thư mục', + kanban_workspace_path_required: 'Workspace không phải tạm thời cần có đường dẫn.', + kanban_add_dependency: 'Thêm phụ thuộc', + kanban_remove_dependency: 'Xóa phụ thuộc', + kanban_dependency_placeholder: 'ID tác vụ cần liên kết', + kanban_dependency_self: 'Một tác vụ không thể phụ thuộc vào chính 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 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: '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…', + kanban_archive_board_confirm: 'Lưu trữ bảng "{name}"? Các tác vụ vẫn còn trên đĩa và bảng có thể được khôi phục từ kanban/boards/_archived/.', + kanban_board_archived: 'Đã lưu trữ bảng', + 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: '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', + kanban_card_complete: 'hoàn tất', + kanban_card_archive: 'lưu trữ', + kanban_unassigned: 'chưa gán', + kanban_status_archived: 'Đã lưu trữ', + tab_todos: 'Todos', + tab_insights: 'Insights', + 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', + close_menu: 'Đóng menu', + new_conversation: 'Cuộc trò chuyện mới', + filter_conversations: 'Lọc cuộc trò chuyện...', + markdown_table_filter: 'Lọc bảng', + markdown_table_sort_column: 'Sắp xếp cột', + session_time_unknown: 'Không xác định', + session_time_minutes_ago: (n) => `${n}p`, + session_time_hours_ago: (n) => `${n}g`, + session_time_days_ago: (n) => `${n}n`, + session_time_last_week: '1t', + session_time_bucket_today: 'Hôm nay', + session_time_bucket_yesterday: 'Hôm qua', + session_time_bucket_this_week: 'Tuần này', + session_time_bucket_last_week: 'Tuần trước', + session_time_bucket_older: 'Cũ hơn', + scheduled_jobs: 'Tác vụ đã lên lịch', + new_job: 'Tác vụ mới', + loading: 'Đang tải...', + search_skills: 'Tìm kỹ năng...', + new_skill: 'Kỹ năng mới', + personal_memory: 'Bộ nhớ cá nhân', + current_task_list: 'Danh sách tác vụ hiện tại', + // Logs + logs_title: 'Logs', + logs_file: 'File', + logs_tail: 'Tail', + logs_auto_refresh: 'Tự động làm mới (5s)', + logs_wrap: 'Xuống dòng', + logs_copy_all: 'Sao chép tất cả', + logs_empty: 'Chưa có dòng log nào.', + logs_loading: 'Đang tải log…', + logs_load_failed: 'Tải log thất bại', + logs_status_idle: 'Chọn file log để xem các dòng gần đây.', + logs_no_mtime: 'chưa được ghi', + logs_truncated_hint: 'Đang hiển thị phần cuối của file log lớn; các byte cũ hơn đã bị bỏ qua để giới hạn bộ nhớ.', + logs_copied: 'Đã sao chép log', + logs_severity: 'Mức độ', + logs_severity_all: 'Tất cả', + logs_severity_errors: 'Lỗi', + logs_severity_warnings: 'Cảnh báo+', + logs_filter_active: 'đang hiển thị (bộ lọc đang hoạt động)', + + // Insights + insights_title: 'Phân tích sử dụng', + insights_sessions: 'Phiên', + insights_messages: 'Tin nhắn', + insights_tokens: 'Token', + insights_cost: 'Chi phí ước tính', + insights_no_cost: 'Không có', + insights_models: 'Model', + insights_activity_by_day: 'Hoạt động theo ngày', + insights_activity_by_hour: 'Hoạt động theo giờ', + insights_peak_hour: 'Cao điểm: {hour}', + insights_token_breakdown: 'Phân tích token', + insights_input_tokens: 'Đầu vào', + insights_output_tokens: 'Đầu ra', + insights_total: 'Tổng', + insights_daily_tokens: 'Token hàng ngày', + insights_model_name: 'Model', + insights_model_sessions: 'Phiên', + insights_model_tokens: 'Token', + insights_model_cost: 'Chi phí', + insights_model_share: 'Tỷ lệ', + insights_no_usage_data: 'Chưa có dữ liệu sử dụng', + insights_footer: 'Hiển thị dữ liệu trong {days} ngày gần nhất', + insights_skill_usage_title: 'Sử dụng kỹ năng', + insights_skill_usage_sub: 'Tần suất gọi công cụ', + insights_skill_usage_total: 'Tổng số lần gọi', + insights_skill_usage_skills_used: 'Kỹ năng đã dùng', + insights_skill_usage_no_data: 'Chưa có dữ liệu sử dụng kỹ năng', + insights_skill_usage_no_data_hint: 'Kỹ năng sẽ xuất hiện ở đây sau khi được dùng trong cuộc trò chuyện.', + insights_skill_usage_footer: 'Số lượng từ ~/.hermes/skills/', + insights_skill_usage_col_skill: 'Kỹ năng', + insights_skill_usage_col_uses: 'Lần dùng', + insights_skill_usage_col_views: 'Lượt xem', + insights_skill_usage_col_share: 'Tỷ lệ sử dụng %', + insights_skill_usage_col_patches: 'Patch', + workspace_desc: 'Thêm và chuyển đổi workspace cho các phiên.', + session_meta_messages: (n) => `${n} tin nhắn`, + session_meta_children: (n) => `${n} phiên con`, + // Softened label: avoids exposing the technical internal term + // 'segment' in the default visible badge. User-facing copy remains + // translatable for locales that prefer a different wording. (#2155) + session_meta_segments: (n) => `${n} lượt trước`, + session_lineage_toggle_hint: '{0} — các lượt ngữ cảnh trước đó đang được thu gọn ở đây. Bấm để hiện hoặc ẩn chúng.', + session_lineage_static_hint: '{0} — các lượt ngữ cảnh trước đó đang được thu gọn ở đây.', + 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ở đ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: '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', + settings_desc_rtl: 'Đảo hướng căn chỉnh tin nhắn chat và ô nhập cho các ngôn ngữ như Arabic hoặc Hebrew. Chỉ ảnh hưởng vùng chat — sidebar và các panel khác vẫn giữ trái sang phải.', + settings_label_sound: 'Âm thanh thông báo', + settings_desc_sound: 'Phát âm thanh khi assistant hoàn tất phản hồi.', + // TTS (#499) + tts_listen: 'Nghe', + tts_not_supported: 'Trình duyệt này không hỗ trợ tổng hợp giọng nói.', + settings_label_tts: 'Chuyển văn bản thành giọng nói cho phản hồi', + settings_desc_tts: "Hiển thị nút loa trên mỗi tin nhắn assistant để đọc to bằng chức năng speech synthesis của trình duyệt.", + settings_label_tts_auto_read: 'Tự động đọc phản hồi', + settings_desc_tts_auto_read: 'Tự động đọc to mỗi phản hồi mới của assistant khi hoàn tất. Sẽ tạm dừng khi bạn bắt đầu nhập.', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Nút chế độ giọng nói rảnh tay', + settings_desc_voice_mode: 'Hiển thị nút voice-mode (dạng sóng âm thanh) cạnh microphone đọc chính tả. Cho phép bạn nói tự nhiên — Hermes tự gửi sau khoảng dừng và đọc phản hồi thành tiếng. Yêu cầu trình duyệt hỗ trợ cả nhận diện giọng nói và TTS.', + settings_label_raw_audio: 'Gửi âm thanh gốc thay vì chuyển thành văn bản', + settings_desc_raw_audio: 'Ghi và gửi file âm thanh gốc tới agent thay vì chuyển thành text trước. Agent sau đó có thể tự chuyển lời nói thành văn bản hoặc xử lý âm thanh gốc (cảm xúc, tiếng nền, STT tùy chỉnh). Tương tự hành vi voice message của Telegram.', + voice_send_raw: 'Gửi âm thanh gốc', + voice_raw_attached: 'Đã đính kèm âm thanh. Nhấn Gửi hoặc nhập thêm.', + settings_label_tts_voice: 'Giọng nói', + settings_desc_tts_voice: "Giọng nói ưu tiên. Lấy từ các giọng có sẵn trong trình duyệt.", + settings_label_tts_rate: 'Tốc độ nói', + settings_label_tts_pitch: 'Cao độ giọng nói', + settings_label_tts_engine: 'Engine TTS', + settings_desc_tts_engine: 'Chọn engine giọng nói. Edge TTS dùng giọng neural của Microsoft thông qua server.', + settings_label_notifications: 'Thông báo trình duyệt', + settings_desc_notifications: 'Hiển thị thông báo hệ thống khi phản hồi hoàn tất trong lúc ứng dụng đang chạy nền.', + notifications_enable_btn: 'Bật thông báo', + notifications_test_btn: 'Gửi thử', + notifications_permission_status: (status) => `Quyền: ${status}`, + notifications_enabled_toast: 'Đã bật thông báo.', + notifications_denied: 'Thông báo đang bị chặn trong trình duyệt này. Hãy bật trong cài đặt trình duyệt.', + notifications_unsupported: 'Trình duyệt này không hỗ trợ thông báo.', + settings_desc_token_usage: 'Hiển thị số lượng token đầu vào/đầu ra dưới mỗi phản hồi của assistant. Cũng có thể bật/tắt bằng /usage.', + settings_label_api_redact: 'Ẩn dữ liệu nhạy cảm trong phản hồi API', + settings_desc_api_redact: 'Người dùng self-hosted có thể tắt để minh bạch hơn (không khuyến nghị cho instance dùng chung).', + settings_sidebar_density_compact: 'Gọn', + settings_sidebar_density_detailed: 'Chi tiết', + settings_desc_sidebar_density: 'Điều khiển lượng metadata hiển thị trong danh sách phiên ở sidebar trái.', + settings_label_auto_title_refresh: 'Tự động làm mới tiêu đề', + settings_auto_title_refresh_off: 'Tắt', + settings_auto_title_refresh_5: 'Mỗi 5 lượt trao đổi', + settings_auto_title_refresh_10: 'Mỗi 10 lượt trao đổi', + settings_auto_title_refresh_20: 'Mỗi 20 lượt trao đổi', + settings_desc_auto_title_refresh: 'Tự động tạo lại tiêu đề phiên dựa trên lượt trao đổi mới nhất, giúp tiêu đề luôn phù hợp khi cuộc trò chuyện phát triển. Yêu cầu có model tạo tiêu đề LLM được cấu hình.', + settings_desc_external_sessions: 'Hiển thị các cuộc trò chuyện từ CLI, Telegram, Discord, Slack và các kênh khác trong danh sách phiên. Bấm để import và tiếp tục.', + settings_desc_cron_sessions: 'Hiển thị output của cron job như các cuộc trò chuyện trong sidebar. Chỉ hoạt động khi bật phiên không phải WebUI. Mặc định tắt; job tần suất cao có thể làm sidebar bị ngập.', + settings_desc_previous_messaging_sessions: 'Hiển thị các phiên Discord, Telegram, Slack và Weixin cũ đã bị thay thế bởi reset hoặc nén.', + settings_desc_sync_insights: 'Đồng bộ token usage của WebUI sang state.db để hermes /insights bao gồm dữ liệu phiên trình duyệt. Mặc định tắt.', + settings_desc_check_updates: 'Hiển thị banner khi có phiên bản mới của WebUI hoặc Agent. Định kỳ chạy git fetch nền.', + settings_desc_ignore_agent_updates: 'Giữ kiểm tra cập nhật WebUI nhưng ẩn thông báo cập nhật Agent và bỏ qua fetch cập nhật Agent.', + settings_desc_whats_new_summary: "Thay đổi hành động What's New từ mở diff thô sang tạo tóm tắt ngắn dễ đọc bằng AI. So sánh diff thông thường vẫn khả dụng sau phần tóm tắt.", + settings_desc_bot_name: 'Chỉ dùng cho profile mặc định. Các profile khác dùng tên profile riêng.', + settings_desc_password: 'Nhập mật khẩu mới để đặt hoặc thay đổi. Để trống để giữ cài đặt hiện tại.', + password_placeholder: 'Nhập mật khẩu mới…', + password_env_var_locked: 'Biến môi trường HERMES_WEBUI_PASSWORD hiện đang được đặt và có ưu tiên cao hơn. Hãy unset biến này và khởi động lại server để quản lý mật khẩu tại đây.', + 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 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 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: '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', + providers_status_api_key: 'API key', + providers_status_not_configured_label: 'Chưa cấu hình', + providers_oauth_hint: 'Đã xác thực bằng OAuth. Không cần API key.', + providers_oauth_config_yaml_hint: 'Token được cấu hình qua config.yaml. Để cập nhật, chỉnh sửa phần providers trong config.yaml hoặc chạy hermes auth.', + providers_oauth_not_configured_hint: 'Chưa xác thực. Chạy hermes auth trong terminal để cấu hình provider này.', + providers_save: 'Lưu', + providers_refresh_models: 'Làm mới model', + providers_refreshing: 'Đang làm mới…', + providers_models_refreshed: 'Đã làm mới model', + providers_remove: 'Xóa', + providers_saving: 'Đang lưu…', + providers_removing: 'Đang xóa…', + 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-...', + providers_key_placeholder_replace: 'Nhập key mới để thay thế…', + provider_quota_title: 'Quota provider đang hoạt động', + provider_quota_active_provider: 'Provider đang hoạt động', + provider_quota_last_checked_after_refresh: 'Kiểm tra lần cuối sau khi làm mới', + provider_quota_last_checked: 'Kiểm tra lần cuối {0}', + provider_quota_refresh_usage: 'Làm mới usage', + provider_quota_refreshing: 'Đang làm mới...', + provider_quota_refresh_title: 'Làm mới giới hạn sử dụng provider ngay bây giờ', + provider_quota_refresh_succeeded: 'Đã làm mới usage provider', + provider_quota_refresh_failed: 'Làm mới usage provider thất bại', + provider_quota_session_limit: 'Giới hạn 5 giờ', + provider_quota_weekly_limit: 'Giới hạn hàng tuần', + provider_quota_window_fallback: 'Khoảng thời gian', + provider_quota_metric_remaining: 'Còn lại', + provider_quota_metric_used: 'Đã dùng', + provider_quota_metric_limit: 'Giới hạn', + provider_quota_status_available: 'khả dụng', + provider_quota_status_exhausted: 'đã hết', + provider_quota_status_unavailable: 'không khả dụng', + provider_quota_status_failed: 'thất bại', + provider_quota_status_checked: 'đã kiểm tra', + provider_quota_status_no_key: 'không có key', + provider_quota_status_invalid_key: 'key không hợp lệ', + 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: '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.', + provider_quota_account_limits_loaded: 'Đã tải giới hạn tài khoản.', + provider_quota_unavailable: 'Trạng thái quota không khả dụng', + provider_quota_retry_after: 'Thử lại sau {0}', + provider_quota_pool_summary_available: '{0}/{1} khả dụng', + provider_quota_pool_summary_exhausted: '{0} đã hết', + provider_quota_pool_summary_failed: '{0} thất bại', + provider_quota_pool_summary_checked: '{0} đã kiểm tra', + cancel: 'Hủy', + create_job: 'Tạo job', + save_skill: 'Lưu kỹ năng', + editing: 'Đang chỉnh sửa', + // Empty state + empty_title: 'Tôi có thể giúp gì cho bạn?', + empty_subtitle: 'Hỏi bất cứ điều gì, chạy lệnh, khám phá file hoặc quản lý các tác vụ đã lên lịch.', + suggest_files: 'Workspace này có những file gì?', + suggest_schedule: "Hôm nay tôi có lịch gì?", + suggest_plan: 'Giúp tôi lập kế hoạch cho một dự án nhỏ.', + // onboarding + onboarding_badge: 'LẦN CHẠY ĐẦU', + onboarding_title: 'Chào mừng đến với Hermes Web UI', + onboarding_lead: 'Thiết lập nhanh có hướng dẫn sẽ kiểm tra Hermes, lưu cấu hình provider thực, chọn workspace và model, đồng thời có thể bảo vệ ứng dụng bằng mật khẩu.', + onboarding_back: 'Quay lại', + onboarding_continue: 'Tiếp tục', + onboarding_skip: 'Bỏ qua thiết lập', + onboarding_skipped: 'Đã bỏ qua thiết lập — dùng cấu hình hiện có.', + 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 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: '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ẻ.', + onboarding_step_finish_title: 'Hoàn tất', + onboarding_step_finish_desc: 'Xem lại và vào ứng dụng.', + onboarding_notice_system_ready: 'Hermes Agent có vẻ truy cập được từ Web UI.', + onboarding_notice_system_unavailable: 'Hermes Agent chưa hoàn toàn khả dụng. Bootstrap có thể cài đặt nhưng thiết lập provider vẫn có thể cần terminal.', + onboarding_check_agent: 'Hermes Agent', + onboarding_check_agent_ready: 'Đã phát hiện và có thể import', + onboarding_check_agent_missing: 'Thiếu hoặc chỉ import được một phần', + 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 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', + onboarding_config_file: 'File config:', + 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: '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', + onboarding_oauth_provider_ready_body: 'Instance này được cấu hình dùng provider OAuth ({provider}) đã được thiết lập qua Hermes CLI. Không cần API key tại đây — nhấn Tiếp tục để hoàn tất thiết lập.', + onboarding_oauth_provider_not_ready_title: 'Provider OAuth chưa được xác thực', + onboarding_oauth_provider_not_ready_body: 'Instance này được cấu hình dùng {provider}, sử dụng OAuth thay vì API key. Hãy chạy hermes auth hoặc hermes model trong terminal để xác thực rồi tải lại Web UI.', + onboarding_oauth_switch_hint: 'Hoặc chọn provider khác bên dưới để chuyển sang thiết lập bằng API key:', + oauth_login_codex: 'Đăng nhập bằng Codex (ChatGPT)', + oauth_codex_step1: 'Bước 1: Truy cập URL này và nhập mã', + oauth_codex_step2: 'Bước 2: Nhập mã này trên trang', + oauth_codex_polling: 'Đang chờ xác thực...', + oauth_codex_success: 'Đăng nhập OAuth Codex thành công!', + oauth_codex_error: 'Đăng nhập OAuth thất bại', + oauth_codex_expired: 'Mã đã hết hạn, vui lòng thử lại', + onboarding_notice_workspace: 'Các giá trị này dùng lại cùng API cài đặt như ứng dụng bình thường.', + onboarding_workspace_label: 'Workspace', + onboarding_workspace_or_path: 'Hoặc nhập đường dẫn workspace', + onboarding_workspace_placeholder: '/home/you/workspace', + onboarding_provider_label: 'Chế độ thiết lập', + onboarding_quick_setup_badge: 'thiết lập nhanh', + provider_category_easy_start: 'Khởi đầu dễ dàng', + provider_category_self_hosted: 'Mở / self-hosted', + provider_category_specialized: 'Chuyên biệt', + onboarding_api_key_label: 'API key', + onboarding_api_key_placeholder: 'Để trống để giữ key đã lưu hiện có', + onboarding_api_key_label_optional: 'API key (tùy chọn)', + onboarding_api_key_placeholder_optional: 'Để trống cho server không cần key', + onboarding_api_key_help_keyless: 'Hầu hết cài đặt LM Studio / Ollama / vLLM chạy không cần key — hãy để trống nếu server của bạn không yêu cầu xác thực. Dùng nút Test connection để xác minh.', + onboarding_api_key_help_prefix: 'Được lưu dưới dạng secret trong file .env của Hermes bằng', + onboarding_base_url_label: 'Base URL', + onboarding_base_url_placeholder: 'https://your-endpoint.example/v1', + onboarding_base_url_help: 'Dùng cho router tương thích OpenAI, server self-hosted, LiteLLM, Ollama, LM Studio, vLLM hoặc endpoint tương tự.', + onboarding_model_label: 'Model mặc định', + onboarding_workspace_help: 'Chọn model Hermes sẽ dùng cho các chat mới sau khi hoàn tất thiết lập.', + onboarding_custom_model_placeholder: 'ten-model-cua-ban', + onboarding_custom_model_help: 'Với endpoint tùy chỉnh, nhập đúng model ID mà server yêu cầu.', + onboarding_notice_password_enabled: 'Mật khẩu đã được cấu hình. Chỉ nhập mật khẩu mới nếu bạn muốn thay thế.', + onboarding_notice_password_recommended: 'Tùy chọn nhưng được khuyến nghị nếu bạn sẽ mở UI ngoài localhost.', + onboarding_password_label: 'Mật khẩu (tùy chọn)', + onboarding_password_placeholder: 'Để trống để bỏ qua', + onboarding_password_help: 'Mật khẩu được lưu qua API settings hiện có và hash ở phía server.', + onboarding_notice_finish: 'Bạn có thể mở lại Settings sau để thay đổi bất kỳ mục nào.', + onboarding_not_set: 'Chưa đặt', + onboarding_password_will_enable: 'Sẽ được bật', + onboarding_password_will_replace: 'Sẽ được thay thế', + onboarding_password_keep_existing: 'Giữ mật khẩu hiện tại', + onboarding_password_remains_disabled: 'Sẽ vẫn tắt', + onboarding_password_skipped: 'Tạm bỏ qua', + onboarding_finish_help: 'Hoàn tất sẽ lưu onboarding_completed vào settings và đưa bạn vào ứng dụng bình thường.', + onboarding_error_choose_workspace: 'Chọn workspace trước khi tiếp tục.', + onboarding_error_choose_model: 'Chọn model trước khi tiếp tục.', + onboarding_error_provider_required: 'Chọn chế độ thiết lập trước khi tiếp tục.', + onboarding_error_base_url_required: 'Base URL là bắt buộc cho endpoint tùy chỉnh.', + onboarding_probe_test_button: 'Kiểm tra kết nối', + onboarding_probe_probing: 'Đang kiểm tra kết nối…', + onboarding_probe_ok: 'Đã kết nối. Có {n} model khả dụng.', + onboarding_probe_error_generic: 'Không thể truy cập Base URL đã cấu hình.', + onboarding_probe_error_invalid_url: 'Base URL phải bắt đầu bằng http:// hoặc https://.', + onboarding_probe_error_dns: 'Không thể phân giải hostname. Kiểm tra URL hoặc dùng địa chỉ IP của host.', + onboarding_probe_error_connect_refused: 'Kết nối bị từ chối — server có thể chưa chạy trên địa chỉ đó. Nếu chạy trong Docker, hãy thử IP host thay vì localhost.', + onboarding_probe_error_timeout: 'Endpoint không phản hồi kịp thời. Kiểm tra server đang chạy và URL chính xác.', + onboarding_probe_error_http_4xx: 'Endpoint trả về lỗi client. Kiểm tra xác thực và đường dẫn URL (thường kết thúc bằng /v1).', + onboarding_probe_error_http_5xx: 'Endpoint trả về lỗi server. Kiểm tra log LM Studio / Ollama.', + onboarding_probe_error_parse: 'Endpoint không trả về danh sách model đúng định dạng mong đợi. Kiểm tra URL có trỏ đúng tới API root tương thích OpenAI.', + onboarding_probe_error_unreachable: 'Không thể truy cập Base URL đã cấu hình.', + 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 thiết lập ban đầu', + + // panel/runtime i18n + error_prefix: 'Lỗi: ', + default: 'mặc định', + search: 'Tìm kiếm', + not_available: 'Không có', + never: 'không bao giờ', + add: 'Thêm', + add_failed: 'Thêm thất bại: ', + remove_failed: 'Xóa thất bại: ', + switch_failed: 'Chuyển đổi thất bại: ', + name_required: 'Tên là bắt buộc', + content_required: 'Nội dung là bắt buộc', + view: 'Xem', + dismiss: 'Đóng', + disable: 'Tắt', + cron_no_jobs: 'Không tìm thấy job đã lên lịch.', + cron_status_off: 'tắt', + cron_status_paused: 'tạm dừng', + cron_status_error: 'lỗi', + cron_status_active: 'đang hoạt động', + cron_status_running: 'đang chạy\u2026', + cron_status_needs_attention: 'cần chú ý', + cron_attention_desc: 'Job lặp lại này không có thời gian chạy tiếp theo. Scheduler có thể đã không tính được lần chạy kế tiếp.', + cron_attention_croniter_hint: 'Runtime Gateway có thể thiếu package croniter. Hãy khởi động lại Gateway với hỗ trợ cron rồi tiếp tục job này.', + cron_attention_resume: 'Tiếp tục và tính toán lại', + cron_jobs_project: 'Cron Jobs', + cron_attention_run_once: 'Chạy một lần ngay', + cron_attention_copy_diagnostics: 'Sao chép chẩn đoán', + cron_diagnostics_copied: 'Đã sao chép chẩn đoán cron', + cron_next: 'Tiếp theo', + cron_last: 'Lần cuối', + cron_run_now: 'Chạy ngay', + cron_pause: 'Tạm dừng', + cron_resume: 'Tiếp tục', + cron_job_name_placeholder: 'Tên job', + cron_schedule_placeholder: 'Lịch chạy', + cron_prompt_placeholder: 'Prompt', + cron_last_output: 'Output cuối', + cron_expand_prompt: 'Mở rộng prompt', + cron_collapse_prompt: 'Thu gọn prompt', + cron_expand_output: 'Mở rộng output', + cron_collapse_output: 'Thu gọn output', + cron_mode_agent: 'Agent', + cron_mode_script: 'Script', + 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: 'Đầ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: '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ộ đầ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)', + cron_schedule_required_example: 'Lịch chạy là bắt buộc (ví dụ: "0 9 * * *" hoặc "every 1h")', + cron_schedule_required: 'Lịch chạy là bắt buộc', + cron_prompt_required: 'Prompt là bắt buộc', + cron_job_created: 'Đã tạo job', + cron_duplicate: 'Nhân bản', + cron_duplicated: 'Đã nhân bản job (tạm dừng)', + cron_job_triggered: 'Đã kích hoạt job', + cron_job_paused: 'Đã tạm dừng job', + cron_job_resumed: 'Đã tiếp tục job', + cron_job_updated: 'Đã cập nhật job', + cron_model_label: 'Ghi đè model', + cron_model_hint: 'Ghi đè model mặc định cho job này.', + cron_model_no_agent_hint: 'Job no-agent chạy trực tiếp script đã cấu hình; không dùng model.', + cron_model_use_default: 'Mặc định (dùng profile/hệ thống mặc định)', + cron_delete_confirm_title: 'Xóa cron job', + cron_delete_confirm_message: 'Hành động này không thể hoàn tác.', + cron_job_deleted: 'Đã xóa job', + cron_completion_status: (name, status) => `Cron "${name}" ${status}`, + status_failed: 'thất bại', + status_completed: 'hoàn tất', + todos_no_active: 'Không có danh sách tác vụ hoạt động trong phiên này.', + clear_conversation_title: 'Xóa cuộc trò chuyện', + clear_conversation_message: 'Xóa toàn bộ tin nhắn? Hành động này không thể hoàn tác.', + clear_failed: 'Xóa thất bại: ', + skills_no_match: 'Không có kỹ năng phù hợp.', + skill_enabled: 'Đã bật', + skill_disabled: 'Đã tắt', + skill_toggle_failed: 'Bật/tắt kỹ năng thất bại: ', + linked_files: 'File liên kết', + skill_load_failed: 'Không thể tải kỹ năng: ', + skill_file_load_failed: 'Không thể tải file: ', + skill_name_required: 'Tên kỹ năng là bắt buộc', + skill_updated: 'Đã cập nhật kỹ năng', + skill_created: 'Đã tạo kỹ năng', + skill_deleted: 'Đã xóa kỹ năng', + skill_delete_confirm: 'Xóa kỹ năng "{0}"?', + skills_empty_title: 'Chọn một kỹ năng', + skills_empty_sub: 'Chọn một kỹ năng từ sidebar để xem nội dung hoặc tạo kỹ năng mới.', + skills_edit: 'Chỉnh sửa', + skills_delete: 'Xóa', + skills_back_to: 'Quay lại {0}', + tasks_empty_title: 'Chọn một job đã lên lịch', + 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 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ớ', + my_notes: 'Ghi chú của tôi', + user_profile: 'Hồ sơ người dùng', + external_notes_sources: 'Nguồn ghi chú bên thứ ba', + external_notes_empty: 'Chưa có nguồn MCP ghi chú hoặc tri thức nào hiển thị. Hãy cấu hình Joplin, Obsidian, Notion, llm-wiki hoặc server ghi chú khác để liệt kê tại đây.', + external_notes_auto_recall_hint: 'Tự động gọi lại phiên không thay đổi; ngăn này chỉ hiển thị các nguồn đã cấu hình và công cụ đọc/tìm kiếm khả dụng.', + external_notes_no_tools: 'Hiện không có công cụ đọc/tìm kiếm nào hiển thị cho nguồn này.', + external_notes_tool_count: (count) => `${count} công cụ ghi chú khả dụng`, + external_notes_configured_hint: 'Tên công cụ được mong đợi từ nguồn đã cấu hình này; schema trực tiếp sẽ xuất hiện khi runtime WebUI hiển thị chúng.', + external_notes_recent_ai: 'AI dùng gần đây', + external_notes_auto: 'tự động', + external_notes_recent_ai_reason: 'Tự động gọi lại', + external_notes_search_placeholder: 'Tìm ghi chú…', + external_notes_search_empty: 'Tìm kiếm một nguồn ghi chú đã cấu hình để xem trước ghi chú tại đây.', + source_active: 'đang hoạt động', + source_configured: 'đã cấu hình', + no_notes_yet: 'Chưa có ghi chú.', + no_profile_yet: 'Chưa có profile.', + agent_soul: 'Linh hồn Agent', + 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ý 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)', + workspace_paths_validated_hint: 'Đường dẫn được xác minh là thư mục tồn tại trước khi lưu.', + + workspace_drag_hint: 'Kéo để sắp xếp lại', + workspace_reorder_failed: 'Sắp xếp lại thất bại', + workspace_added: 'Đã thêm workspace', + workspace_renamed: 'Đã đổi tên workspace', + 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 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', + workspace_not_added: 'Workspace chưa được thêm', + workspace_already_saved: 'Workspace đã được lưu — hãy chọn từ danh sách', + workspace_busy_switch: 'Không thể chuyển workspace khi agent đang chạy', + discard_file_edits_title: 'Bỏ thay đổi file?', + discard_file_edits_message: 'Chuyển workspace sẽ bỏ các chỉnh sửa file chưa lưu trong phần xem trước.', + 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 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 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 / 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)', + profile_api_key_placeholder: 'API key (tùy chọn)', + manage_profiles: 'Quản lý profile', + profiles_load_failed: 'Tải profile thất bại', + profile_switched_new_conversation: (name) => `Đã chuyển sang profile: ${name} — đã bắt đầu cuộc trò chuyện mới`, + profile_switched: (name) => `Đã chuyển sang profile: ${name}`, + 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 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.', + active_conversation_meta: (title, count) => `${title} · ${count} tin nhắn`, + settings_unsaved_changes: 'Bạn có thay đổi chưa lưu.', + sign_out_failed: 'Đăng xuất thất bại: ', + disable_auth_confirm_title: 'Tắt bảo vệ mật khẩu', + disable_auth_confirm_message: 'Bất kỳ ai cũng có thể truy cập instance này.', + auth_disabled: 'Đã tắt xác thực — đã gỡ bảo vệ mật khẩu', + disable_auth_failed: 'Tắt xác thực thất bại: ', + current_password_label: 'Mật khẩu hiện tại', + current_password_placeholder: 'Nhập mật khẩu hiện tại\u2026', + current_password_required: 'Cần mật khẩu hiện tại để thay đổi hoặc tắt xác thực.', + current_password_incorrect: 'Mật khẩu hiện tại không đúng.', + disable_auth_typed_confirm: 'Nhập DISABLE AUTH để xác nhận', + auth_status_password: 'Xác thực bằng mật khẩu', + auth_status_passkey_only: 'Chỉ dùng passkey', + auth_status_unauthenticated: 'Chưa xác thực', + auth_warning_badge: 'Xác thực đã tắt', + auth_disabled_warning_message: 'Instance này có thể truy cập mà không cần xác thực.', + auth_acknowledged_label: 'Tôi đã xem rủi ro này; hiển thị cảnh báo điều hướng ít khẩn cấp hơn.', + auth_ack_save_failed: 'Cập nhật xác nhận thất bại: ', + bg_error_single: (title) => `"${title}" đã gặp lỗi`, + bg_error_multi: (count) => `${count} phiên đã gặp lỗi`, + // skill form + skill_name: 'Tên', + skill_category: 'Danh mục', + skill_category_placeholder: 'Tùy chọn, ví dụ: devops', + 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 + cron_name_label: 'Tên', + cron_name_placeholder: 'Tùy chọn', + cron_schedule_label: 'Lịch chạy', + cron_schedule_hint: "Dùng 'every 1h' hoặc biểu thức cron cho job lặp lại. Các khoảng thời gian đơn như '30m' chỉ chạy một lần.", + 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: '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', + cron_profile_server_default_hint: 'Dùng profile mặc định của server WebUI tại thời điểm chạy. Các job hiện có không có profile sẽ giữ hành vi cũ này.', + cron_toast_notifications_label: 'Thông báo toast khi hoàn tất', + cron_toast_notifications_hint: 'Hiển thị toast khi cron này hoàn tất. Badge Tasks và dấu lần chạy mới vẫn cập nhật khi tùy chọn này tắt.', + cron_toast_notifications_enabled: 'Đã bật', + cron_toast_notifications_disabled: 'Đã tắt', + cron_skills_label: 'Kỹ năng', + cron_skills_placeholder: 'Thêm kỹ năng (tùy chọn)…', + cron_skills_edit_hint: 'Danh sách kỹ năng không thể chỉnh sửa sau khi tạo.', + // workspace form + workspace_name_label: 'Tên', + workspace_name_placeholder: 'Tên thân thiện tùy chọn', + workspace_path_label: 'Đường dẫn', + workspace_path_required: 'Đường dẫn là bắt buộc', + workspace_path_readonly: 'Không thể thay đổi đường dẫn. Chỉ có thể đổi tên.', + workspace_new_title: 'Space mới', + // profile form + 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_gateway_unsupported_label: 'Không hỗ trợ phê duyệt', + 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', + context_block_rename_hint: 'Bấm hoặc nhấn Enter để đổi tên', + context_block_rename_aria: 'Đổi tên khối ngữ cảnh', + context_block_remove: 'Xóa khối ngữ cảnh', + 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}', + external_link_open_confirm: 'Symlink này trỏ ra ngoài workspace và không thể mở từ đây.', + external_link_read_only: 'Chế độ xem bên ngoài này chỉ đọc. Các thao tác tạo, đổi tên, di chuyển, xóa, tải lên, hiển thị và chỉnh sửa vẫn bị tắt tại đây.', + external_link_grant_expired: 'Chế độ xem bên ngoài này đã hết hạn. Hãy mở lại symlink trỏ ra ngoài để cấp quyền lại.', + 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', +}, + }; const _I18N_TOOL_ACTION_TEXT_EN = { @@ -20144,6 +21718,47 @@ function _i18nToolSummaryJoinCjk(parts) { return `${items.slice(0, -1).join('、')}和${items[items.length - 1]}`; } +const _I18N_TOOL_ACTION_TEXT_VI = { + shell: { running: 'Đang chạy', done: 'Đã chạy', fail: 'chạy', fallback: 'lệnh' }, + read: { running: 'Đang đọc', done: 'Đã đọc', fail: 'đọc', fallback: 'tệp' }, + list: { running: 'Đang liệt kê', done: 'Đã liệt kê', fail: 'liệt kê', fallback: 'tệp' }, + search: { running: 'Đang tìm kiếm', done: 'Đã tìm kiếm', fail: 'tìm kiếm', fallback: 'workspace' }, + web: { running: 'Đang kiểm tra', done: 'Đã kiểm tra', fail: 'kiểm tra', fallback: 'dữ liệu web' }, + write: { running: 'Đang cập nhật', done: 'Đã cập nhật', fail: 'cập nhật', fallback: 'tệp' }, + skill: { running: 'Đang tải', done: 'Đã tải', fail: 'tải', fallback: 'kỹ năng' }, + memory: { running: 'Đang lưu', done: 'Đã lưu', fail: 'lưu', fallback: 'bộ nhớ' }, + delegate: { running: 'Đang ủy quyền', done: 'Đã ủy quyền', fail: 'ủy quyền', fallback: 'tác vụ' }, + unknown: { running: 'Đang chạy', done: 'Đã chạy', fail: 'chạy', fallback: 'công cụ' }, +}; +const _I18N_TOOL_SUMMARY_TEXT_VI = { + shell: { running: ['Đang chạy lệnh', 'Đang chạy {n} lệnh'], done: ['Đã chạy lệnh', 'Đã chạy {n} lệnh'] }, + read: { running: ['Đang đọc tệp', 'Đang đọc {n} tệp'], done: ['Đã đọc tệp', 'Đã đọc {n} tệp'] }, + list: { running: ['Đang liệt kê tệp', 'Đang liệt kê {n} mục'], done: ['Đã liệt kê tệp', 'Đã liệt kê {n} tệp'] }, + search: { running: ['Đang tìm kiếm workspace', 'Đang tìm kiếm workspace {n} lần'], done: ['Đã tìm kiếm workspace', 'Đã tìm kiếm workspace {n} lần'] }, + web: { running: ['Đang kiểm tra web', 'Đang kiểm tra web {n} lần'], done: ['Đã kiểm tra web', 'Đã kiểm tra web {n} lần'] }, + write: { running: ['Đang cập nhật tệp', 'Đang cập nhật {n} tệp'], done: ['Đã cập nhật tệp', 'Đã cập nhật {n} tệp'] }, + skill: { running: ['Đang tải kỹ năng', 'Đang tải {n} kỹ năng'], done: ['Đã tải kỹ năng', 'Đã tải {n} kỹ năng'] }, + memory: { running: ['Đang lưu bộ nhớ', 'Đang lưu {n} cập nhật bộ nhớ'], done: ['Đã lưu bộ nhớ', 'Đã lưu {n} cập nhật bộ nhớ'] }, + delegate: { running: ['Đang ủy quyền tác vụ', 'Đang ủy quyền {n} tác vụ'], done: ['Đã ủy quyền tác vụ', 'Đã ủy quyền {n} tác vụ'] }, + unknown: { running: ['Đang chạy công cụ', 'Đang chạy {n} công cụ'], done: ['Đã chạy công cụ', 'Đã chạy {n} công cụ'] }, +}; +function _i18nProcessedElapsedVi(duration) { + return _i18nProcessedElapsed('Đã xử lý', duration); +} +function _i18nToolActionLabelVi(kind, state, target, display, failed) { + const verbs = _I18N_TOOL_ACTION_TEXT_VI[kind] || _I18N_TOOL_ACTION_TEXT_VI.unknown; + const object = target || verbs.fallback || display || 'công cụ'; + if (failed) return `Không thể ${verbs.fail || 'chạy'} ${object}`; + return `${verbs[state] || verbs.running} ${object}`; +} +function _i18nToolWorklogSummaryVi(kind, state, count) { + return _i18nToolWorklogSummaryFromMap(_I18N_TOOL_SUMMARY_TEXT_VI, kind, state, count); +} +function _i18nToolSummaryJoinVi(parts) { + return Array.isArray(parts) ? parts.filter(Boolean).join(', ') : ''; +} + + // Active locale — defaults to English; overridden by loadLocale() at boot. let _locale = LOCALES.en; diff --git a/tests/test_2735_open_in_vscode.py b/tests/test_2735_open_in_vscode.py index 79184ed1..dfa2e3e0 100644 --- a/tests/test_2735_open_in_vscode.py +++ b/tests/test_2735_open_in_vscode.py @@ -229,7 +229,7 @@ class TestOpenInVsCodeI18n: """open_in_vscode key must appear exactly once per locale (11 total).""" src = I18N.read_text(encoding="utf-8") count = src.count("open_in_vscode:") - assert count == 12, ( + assert count == 13, ( f"Expected 11 open_in_vscode: entries (one per locale), found {count}" ) @@ -237,7 +237,7 @@ class TestOpenInVsCodeI18n: """open_in_vscode_failed key must appear exactly once per locale (11 total).""" src = I18N.read_text(encoding="utf-8") count = src.count("open_in_vscode_failed:") - assert count == 12, ( + assert count == 13, ( f"Expected 11 open_in_vscode_failed: entries (one per locale), found {count}" ) diff --git a/tests/test_auxiliary_models_settings.py b/tests/test_auxiliary_models_settings.py index 25fd253e..e39c24f4 100644 --- a/tests/test_auxiliary_models_settings.py +++ b/tests/test_auxiliary_models_settings.py @@ -305,8 +305,8 @@ class TestAuxiliaryModelsI18n: """Count of each key should equal the number of locales (12 with Turkish).""" for key in self.REQUIRED_KEYS: count = I18N_JS.count(f"{key}:") - assert count == 13, ( - f"i18n key '{key}' found {count} times — expected 12 (one per locale)" + assert count == 14, ( + f"i18n key '{key}' found {count} times — expected 14 (one per locale)" ) diff --git a/tests/test_issue1488_composer_voice_buttons.py b/tests/test_issue1488_composer_voice_buttons.py index 0f08eb25..7cd02c2f 100644 --- a/tests/test_issue1488_composer_voice_buttons.py +++ b/tests/test_issue1488_composer_voice_buttons.py @@ -123,7 +123,7 @@ class TestComposerVoiceButtonI18n: "voice_mode_toggle_active", ) - LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl") + LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl", "vi") def test_legacy_voice_toggle_key_removed(self): """The old key whose string was 'Voice input' caused the duplicate- @@ -171,7 +171,7 @@ class TestComposerVoiceButtonI18n: class TestVoiceModePreferenceGate: """boot.js must hide btnVoiceMode by default, surface it via Preferences.""" - LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl") + LOCALES = ("en", "fr", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "tr", "pl", "vi") def test_voice_mode_pref_is_localstorage_backed(self): """The pref reads from localStorage key 'hermes-voice-mode-button'.""" diff --git a/tests/test_issue2419_cache_usage_display.py b/tests/test_issue2419_cache_usage_display.py index e7025889..836694ee 100644 --- a/tests/test_issue2419_cache_usage_display.py +++ b/tests/test_issue2419_cache_usage_display.py @@ -63,8 +63,8 @@ def test_context_indicator_surfaces_cache_hit_rate(): def test_cache_usage_labels_are_localized(): src = (ROOT / "static" / "i18n.js").read_text() - assert src.count("usage_cache_hit_detail:") == 13 - assert src.count("usage_cached_percent:") == 13 + assert src.count("usage_cache_hit_detail:") == 14 + assert src.count("usage_cached_percent:") == 14 assert "usage_cache_hit_detail: 'Cache: {0}% hit ({1} read / {2} write)'" in src assert "usage_cached_percent: '{0}% cached'" in src diff --git a/tests/test_issue2679_hide_suggestions.py b/tests/test_issue2679_hide_suggestions.py index 893d02c1..33c8bbc3 100644 --- a/tests/test_issue2679_hide_suggestions.py +++ b/tests/test_issue2679_hide_suggestions.py @@ -50,8 +50,8 @@ def test_panels_round_trip_and_hot_apply_hide_suggestions(): def test_hide_suggestions_i18n_all_locales_and_changelog(): js = I18N.read_text(encoding="utf-8") - assert js.count("settings_label_hide_suggestions:") == 13 - assert js.count("settings_desc_hide_suggestions:") == 13 + assert js.count("settings_label_hide_suggestions:") == 14 + assert js.count("settings_desc_hide_suggestions:") == 14 changelog = CHANGELOG.read_text(encoding="utf-8") assert "#2679" in changelog assert "hide_empty_state_suggestions" in changelog diff --git a/tests/test_issue3227_help_tab.py b/tests/test_issue3227_help_tab.py index 47b87337..99d58aec 100644 --- a/tests/test_issue3227_help_tab.py +++ b/tests/test_issue3227_help_tab.py @@ -6,7 +6,7 @@ PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8") I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8") STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8") -LOCALE_COUNT = 13 # en, it, ja, ru, es, de, zh, zh-TW, pt, ko, fr, tr, pl +LOCALE_COUNT = 14 # en, it, ja, ru, es, de, zh, zh-TW, pt, ko, fr, tr, pl def test_help_nav_button_present(): diff --git a/tests/test_issue3242_3214_i18n_tooltips.py b/tests/test_issue3242_3214_i18n_tooltips.py index ad5d8814..cf5d13a4 100644 --- a/tests/test_issue3242_3214_i18n_tooltips.py +++ b/tests/test_issue3242_3214_i18n_tooltips.py @@ -5,7 +5,7 @@ BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8") I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8") SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8") -LOCALE_COUNT = 13 +LOCALE_COUNT = 14 def test_raw_audio_active_recording_uses_dedicated_i18n_key(): diff --git a/tests/test_issue4325_virtualization_toggle.py b/tests/test_issue4325_virtualization_toggle.py index 7aacd98f..1f0fbc62 100644 --- a/tests/test_issue4325_virtualization_toggle.py +++ b/tests/test_issue4325_virtualization_toggle.py @@ -76,8 +76,8 @@ def test_panels_round_trip_and_hot_apply_virtualize_toggle(): def test_virtualize_toggle_i18n_all_locales(): js = I18N.read_text(encoding="utf-8") - assert js.count("settings_label_virtualize_transcript:") == 13 - assert js.count("settings_desc_virtualize_transcript:") == 13 + assert js.count("settings_label_virtualize_transcript:") == 14 + assert js.count("settings_desc_virtualize_transcript:") == 14 # ── #4343 force-off-for-everyone migration (load_settings behavior) ────────── diff --git a/tests/test_issue4356_no_git_update_check.py b/tests/test_issue4356_no_git_update_check.py index 5f8243d6..92e93d20 100644 --- a/tests/test_issue4356_no_git_update_check.py +++ b/tests/test_issue4356_no_git_update_check.py @@ -80,10 +80,10 @@ def test_panels_has_no_git_branch(): def test_i18n_no_git_key_all_locales(): - """settings_update_no_git key appears in all 13 locale blocks in static/i18n.js.""" + """settings_update_no_git key appears in all 14 locale blocks in static/i18n.js.""" i18n_path = Path(__file__).resolve().parent.parent / 'static' / 'i18n.js' content = i18n_path.read_text() # Count occurrences of the new key (should be exactly 13, one per locale) count = content.count("settings_update_no_git") - assert count == 13, f"Expected exactly 13 settings_update_no_git keys, found {count}" + assert count == 14, f"Expected exactly 14 settings_update_no_git keys, found {count}" diff --git a/tests/test_pr1721_rtl_salvage.py b/tests/test_pr1721_rtl_salvage.py index 4d365578..f7c8cb61 100644 --- a/tests/test_pr1721_rtl_salvage.py +++ b/tests/test_pr1721_rtl_salvage.py @@ -112,5 +112,5 @@ def test_rtl_in_config_defaults_and_writable_keys(): def test_rtl_localized_in_all_locales(): js = I18N.read_text(encoding="utf-8") # Count occurrences — should match the 11 locale blocks - assert js.count("settings_label_rtl:") == 13 - assert js.count("settings_desc_rtl:") == 13 + assert js.count("settings_label_rtl:") == 14 + assert js.count("settings_desc_rtl:") == 14 diff --git a/tests/test_quota_chip_settings_toggle.py b/tests/test_quota_chip_settings_toggle.py index 1908fb50..13387873 100644 --- a/tests/test_quota_chip_settings_toggle.py +++ b/tests/test_quota_chip_settings_toggle.py @@ -88,5 +88,5 @@ def test_quota_chip_panels_round_trip(): def test_quota_chip_localized_in_all_locales(): js = I18N.read_text(encoding="utf-8") - assert js.count("settings_label_quota_chip:") == 13, "12 locales expected" - assert js.count("settings_desc_quota_chip:") == 13, "12 locales expected" + assert js.count("settings_label_quota_chip:") == 14, "12 locales expected" + assert js.count("settings_desc_quota_chip:") == 14, "12 locales expected" diff --git a/tests/test_verdigris_skin.py b/tests/test_verdigris_skin.py index fa2736a0..0c8c6ce4 100644 --- a/tests/test_verdigris_skin.py +++ b/tests/test_verdigris_skin.py @@ -38,4 +38,4 @@ def test_verdigris_has_no_light_variant(): def test_verdigris_i18n_lists_skin_in_all_locales(): # There are 12 locales; each should now include verdigris as the trailing skin. # 10 locales use ASCII closing paren, 2 Chinese locales use full-width paren. - assert I18N_JS.count("verdigris)") + I18N_JS.count("verdigris)") == 13 + assert I18N_JS.count("verdigris)") + I18N_JS.count("verdigris)") == 14 diff --git a/tests/test_vietnamese_locale.py b/tests/test_vietnamese_locale.py new file mode 100644 index 00000000..104f5389 --- /dev/null +++ b/tests/test_vietnamese_locale.py @@ -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}" diff --git a/tests/test_zeus_skin.py b/tests/test_zeus_skin.py index 34b4e877..abca40e9 100644 --- a/tests/test_zeus_skin.py +++ b/tests/test_zeus_skin.py @@ -45,4 +45,4 @@ def test_zeus_modals_are_not_navy(): def test_zeus_i18n_lists_skin_in_all_locales(): # There are 12 locales; each should include zeus in the skin list. - assert I18N_JS.count("/zeus/") == 13 + assert I18N_JS.count("/zeus/") == 14