diff --git a/frontend/src/locale/Utils.test.tsx b/frontend/src/locale/Utils.test.tsx index d1c4b26485..77db2b6b6b 100644 --- a/frontend/src/locale/Utils.test.tsx +++ b/frontend/src/locale/Utils.test.tsx @@ -112,3 +112,14 @@ describe("formatDateTime Estonian locale", () => { } }); }); + +describe("formatDateTime Vietnamese locale", () => { + it("uses 24-hour clock for vi and vi-VN", () => { + const value = "2024-06-15T15:30:00.000Z"; + for (const locale of ["vi", "vi-VN"]) { + const text = formatDateTime(value, locale); + // Vietnamese 12-hour output marks the period with SA (sáng) / CH (chiều) + expect(text).not.toMatch(/\b(SA|CH)\b/); + } + }); +}); diff --git a/frontend/src/locale/Utils.ts b/frontend/src/locale/Utils.ts index 9def46f806..d274dca2cd 100644 --- a/frontend/src/locale/Utils.ts +++ b/frontend/src/locale/Utils.ts @@ -26,6 +26,7 @@ const DATE_LOCALE_BY_LANG: Record = { const HOUR_CYCLE_BY_LANG: Record = { et: "h23", + vi: "h23", }; const formatDateTime = (value: string | number, locale = "en-US"): string => { diff --git a/frontend/src/locale/src/HelpDoc/vi/AccessLists.md b/frontend/src/locale/src/HelpDoc/vi/AccessLists.md index 50e5375a4c..0b0b8e77ed 100644 --- a/frontend/src/locale/src/HelpDoc/vi/AccessLists.md +++ b/frontend/src/locale/src/HelpDoc/vi/AccessLists.md @@ -1,11 +1,10 @@ -## Khái niệm Access List là gì? +## Danh sách truy cập là gì? -Access List (Danh sách truy cập) cung cấp cơ chế chặn (blacklist) hoặc cho phép (whitelist) các địa chỉ IP của client, đồng thời hỗ trợ xác thực Basic HTTP Authentication cho các Proxy Host. +Danh sách truy cập (Access List) cung cấp danh sách chặn hoặc danh sách cho phép theo địa chỉ IP của client, đồng thời hỗ trợ xác thực HTTP cơ bản (Basic HTTP Authentication) cho các máy chủ proxy. -Bạn có thể cấu hình nhiều quy tắc client, nhiều tên người dùng và mật khẩu trong một Access List duy nhất, sau đó áp dụng Access List đó cho một hoặc nhiều Proxy Host. +Bạn có thể cấu hình nhiều quy tắc client, nhiều tên người dùng và mật khẩu trong một danh sách truy cập, sau đó áp dụng nó cho một hoặc nhiều _máy chủ proxy_. -Tính năng này đặc biệt hữu ích đối với: +Tính năng này đặc biệt hữu ích khi: -các dịch vụ web được forward mà không có cơ chế xác thực tích hợp, hoặc - -khi bạn muốn bảo vệ tài nguyên khỏi những client không xác định. +- dịch vụ web được chuyển tiếp không có sẵn cơ chế xác thực, hoặc +- bạn muốn chặn các client không xác định. diff --git a/frontend/src/locale/src/HelpDoc/vi/Certificates.md b/frontend/src/locale/src/HelpDoc/vi/Certificates.md index d25ef00784..e633a3a750 100644 --- a/frontend/src/locale/src/HelpDoc/vi/Certificates.md +++ b/frontend/src/locale/src/HelpDoc/vi/Certificates.md @@ -1,25 +1,24 @@ -## Hỗ trợ Chứng chỉ +## Trợ giúp về chứng chỉ -### Chứng chỉ HTTP (HTTP Certificate) +### Chứng chỉ HTTP -Chứng chỉ được xác thực qua HTTP nghĩa là máy chủ của Let's Encrypt sẽ cố gắng truy cập vào tên miền của bạn thông qua HTTP (không phải HTTPS!). Nếu kiểm tra thành công, chứng chỉ sẽ được cấp. +Chứng chỉ xác thực qua HTTP nghĩa là máy chủ của Let's Encrypt sẽ truy cập vào tên miền của bạn qua HTTP (không phải HTTPS!). Nếu kiểm tra thành công, chứng chỉ sẽ được cấp. -Với phương thức này, bạn phải tạo trước một Proxy Host cho tên miền, có thể truy cập qua HTTP và trỏ về đúng cài đặt Nginx này. -Sau khi chứng chỉ được cấp, bạn có thể chỉnh sửa Proxy Host để sử dụng chứng chỉ đó cho kết nối HTTPS. +Với phương thức này, bạn phải tạo trước một _máy chủ proxy_ cho tên miền, truy cập được qua HTTP và trỏ về máy chủ Nginx này. +Sau khi chứng chỉ được cấp, bạn có thể chỉnh sửa _máy chủ proxy_ để dùng chứng chỉ đó cho kết nối HTTPS. +Tuy nhiên, _máy chủ proxy_ vẫn phải truy cập được qua HTTP thì chứng chỉ mới gia hạn được. -Tuy nhiên, Proxy Host vẫn phải hỗ trợ truy cập HTTP để việc gia hạn chứng chỉ diễn ra bình thường. +Phương thức này _không hỗ trợ_ tên miền wildcard (ví dụ `*.example.com`). -Phương thức này _không hỗ trợ_ wildcard domain. +### Chứng chỉ DNS -### Chứng chỉ DNS (DNS Certificate) +Chứng chỉ xác thực qua DNS yêu cầu bạn dùng plugin của nhà cung cấp DNS. +Plugin sẽ tạo các bản ghi tạm thời cho tên miền của bạn, sau đó Let's Encrypt truy vấn các bản ghi này để xác nhận bạn là chủ sở hữu. Nếu hợp lệ, chứng chỉ sẽ được cấp. -Chứng chỉ được xác thực qua DNS yêu cầu bạn sử dụng plugin của DNS Provider. -Plugin này sẽ tạo các bản ghi tạm thời trong DNS của bạn để Let's Encrypt kiểm tra quyền sở hữu tên miền. Nếu hợp lệ, chứng chỉ sẽ được cấp. +Bạn không cần tạo _máy chủ proxy_ trước khi yêu cầu loại chứng chỉ này, cũng không cần cấu hình _máy chủ proxy_ cho truy cập HTTP. -Khi dùng phương thức này: Bạn không cần tạo sẵn Proxy Host trước và bạn không cần mở HTTP cho Proxy Host. +Phương thức này _có hỗ trợ_ tên miền wildcard. -Phương thức DNS _có hỗ trợ_ wildcard domain. +### Chứng chỉ tùy chỉnh -### Chứng chỉ tùy chỉnh (Custom Certificate) - -Tùy chọn này cho phép bạn tải lên chứng chỉ SSL của riêng mình, được cung cấp bởi Certificate Authority (CA) mà bạn tự chọn. +Dùng tùy chọn này để tải lên chứng chỉ SSL của riêng bạn, do tổ chức cấp chứng chỉ (CA) của bạn cung cấp. diff --git a/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md b/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md index 4c0aae86c2..d380f35644 100644 --- a/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md +++ b/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md @@ -1,8 +1,7 @@ -## 404 Host là gì? +## Máy chủ 404 là gì? -404 Host đơn giản là một host được thiết lập để hiển thị trang 404. +Máy chủ 404 đơn giản là một cấu hình máy chủ chỉ hiển thị trang 404. -Điều này có thể hữu ích khi tên miền của bạn vẫn xuất hiện trên các công cụ tìm kiếm và bạn muốn hiển thị một trang lỗi đẹp hơn, hoặc muốn thông báo rõ ràng cho các trình thu thập dữ liệu tìm kiếm rằng các trang thuộc tên miền đó không còn tồn tại. +Nó hữu ích khi tên miền của bạn vẫn xuất hiện trên các công cụ tìm kiếm và bạn muốn hiển thị một trang lỗi đẹp hơn, hoặc muốn báo rõ cho các trình thu thập dữ liệu rằng các trang thuộc tên miền đó không còn tồn tại. -Một lợi ích khác của việc có 404 Host là bạn có thể theo dõi nhật ký truy cập vào nó và -xem các nguồn giới thiệu (referrers). \ No newline at end of file +Một lợi ích khác là bạn có thể theo dõi nhật ký các lượt truy cập vào đó và xem trang giới thiệu (referrer). diff --git a/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md index 969f8a7734..e04552c652 100644 --- a/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md +++ b/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md @@ -1,7 +1,7 @@ -## Proxy Host là gì? +## Máy chủ proxy là gì? -Proxy Host là điểm truy cập đầu vào cho một dịch vụ web mà bạn muốn chuyển tiếp. +Máy chủ proxy là điểm tiếp nhận yêu cầu cho một dịch vụ web mà bạn muốn chuyển tiếp. -Nó cung cấp khả năng kết thúc SSL (SSL termination) tùy chọn cho các dịch vụ vốn không hỗ trợ SSL tích hợp. +Nó có thể đảm nhận phần mã hóa SSL (SSL termination) cho những dịch vụ không có sẵn hỗ trợ SSL. -Proxy Host là loại cấu hình phổ biến nhất trong Nginx Proxy Manager. \ No newline at end of file +Máy chủ proxy là tính năng được dùng nhiều nhất trong Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md index 4339923643..ecda55c107 100644 --- a/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md +++ b/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md @@ -1,5 +1,5 @@ -## Redirection Host là gì? +## Máy chủ chuyển hướng là gì? -Redirection Host sẽ chuyển hướng các yêu cầu từ tên miền truy cập vào và đưa người xem sang một tên miền khác +Máy chủ chuyển hướng sẽ chuyển các yêu cầu tới tên miền này sang một tên miền khác. -Lý do phổ biến nhất để sử dụng loại host này là khi trang web của bạn đổi sang tên miền mới nhưng vẫn còn các liên kết từ công cụ tìm kiếm hoặc nguồn giới thiệu trỏ về tên miền cũ. \ No newline at end of file +Lý do phổ biến nhất để dùng loại máy chủ này là khi trang web của bạn đổi sang tên miền mới nhưng các công cụ tìm kiếm hoặc trang giới thiệu vẫn còn liên kết trỏ về tên miền cũ. diff --git a/frontend/src/locale/src/HelpDoc/vi/Streams.md b/frontend/src/locale/src/HelpDoc/vi/Streams.md index 43cf97b2f6..c6b451bc3f 100644 --- a/frontend/src/locale/src/HelpDoc/vi/Streams.md +++ b/frontend/src/locale/src/HelpDoc/vi/Streams.md @@ -1,6 +1,5 @@ -## Stream là gì? +## Luồng TCP/UDP là gì? -Stream là một tính năng tương đối mới của Nginx, dùng để chuyển tiếp lưu lượng -TCP/UDP trực tiếp tới một máy khác trong mạng. +Luồng TCP/UDP (Stream) là một tính năng tương đối mới của Nginx, dùng để chuyển tiếp lưu lượng TCP/UDP trực tiếp tới một máy khác trong mạng. -Nếu bạn đang vận hành các máy chủ game, FTP hoặc SSH thì tính năng này sẽ rất hữu ích. \ No newline at end of file +Tính năng này rất hữu ích nếu bạn đang chạy máy chủ game, FTP hoặc SSH. diff --git a/frontend/src/locale/src/vi.json b/frontend/src/locale/src/vi.json index 32d26d5590..f0a180038d 100644 --- a/frontend/src/locale/src/vi.json +++ b/frontend/src/locale/src/vi.json @@ -1,4 +1,61 @@ { + "2fa.backup-codes-remaining": { + "defaultMessage": "Số mã dự phòng còn lại: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Hãy lưu các mã dự phòng này ở nơi an toàn. Mỗi mã chỉ dùng được một lần." + }, + "2fa.disable": { + "defaultMessage": "Tắt xác thực hai lớp" + }, + "2fa.disable-confirm": { + "defaultMessage": "Tắt 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Tắt xác thực hai lớp sẽ khiến tài khoản của bạn kém an toàn hơn." + }, + "2fa.disabled": { + "defaultMessage": "Đã tắt" + }, + "2fa.done": { + "defaultMessage": "Tôi đã lưu các mã dự phòng" + }, + "2fa.enable": { + "defaultMessage": "Bật xác thực hai lớp" + }, + "2fa.enabled": { + "defaultMessage": "Đã bật" + }, + "2fa.enter-code": { + "defaultMessage": "Nhập mã xác minh" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Nhập mã xác minh để tắt" + }, + "2fa.regenerate": { + "defaultMessage": "Tạo lại" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Tạo lại mã dự phòng" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Nhập mã xác minh để tạo mã dự phòng mới. Các mã cũ sẽ không còn hiệu lực." + }, + "2fa.secret-key": { + "defaultMessage": "Khóa bí mật" + }, + "2fa.setup-instructions": { + "defaultMessage": "Quét mã QR này bằng ứng dụng xác thực, hoặc nhập khóa bí mật thủ công." + }, + "2fa.status": { + "defaultMessage": "Trạng thái" + }, + "2fa.title": { + "defaultMessage": "Xác thực hai lớp" + }, + "2fa.verify-enable": { + "defaultMessage": "Xác minh và bật" + }, "access-list": { "defaultMessage": "Danh sách truy cập" }, @@ -9,13 +66,19 @@ "defaultMessage": "{count} người dùng" }, "access-list.help-rules-last": { - "defaultMessage": "Quy tắc từ chối tất cả này sẽ được thêm vào cuối khi tồn tại ít nhất 1 quy tắc" + "defaultMessage": "Khi có ít nhất 1 quy tắc, quy tắc từ chối tất cả này sẽ được thêm vào cuối" }, "access-list.help.rules-order": { - "defaultMessage": "Các quy tắc cho phép và từ chối sẽ được thực thi theo thứ tự được xác định." + "defaultMessage": "Lưu ý: các quy tắc cho phép và từ chối được áp dụng theo đúng thứ tự khai báo." + }, + "access-list.inherit": { + "defaultMessage": "Kế thừa từ máy chủ proxy" + }, + "access-list.inherit.subtitle": { + "defaultMessage": "Sử dụng danh sách truy cập của máy chủ proxy" }, "access-list.pass-auth": { - "defaultMessage": "Chuyển xác thực lên thượng nguồn" + "defaultMessage": "Chuyển tiếp thông tin xác thực tới máy chủ đích" }, "access-list.public": { "defaultMessage": "Có thể truy cập công khai" @@ -23,6 +86,9 @@ "access-list.public.subtitle": { "defaultMessage": "Không cần xác thực cơ bản" }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 hoặc 192.168.1.0/24 hoặc 2001:0db8::/32" + }, "access-list.satisfy-any": { "defaultMessage": "Thỏa mãn điều kiện bất kỳ" }, @@ -36,7 +102,13 @@ "defaultMessage": "Thêm" }, "action.add-location": { - "defaultMessage": "Thêm Vị trí" + "defaultMessage": "Thêm đường dẫn" + }, + "action.allow": { + "defaultMessage": "Cho phép" + }, + "action.clear": { + "defaultMessage": "Xóa bộ lọc" }, "action.close": { "defaultMessage": "Đóng" @@ -44,6 +116,9 @@ "action.delete": { "defaultMessage": "Xóa" }, + "action.deny": { + "defaultMessage": "Từ chối" + }, "action.disable": { "defaultMessage": "Tắt" }, @@ -56,6 +131,9 @@ "action.enable": { "defaultMessage": "Bật" }, + "action.logs": { + "defaultMessage": "Nhật ký" + }, "action.permissions": { "defaultMessage": "Quyền" }, @@ -63,10 +141,13 @@ "defaultMessage": "Gia hạn" }, "action.view-details": { - "defaultMessage": "Xem Chi tiết" + "defaultMessage": "Xem chi tiết" }, "auditlogs": { - "defaultMessage": "Nhật ký kiểm tra" + "defaultMessage": "Nhật ký hoạt động" + }, + "auto": { + "defaultMessage": "Tự động" }, "cancel": { "defaultMessage": "Hủy" @@ -75,19 +156,19 @@ "defaultMessage": "Chứng chỉ" }, "certificate.custom-certificate": { - "defaultMessage": "Certificate (crt)" + "defaultMessage": "Chứng chỉ" }, "certificate.custom-certificate-key": { - "defaultMessage": "Certificate Key" + "defaultMessage": "Khóa riêng của chứng chỉ" }, "certificate.custom-intermediate": { - "defaultMessage": "Intermediate Certificate" + "defaultMessage": "Chứng chỉ trung gian" }, "certificate.in-use": { "defaultMessage": "Đang sử dụng" }, "certificate.none.subtitle": { - "defaultMessage": "Không có chứng chỉ nào được chỉ định" + "defaultMessage": "Chưa gán chứng chỉ nào" }, "certificate.none.subtitle.for-http": { "defaultMessage": "Máy chủ này sẽ không sử dụng HTTPS" @@ -96,19 +177,19 @@ "defaultMessage": "Không có" }, "certificate.not-in-use": { - "defaultMessage": "Không được dùng" + "defaultMessage": "Chưa sử dụng" }, "certificate.renew": { - "defaultMessage": "Gia hạn Chứng chỉ" + "defaultMessage": "Gia hạn chứng chỉ" }, "certificates": { - "defaultMessage": "Danh sách chứng chỉ" + "defaultMessage": "Chứng chỉ" }, "certificates.custom": { "defaultMessage": "Chứng chỉ tùy chỉnh" }, "certificates.custom.warning": { - "defaultMessage": "Các tệp chính được bảo vệ bằng cụm mật khẩu không được hỗ trợ." + "defaultMessage": "Không hỗ trợ tệp khóa có đặt cụm mật khẩu bảo vệ." }, "certificates.dns.credentials": { "defaultMessage": "Nội dung tệp thông tin xác thực" @@ -120,40 +201,43 @@ "defaultMessage": "Dữ liệu này sẽ được lưu trữ dưới dạng bản rõ trong cơ sở dữ liệu và trong một tệp!" }, "certificates.dns.propagation-seconds": { - "defaultMessage": "Thời gian lan truyền (Giây)" + "defaultMessage": "Thời gian lan truyền (giây)" }, "certificates.dns.propagation-seconds-note": { - "defaultMessage": "Để trống để sử dụng giá trị mặc định của plugin. Số giây chờ truyền DNS." + "defaultMessage": "Để trống để dùng giá trị mặc định của plugin. Số giây chờ bản ghi DNS lan truyền." }, "certificates.dns.provider": { "defaultMessage": "Nhà cung cấp DNS" }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Chọn nhà cung cấp…" + }, "certificates.dns.warning": { "defaultMessage": "Phần này yêu cầu một số kiến thức về Certbot và các plugin DNS của nó. Vui lòng tham khảo tài liệu plugin tương ứng." }, "certificates.http.reachability-404": { - "defaultMessage": "Có một máy chủ được tìm thấy ở miền này nhưng có vẻ như nó không phải là NPM. Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy." + "defaultMessage": "Tìm thấy một máy chủ tại tên miền này nhưng có vẻ không phải Nginx Proxy Manager. Vui lòng đảm bảo tên miền trỏ đến IP của máy đang chạy NPM." }, "certificates.http.reachability-failed-to-check": { "defaultMessage": "Không thể kiểm tra khả năng truy cập do lỗi giao tiếp với site24x7.com." }, "certificates.http.reachability-not-resolved": { - "defaultMessage": "Không có máy chủ có sẵn tại tên miền này. Vui lòng đảm bảo rằng miền của bạn tồn tại và trỏ đến IP nơi phiên bản NPM của bạn đang chạy và nếu cần, cổng 80 sẽ được chuyển tiếp trong bộ định tuyến của bạn." + "defaultMessage": "Không có máy chủ nào tại tên miền này. Vui lòng đảm bảo tên miền tồn tại, trỏ đến IP của máy đang chạy NPM và nếu cần, cổng 80 đã được chuyển tiếp (port forwarding) trên router." }, "certificates.http.reachability-ok": { - "defaultMessage": "Máy chủ của bạn có thể truy cập được và có thể tạo chứng chỉ." + "defaultMessage": "Máy chủ của bạn truy cập được, có thể tiến hành tạo chứng chỉ." }, "certificates.http.reachability-other": { - "defaultMessage": "Có một máy chủ được tìm thấy ở miền này nhưng nó trả về mã trạng thái không mong muốn {code}. Đây có phải là máy chủ NPM không? Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy." + "defaultMessage": "Tìm thấy một máy chủ tại tên miền này nhưng nó trả về mã trạng thái không mong đợi {code}. Đây có phải máy chủ NPM không? Vui lòng đảm bảo tên miền trỏ đến IP của máy đang chạy NPM." }, "certificates.http.reachability-wrong-data": { - "defaultMessage": "Có một máy chủ được tìm thấy ở miền này nhưng nó trả về một dữ liệu không mong muốn. Đây có phải là máy chủ NPM không? Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy." + "defaultMessage": "Tìm thấy một máy chủ tại tên miền này nhưng nó trả về dữ liệu không mong đợi. Đây có phải máy chủ NPM không? Vui lòng đảm bảo tên miền trỏ đến IP của máy đang chạy NPM." }, "certificates.http.test-results": { "defaultMessage": "Kết quả kiểm tra" }, "certificates.http.warning": { - "defaultMessage": "Các miền này phải được cấu hình sẵn để trỏ đến cài đặt này." + "defaultMessage": "Các tên miền này phải được cấu hình sẵn để trỏ đến máy chủ NPM này." }, "certificates.key-type": { "defaultMessage": "Loại khóa" @@ -177,16 +261,16 @@ "defaultMessage": "Truy cập" }, "column.authorization": { - "defaultMessage": "Ủy quyền" + "defaultMessage": "Xác thực" }, "column.authorizations": { - "defaultMessage": "Danh sách ủy quyền" + "defaultMessage": "Xác thực" }, "column.custom-locations": { - "defaultMessage": "Quy tắc đường dẫn tùy chỉnh (Vị trí)" + "defaultMessage": "Đường dẫn tùy chỉnh" }, "column.destination": { - "defaultMessage": "Mục tiêu" + "defaultMessage": "Đích" }, "column.details": { "defaultMessage": "Chi tiết" @@ -194,6 +278,9 @@ "column.email": { "defaultMessage": "Email" }, + "column.error": { + "defaultMessage": "Lỗi" + }, "column.event": { "defaultMessage": "Sự kiện" }, @@ -201,10 +288,10 @@ "defaultMessage": "Hết hạn" }, "column.http-code": { - "defaultMessage": "HTTP Code" + "defaultMessage": "Mã HTTP" }, "column.incoming-port": { - "defaultMessage": "Cổng đến" + "defaultMessage": "Cổng vào" }, "column.name": { "defaultMessage": "Tên" @@ -228,10 +315,10 @@ "defaultMessage": "Tất cả" }, "column.satisfy-any": { - "defaultMessage": "Bất kì" + "defaultMessage": "Bất kỳ" }, "column.scheme": { - "defaultMessage": "Scheme" + "defaultMessage": "Giao thức" }, "column.source": { "defaultMessage": "Nguồn" @@ -243,7 +330,7 @@ "defaultMessage": "Trạng thái" }, "created-on": { - "defaultMessage": "Đã tạo: {date}" + "defaultMessage": "Tạo lúc: {date}" }, "dashboard": { "defaultMessage": "Bảng điều khiển" @@ -255,19 +342,19 @@ "defaultMessage": "Máy chủ 404" }, "dead-hosts.count": { - "defaultMessage": "Số trang lỗi {count}" + "defaultMessage": "{count} máy chủ 404" }, "disabled": { "defaultMessage": "Đã tắt" }, "domain-names": { - "defaultMessage": "Danh sách tên miền" + "defaultMessage": "Tên miền" }, "domain-names.max": { "defaultMessage": "Tối đa {count} tên miền" }, "domain-names.placeholder": { - "defaultMessage": "Nhập tên miền vào đây..." + "defaultMessage": "Nhập để thêm tên miền…" }, "domain-names.wildcards-not-permitted": { "defaultMessage": "Ký tự đại diện không được phép cho loại này" @@ -275,6 +362,9 @@ "domain-names.wildcards-not-supported": { "defaultMessage": "Ký tự đại diện không được hỗ trợ cho CA này" }, + "domains.advanced": { + "defaultMessage": "Nâng cao" + }, "domains.force-ssl": { "defaultMessage": "Bắt buộc SSL" }, @@ -282,13 +372,16 @@ "defaultMessage": "Bật HSTS" }, "domains.hsts-subdomains": { - "defaultMessage": "Tên miền phụ HSTS" + "defaultMessage": "HSTS cho tên miền con" }, "domains.http2-support": { "defaultMessage": "Hỗ trợ HTTP/2" }, + "domains.trust-forwarded-proto": { + "defaultMessage": "Tin cậy header X-Forwarded-Proto từ proxy phía trước" + }, "domains.use-dns": { - "defaultMessage": "Dùng thử thách DNS" + "defaultMessage": "Xác thực qua DNS" }, "email-address": { "defaultMessage": "Địa chỉ email" @@ -297,19 +390,19 @@ "defaultMessage": "Không có kết quả nào" }, "empty-subtitle": { - "defaultMessage": "Tại sao bạn không tạo một cái luôn?" + "defaultMessage": "Hãy bắt đầu bằng cách tạo mới." }, "enabled": { "defaultMessage": "Đã bật" }, "error.access.at-least-one": { - "defaultMessage": "Yêu cầu ít nhất một quy tắc ủy quyền hoặc truy cập" + "defaultMessage": "Cần ít nhất một tài khoản xác thực hoặc một quy tắc truy cập" }, "error.access.duplicate-usernames": { - "defaultMessage": "Tên người dùng được ủy quyền phải là duy nhất" + "defaultMessage": "Tên người dùng xác thực không được trùng nhau" }, "error.invalid-auth": { - "defaultMessage": "Email hoặc Mật khẩu không hợp lệ" + "defaultMessage": "Email hoặc mật khẩu không hợp lệ" }, "error.invalid-domain": { "defaultMessage": "Tên miền không hợp lệ: {domain}" @@ -336,7 +429,7 @@ "defaultMessage": "Mật khẩu phải khớp" }, "error.required": { - "defaultMessage": "Điều này là bắt buộc" + "defaultMessage": "Trường này là bắt buộc" }, "expires.on": { "defaultMessage": "Hết hạn: {date}" @@ -348,10 +441,10 @@ "defaultMessage": "Chặn các hoạt động khai thác phổ biến" }, "host.flags.cache-assets": { - "defaultMessage": "Cache tài nguyên" + "defaultMessage": "Bộ nhớ đệm tài nguyên" }, "host.flags.preserve-path": { - "defaultMessage": "Bảo toàn đường dẫn" + "defaultMessage": "Giữ nguyên đường dẫn" }, "host.flags.protocols": { "defaultMessage": "Giao thức" @@ -360,16 +453,16 @@ "defaultMessage": "Hỗ trợ Websockets" }, "host.forward-port": { - "defaultMessage": "Chuyển tiếp cổng" + "defaultMessage": "Cổng đích" }, "host.forward-scheme": { - "defaultMessage": "Scheme" + "defaultMessage": "Giao thức" }, "hosts": { "defaultMessage": "Máy chủ" }, "http-only": { - "defaultMessage": "HTTP Only" + "defaultMessage": "Chỉ HTTP" }, "lets-encrypt": { "defaultMessage": "Let's Encrypt" @@ -381,28 +474,94 @@ "defaultMessage": "Let's Encrypt qua HTTP" }, "loading": { - "defaultMessage": "Đang tải..." + "defaultMessage": "Đang tải…" + }, + "location.advanced-config": { + "defaultMessage": "Có cấu hình Nginx tùy chỉnh" + }, + "location.filter": { + "defaultMessage": "Lọc theo đường dẫn hoặc đích" + }, + "login.2fa-code": { + "defaultMessage": "Mã xác minh" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Nhập mã" + }, + "login.2fa-description": { + "defaultMessage": "Nhập mã từ ứng dụng xác thực của bạn" + }, + "login.2fa-title": { + "defaultMessage": "Xác thực hai lớp" + }, + "login.2fa-verify": { + "defaultMessage": "Xác minh" }, "login.title": { "defaultMessage": "Đăng nhập vào tài khoản của bạn" }, + "logs": { + "defaultMessage": "Nhật ký" + }, + "logs.channel.access": { + "defaultMessage": "Truy cập" + }, + "logs.channel.error": { + "defaultMessage": "Lỗi" + }, + "logs.download": { + "defaultMessage": "Tải xuống" + }, + "logs.empty": { + "defaultMessage": "Chưa có mục nhật ký nào." + }, + "logs.level": { + "defaultMessage": "Mức độ" + }, + "logs.level.all": { + "defaultMessage": "Tất cả mức độ" + }, + "logs.lines": { + "defaultMessage": "Số dòng" + }, + "logs.live": { + "defaultMessage": "Trực tiếp" + }, + "logs.paused": { + "defaultMessage": "Đã tạm dừng" + }, + "logs.refresh": { + "defaultMessage": "Làm mới" + }, + "logs.search-placeholder": { + "defaultMessage": "Tìm trong nhật ký…" + }, + "logs.source": { + "defaultMessage": "Nguồn" + }, + "logs.source.system": { + "defaultMessage": "Hệ thống" + }, + "logs.truncated-warning": { + "defaultMessage": "Chỉ hiển thị phần mới nhất của tệp này." + }, "nginx-config.label": { "defaultMessage": "Cấu hình Nginx tùy chỉnh" }, "nginx-config.placeholder": { - "defaultMessage": "# Nhập cấu hình Nginx tùy chỉnh của bạn vào đây và bạn phải tự chịu rủi ro!" + "defaultMessage": "# Nhập cấu hình Nginx tùy chỉnh tại đây, bạn tự chịu rủi ro!" }, "no-permission-error": { - "defaultMessage": "Bạn không có quyền truy cập trang này." + "defaultMessage": "Bạn không có quyền xem nội dung này." }, "notfound.action": { "defaultMessage": "Về trang chủ" }, "notfound.content": { - "defaultMessage": "Chúng tôi xin lỗi nhưng trang bạn đang tìm kiếm không được tìm thấy" + "defaultMessage": "Rất tiếc, không tìm thấy trang bạn yêu cầu" }, "notfound.title": { - "defaultMessage": "Rất tiếc… Bạn vừa tìm thấy một trang lỗi" + "defaultMessage": "Ối… Bạn vừa lạc vào một trang lỗi" }, "notification.error": { "defaultMessage": "Lỗi" @@ -417,7 +576,7 @@ "defaultMessage": "{object} đã được bật" }, "notification.object-renewed": { - "defaultMessage": "{object} đã được làm mới" + "defaultMessage": "{object} đã được gia hạn" }, "notification.object-saved": { "defaultMessage": "{object} đã được lưu" @@ -435,13 +594,13 @@ "defaultMessage": "Xóa {object}" }, "object.delete.content": { - "defaultMessage": "Bạn có chắc muốn xóa {object} không?" + "defaultMessage": "Bạn có chắc muốn xóa {object} này không?" }, "object.edit": { "defaultMessage": "Chỉnh sửa {object}" }, "object.empty": { - "defaultMessage": "Không có {objects}" + "defaultMessage": "Chưa có {objects} nào" }, "object.event.created": { "defaultMessage": "Đã tạo {object}" @@ -477,10 +636,10 @@ "defaultMessage": "Tạo mật khẩu ngẫu nhiên" }, "password.hide": { - "defaultMessage": "Ẩn Mật khẩu" + "defaultMessage": "Ẩn mật khẩu" }, "password.show": { - "defaultMessage": "Hiện Mật khẩu" + "defaultMessage": "Hiện mật khẩu" }, "permissions.hidden": { "defaultMessage": "Ẩn" @@ -495,16 +654,16 @@ "defaultMessage": "Tất cả các mục" }, "permissions.visibility.title": { - "defaultMessage": "Khả năng hiển thị mục" + "defaultMessage": "Phạm vi hiển thị" }, "permissions.visibility.user": { - "defaultMessage": "Chỉ các mục đã tạo" + "defaultMessage": "Chỉ các mục do mình tạo" }, "proxy-host": { "defaultMessage": "Máy chủ proxy" }, "proxy-host.forward-host": { - "defaultMessage": "Chuyển tiếp Hostname / IP" + "defaultMessage": "Hostname / IP đích" }, "proxy-hosts": { "defaultMessage": "Máy chủ proxy" @@ -516,25 +675,43 @@ "defaultMessage": "Công khai" }, "redirection-host": { - "defaultMessage": "Redirection Host" + "defaultMessage": "Máy chủ chuyển hướng" }, "redirection-host.forward-domain": { - "defaultMessage": "Chuyển tiếp Tên miền" + "defaultMessage": "Tên miền đích" }, "redirection-host.forward-http-code": { - "defaultMessage": "HTTP Code" + "defaultMessage": "Mã HTTP" }, "redirection-hosts": { - "defaultMessage": "Redirection Hosts" + "defaultMessage": "Máy chủ chuyển hướng" }, "redirection-hosts.count": { "defaultMessage": "{count} máy chủ chuyển hướng" }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Nhiều lựa chọn" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Chuyển hướng vĩnh viễn" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Chuyển hướng tạm thời" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Xem trang khác" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Chuyển hướng tạm thời (giữ phương thức)" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Chuyển hướng vĩnh viễn (giữ phương thức)" + }, "role.admin": { "defaultMessage": "Quản trị viên" }, "role.standard-user": { - "defaultMessage": "Người dùng bình thường" + "defaultMessage": "Người dùng thường" }, "save": { "defaultMessage": "Lưu" @@ -555,10 +732,10 @@ "defaultMessage": "Không có phản hồi (444)" }, "settings.default-site.congratulations": { - "defaultMessage": "Trang chào mừng" + "defaultMessage": "Trang chúc mừng" }, "settings.default-site.description": { - "defaultMessage": "Hiển thị gì khi Nginx gặp phải Máy chủ không xác định" + "defaultMessage": "Nội dung hiển thị khi Nginx nhận yêu cầu tới tên miền không xác định" }, "settings.default-site.html": { "defaultMessage": "HTML tùy chỉnh" @@ -582,19 +759,22 @@ "defaultMessage": "Chứng chỉ SSL" }, "stream": { - "defaultMessage": "Stream" + "defaultMessage": "Luồng TCP/UDP" }, "stream.forward-host": { - "defaultMessage": "Chuyển tiếp Host" + "defaultMessage": "Máy chủ đích" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com hoặc 10.0.0.1 hoặc 2001:db8:3333:4444:5555:6666:7777:8888" }, "stream.incoming-port": { "defaultMessage": "Cổng vào" }, "streams": { - "defaultMessage": "Danh sách các Stream" + "defaultMessage": "Luồng TCP/UDP" }, "streams.count": { - "defaultMessage": "Số Stream {count}" + "defaultMessage": "{count} luồng TCP/UDP" }, "streams.tcp": { "defaultMessage": "TCP" @@ -606,16 +786,16 @@ "defaultMessage": "Kiểm tra" }, "update-available": { - "defaultMessage": "Cập nhật khả dụng: {latestVersion}" + "defaultMessage": "Đã có bản cập nhật: {latestVersion}" }, "user": { "defaultMessage": "Người dùng" }, "user.change-password": { - "defaultMessage": "Đổi Mật khẩu" + "defaultMessage": "Đổi mật khẩu" }, "user.confirm-password": { - "defaultMessage": "Xác nhận Mật khẩu" + "defaultMessage": "Xác nhận mật khẩu" }, "user.current-password": { "defaultMessage": "Mật khẩu hiện tại" @@ -624,10 +804,10 @@ "defaultMessage": "Chỉnh sửa hồ sơ" }, "user.full-name": { - "defaultMessage": "Tên" + "defaultMessage": "Họ và tên" }, "user.login-as": { - "defaultMessage": "Đăng nhập bằng {name}" + "defaultMessage": "Đăng nhập với tư cách {name}" }, "user.logout": { "defaultMessage": "Đăng xuất" @@ -639,7 +819,7 @@ "defaultMessage": "Tên hiển thị" }, "user.set-password": { - "defaultMessage": "Đặt Mật khẩu" + "defaultMessage": "Đặt mật khẩu" }, "user.set-permissions": { "defaultMessage": "Đặt quyền cho {name}" @@ -650,10 +830,13 @@ "user.switch-light": { "defaultMessage": "Chuyển sang chế độ sáng" }, + "user.two-factor": { + "defaultMessage": "Xác thực hai lớp" + }, "username": { "defaultMessage": "Tên người dùng" }, "users": { - "defaultMessage": "Danh sách người dùng" + "defaultMessage": "Người dùng" } } diff --git a/frontend/src/locale/vi.test.ts b/frontend/src/locale/vi.test.ts new file mode 100644 index 0000000000..3b8cfcb855 --- /dev/null +++ b/frontend/src/locale/vi.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import en from "./src/en.json"; +import vi from "./src/vi.json"; + +const english = en as Record; +const vietnamese = vi as Record; + +// Brand names, codes and ICU-only strings can stay the same as English. +const allowedIdentical = new Set([ + "certificates.key-type-ecdsa", + "certificates.key-type-rsa", + "column.email", + "column.ssl", + "lets-encrypt", + "object.actions-title", + "streams.tcp", + "streams.udp", +]); + +describe("Vietnamese UI locale", () => { + it("covers every English UI key", () => { + const missing = Object.keys(english).filter((key) => !(key in vietnamese)); + expect(missing).toEqual([]); + }); + + it("does not add keys that are missing from English", () => { + const extra = Object.keys(vietnamese).filter((key) => !(key in english)); + expect(extra).toEqual([]); + }); + + it("is translated, not an English copy", () => { + const identical = Object.keys(english).filter( + (key) => vietnamese[key]?.defaultMessage === english[key]?.defaultMessage, + ); + const unexpected = identical.filter((key) => !allowedIdentical.has(key)); + expect(unexpected).toEqual([]); + }); + + it("keeps ICU data placeholders from English strings", () => { + const argNames = [ + "count", + "object", + "objects", + "id", + "date", + "users", + "rules", + "max", + "min", + "domain", + "code", + "latestVersion", + "name", + ]; + const usedArgs = (message: string) => argNames.filter((name) => message.includes(`{${name}`)); + const broken: string[] = []; + + for (const key of Object.keys(english)) { + const expected = usedArgs(english[key].defaultMessage).join(","); + const actual = usedArgs(vietnamese[key]?.defaultMessage ?? "").join(","); + if (expected !== actual) { + broken.push(`${key}: expected {${expected}} got {${actual}}`); + } + } + + expect(broken).toEqual([]); + }); +});