Files
RecipesCam/docker/frontend/src/i18n/vi.ts
T
3dtours c0c99a9672 web: EXPORT offers a size, and a bigger one is upscaled in the browser
The server still never sees a photo, so the model has to run in the page.
Real-ESRGAN x4v3 ships as a 4.9MB ONNX in public/models and is loaded
lazily on the first export that actually needs it; the wasm runtime is
copied next to CanvasKit at build time and stays lazily fetched, cached
for 30 days. Vite is told onnxruntime-web is external-wasm so no 28MB
asset lands in the bundle.

UNCHANGED keeps the old path and the tier cap; 2K/4K/custom upscale only
when the request is larger than the photo being edited, otherwise they
resize down. Guests keep UNCHANGED and 2K. Tiling is 256px with an 8px
overlap, so memory follows the target size rather than four times it.
2026-09-22 09:40:18 +07:00

321 lines
14 KiB
TypeScript

// Vietnamese dictionary — the source of truth for key names (`en.ts` is typed
// as `Dict`, so a missing or misspelled key there is a typecheck error).
// Not translated anywhere, in any language: film-sim names, user recipe names,
// user watermark text — they are proper nouns, see WEB_PLAN.md.
export const vi = {
'lang.name': 'Tiếng Việt',
'nav.demo': 'DÙNG THỬ',
'nav.login': 'ĐĂNG NHẬP',
'nav.signup': 'ĐĂNG KÝ',
'nav.logout': 'Đăng xuất',
'nav.account': 'Tài khoản',
'nav.theme': 'Giao diện',
'nav.language': 'Ngôn ngữ',
'nav.back': 'Về trang chủ',
'nav.admin': 'Quản trị',
'nav.photos': 'Ảnh của tôi',
'nav.profile': 'Hồ sơ',
'nav.studio': 'Studio',
'nav.guest': 'Khách',
'act.undo': 'HOÀN TÁC',
'act.redo': 'LÀM LẠI',
'act.reset': 'ĐẶT LẠI',
'act.export': 'XUẤT ẢNH',
'act.exporting': 'Đang xuất…',
'act.upscaling': 'Đang phóng to ảnh {done}/{total}…',
'act.cancel': 'Huỷ',
'drop.title': 'Kéo & thả ảnh vào đây',
'drop.hint': 'hoặc bấm để chọn ảnh · dán từ clipboard bằng Ctrl+V',
'drop.replace': 'Mở ảnh',
'drop.clear': 'Xoá ảnh',
'drop.peeking': 'ĐANG XEM ẢNH GỐC',
'drop.peekHint': 'Giữ chuột phải hoặc phím \\ để so ảnh gốc',
'drop.compare': 'So với ảnh gốc',
'drop.keep': 'LƯU ẢNH GỐC',
'drop.keepHint': 'Lưu nguyên ảnh gốc chưa chỉnh vào thư viện ảnh của máy. Trình duyệt không chia sẻ được tệp thì ảnh sẽ được tải về.',
'drop.demoLimit': 'Bản demo: ảnh xuất ra tối đa {px}px và có watermark.',
// The histogram overlay. 'HISTOGRAM' is the word Vietnamese photographers use.
'hist.title': 'HISTOGRAM',
'hist.drag': 'Kéo để di chuyển',
'hist.hide': 'Ẩn histogram',
'tab.presets': 'PRESETS',
'tab.favorited': 'FAVORITED',
'tab.photos': 'ẢNH CỦA TÔI',
'tab.light': 'LIGHT',
'tab.wb': 'WB',
'tab.fx': 'FX',
'tab.hsl': 'HSL',
'hsl.hint': 'BẤM PICK RỒI CHỌN MỘT MÀU TRÊN ẢNH',
'tab.frame': 'FRAME',
'tab.save': 'LƯU GẦN ĐÂY',
'tab.create': 'CREATE RECIPES',
'create.title': 'TẠO CÔNG THỨC',
'create.save': 'LƯU CÔNG THỨC',
'create.export': 'XUẤT TỆP .RECIPE',
'create.guestHint':
'Khách: công thức chỉ nằm trong RECIPES tới khi đóng trang. Đăng nhập để lưu vào tài khoản và xuất tệp.',
'create.userHint':
'Đã lưu vào tài khoản. Xuất tệp .recipe để chia sẻ — tệp được mã hóa đúng như trên app.',
'sec.filmSims': 'PHOTO STYLE',
'sec.recipes': 'RECIPES',
'sec.saved': 'ĐÃ LƯU',
'sec.savedEmpty': 'Chưa lưu công thức nào. Đăng nhập rồi bấm LƯU để đồng bộ.',
'sec.frame': 'KHUNG',
'sec.crop': 'CẮT',
'sec.rotate': 'XOAY',
'sec.watermark': 'WATERMARK',
'sec.gps': 'GPS',
'wb.temp': 'COLOR TEMP',
'frame.none': 'KHÔNG KHUNG',
'frame.polaroid': 'RETRO INSTANT',
'frame.classic': 'VIỀN CLASSIC',
'frame.cinematic': 'ĐIỆN ẢNH',
'frame.wall': 'WALL FRAME PORTRAIT',
'frame.wallLand': 'WALL FRAME LANDSCAPE',
'crop.none': 'KHÔNG CẮT',
'crop.1:1': '1:1',
'crop.2:3': '2:3',
'crop.3:2': '3:2',
'crop.3:4': '3:4',
'crop.4:3': '4:3',
'crop.16:9': '16:9',
'crop.free': 'TỰ DO',
'crop.apply': 'ÁP DỤNG',
'crop.cancel': 'HUỶ',
'rot.0': '0°',
'rot.90': '90°',
'rot.180': '180°',
'rot.270': '270°',
'wm.text': 'CHỮ',
'wm.color': 'MÀU',
'wm.size': 'CỠ',
'wm.gps': 'DẤU GPS',
'wm.place': 'TÊN ĐỊA ĐIỂM',
'wm.placePh': 'VD: HÀ NỘI',
'wm.manual': 'NHẬP TOẠ ĐỘ',
'wm.lat': 'Vĩ độ',
'wm.lng': 'Kinh độ',
'wm.hasExif': 'Đã đọc GPS từ ảnh.',
'wm.noExif': 'Ảnh không có GPS — nhập tay hoặc lấy vị trí hiện tại.',
'wm.useLocation': 'DÙNG VỊ TRÍ CỦA TÔI',
'wm.locating': 'ĐANG LẤY VỊ TRÍ…',
'wm.hasDevice': 'GPS lấy từ vị trí hiện tại của thiết bị.',
'wm.hasManual': 'GPS do bạn nhập tay.',
'wm.locFail': 'Không lấy được vị trí — hãy nhập tay.',
'auth.loginTitle': 'Đăng nhập',
'auth.signupTitle': 'Tạo tài khoản',
'auth.email': 'Email',
'auth.password': 'Mật khẩu',
'auth.passwordHint': 'Ít nhất 8 ký tự',
'auth.noAccount': 'Chưa có tài khoản? Đăng ký',
'auth.haveAccount': 'Đã có tài khoản? Đăng nhập',
'auth.continueDemo': 'Tiếp tục dùng thử',
'auth.busy': 'Đang xử lý…',
'auth.loggedInAs': 'Đã đăng nhập: {email}',
'auth.verifyTitle': 'Xác thực email',
'auth.verifyBody': 'Đã gửi một liên kết xác thực tới {email}. Mở liên kết đó để dùng được các tính năng PRO.',
'auth.verifyProHint': 'Khi chưa xác thực, tài khoản dùng y như khách: ảnh xuất ra tối đa 2048px kèm watermark và không lưu được gì.',
'auth.verifySent': 'Đã gửi lại email xác thực.',
'auth.verifiedDone': 'Tôi đã xác thực xong',
'auth.signupVerifyHint': 'Sau khi đăng ký, hệ thống gửi một email xác thực — mở liên kết trong đó để bật các tính năng PRO.',
// The PRO gate. Signed in but unproven is served as a guest, so the studio
// needs one place that says why and one way to ask for the letter again.
'pro.verifyChip': 'XÁC THỰC EMAIL ĐỂ DÙNG PRO',
'pro.unverifiedBar': 'Tài khoản {email} chưa xác thực email — đang dùng như khách.',
'pro.resend': 'GỬI LẠI EMAIL XÁC THỰC',
'pro.resent': 'ĐÃ GỬI',
'save.title': 'Lưu công thức',
'save.name': 'Tên công thức',
'save.recent': 'LƯU THAY ĐỔI VÀO RECIPES',
'save.recentHint': 'Lưu look đang đứng trên màn hình — sim, WB, light, FX và frame — thành một recipe riêng của tài khoản này.',
'save.recentEmpty': 'Chưa có recipe nào của riêng bạn.',
'save.delete': 'Xoá recipe này',
'name.photoTitle': 'ĐẶT TÊN ẢNH',
'name.photoLabel': 'Tên lưu trong thư mục ảnh, và là tiêu đề trên khung ảnh ở landing page.',
'name.recipeTitle': 'LƯU VÀO RECIPES',
'name.recipeLabel': 'Tên cho look này, chỉ trong tài khoản của bạn.',
'name.required': 'Đặt tên trước đã.',
'name.duplicate': 'Bạn đã có một recipe mang tên này.',
'name.save': 'LƯU',
'name.cancel': 'HUỶ',
'save.saved': 'Đã lưu "{name}"',
'save.needLogin': 'Đăng nhập để lưu công thức.',
'save.photo': 'LƯU ẢNH {n}/{max}',
'save.photoNeedLogin': 'Đăng nhập để lưu ảnh.',
'save.photoSaved': 'Đã lưu ảnh {n}/{max}.',
'save.photoFull': 'Đã đủ {max} ảnh — xoá bớt trong Ảnh của tôi.',
'save.photoNoMax': 'LƯU ẢNH {n}',
'save.photoNoSource': 'Chưa có ảnh nào để lưu.',
'save.photoFailed': 'Không lưu được ảnh: {msg}',
'myp.empty': 'CHƯA LƯU ẢNH NÀO.',
'myp.history': 'LỊCH SỬ',
'myp.historyEmpty': 'CHƯA CÓ MỐC LƯU NÀO TRƯỚC ĐÓ.',
'myp.consent': 'Cho phép ảnh này lên landing page',
'myp.remove': 'XOÁ ẢNH',
'del.title': 'XOÁ ẢNH NÀY',
'del.warning': 'Xoá ảnh này sẽ xoá tất cả history đã lưu trước đó ({n} look) và không khôi phục được. Cần tải ảnh về trước khi xoá.',
'del.download': 'TẢI VỀ TRƯỚC',
'del.confirm': 'XOÁ ẢNH',
'del.cancel': 'HUỶ',
// Xác nhận của CLEAR: giữ lại thành quả theo một trong hai cách, hoặc bỏ hẳn.
'clear.title': 'Xoá ảnh đang mở?',
'clear.hint':
'Ảnh sẽ rời khỏi màn hình làm việc. Lưu vào ẢNH CỦA TÔI để lần sau sửa tiếp, xuất ra tệp ảnh, hoặc xoá mà không giữ lại gì.',
'clear.save': 'LƯU ẢNH',
'clear.export': 'XUẤT ẢNH',
'clear.discard': 'XOÁ KHÔNG LƯU',
'clear.cancel': 'HUỶ',
// Xuất ảnh: kích thước đích. Mức lớn hơn cạnh dài nhất của ảnh đang sửa được
// model phóng to ngay trên máy (engine/superRes), nên tệp ra đúng số pixel đã hỏi.
'export.title': 'Xuất ảnh',
'export.hint':
'Cạnh dài nhất của ảnh đang sửa: {px}px. Mức lớn hơn sẽ được phóng to ngay trên máy này.',
'export.same': 'XUẤT KHÔNG ĐỔI',
'export.2k': 'XUẤT 2K (2048px)',
'export.4k': 'XUẤT 4K (4096px)',
'export.customLabel': 'Cạnh dài nhất (px)',
'export.custom': 'XUẤT',
'export.cap': 'Bản khách giới hạn ở {px}px.',
'photo.uploading': 'ĐANG TẢI…',
'photos.title': 'Ảnh của tôi',
'photos.hint': 'Mỗi tài khoản giữ tối đa {max} ảnh. Ảnh lưu từ studio nằm ở đây và xuất hiện trên dải phim cộng đồng.',
'photos.count': '{n}/{max} ảnh',
'photos.countNoMax': '{n} ảnh',
'photos.hintNoMax':
'Tài khoản admin không giới hạn số ảnh. Ảnh lưu từ studio nằm ở đây và xuất hiện trên dải phim cộng đồng.',
'photos.add': 'THÊM ẢNH',
'photos.empty': 'Chưa có ảnh nào. Mở studio rồi bấm LƯU ẢNH, hoặc thêm ảnh ngay ở đây.',
'photos.full': 'Đã đủ {max} ảnh. Xoá bớt trước khi lưu tiếp.',
'photos.delete': 'XOÁ',
'photos.deleted': 'Đã xoá ảnh.',
'photos.failed': 'Không thực hiện được: {msg}',
'photos.noLabels': 'Chưa có nhãn dải phim',
'adm.title': 'Quản trị dải phim',
'adm.subtitle': 'Ảnh do người dùng đóng góp. Xoá một ảnh để gỡ nó khỏi trang chủ.',
'adm.upload': 'THÊM ẢNH',
'adm.uploads': 'Ảnh đã tải lên',
'adm.sections': 'Mục trên landing',
'adm.albumAll': 'Tất cả ảnh',
'adm.filterName': 'Lọc theo tên người dùng',
'adm.sort': 'Sắp xếp',
'adm.sortNewest': 'Mới nhất trước',
'adm.sortRecipe': 'Công thức A→Z',
'adm.sortRating': 'Xếp hạng cao nhất',
'adm.rating': 'Xếp hạng',
'adm.ratingAll': 'Mọi mức xếp hạng',
'adm.ratingAtLeast': 'Từ {n} sao',
'adm.recipe': 'Công thức',
'adm.bigEmpty': 'Chọn một ảnh để xem.',
'adm.pickStrip': 'Dải phim',
'adm.pickTester': 'Live preset tester',
'adm.pickCreator': 'Custom recipe creator',
'adm.pickQr': 'Thẻ QR',
'adm.noPreset': 'không có look để chia sẻ',
'adm.rowEmpty': 'Chưa có gì ở đây.',
'adm.slotHint': 'Tích vào những mục ảnh sẽ xuất hiện — một ảnh có thể nằm cùng lúc ở nhiều mục, và mỗi mục bốc ngẫu nhiên một ảnh trong nhóm của nó mỗi lần ghé. Bỏ tích hết sẽ gỡ ảnh khỏi landing mà không xoá — chủ ảnh vẫn giữ nó trong MY PHOTOS. Mục QR là mục duy nhất phát công thức: thẻ của nó vẽ một mã tải chính tệp .recipe của ảnh để app trên điện thoại quét vào.',
'adm.signIn': 'Đăng nhập bằng tài khoản quản trị',
'adm.notAdmin': 'Tài khoản này không có quyền quản trị.',
'adm.empty': 'Chưa có ảnh đóng góp nào.',
'adm.total': '{n} ảnh',
'adm.owner': 'Người đăng',
'adm.size': 'Dung lượng',
'adm.uploaded': 'Thời điểm',
'adm.delete': 'XOÁ',
'adm.clearAll': 'XOÁ TẤT CẢ',
'adm.clearConfirm': 'Xoá toàn bộ {n} ảnh đóng góp? Không hoàn tác được.',
'adm.removed': 'Đã xoá {n} ảnh.',
'adm.back': 'Về trang chủ',
'adm.settings': 'CÀI ĐẶT',
'adm.tabPictures': 'Ảnh',
'adm.tabUsers': 'Người dùng',
'adm.tabProfile': 'Hồ sơ',
'adm.close': 'Đóng',
'adm.userPicture': 'Ảnh',
'adm.userName': 'Tên',
'adm.userEmail': 'Email',
'adm.userRole': 'Quyền',
'adm.userPhotos': 'Số ảnh',
'adm.userJoined': 'Ngày tạo',
'adm.roleAdmin': 'Quản trị',
'adm.roleMember': 'Thành viên',
'adm.viewPhotos': 'XEM ẢNH',
'adm.noUsers': 'Chưa có tài khoản nào.',
'adm.profileHint': 'Ảnh đại diện, email và mật khẩu của tài khoản đang đăng nhập. Đổi email hoặc mật khẩu cần mật khẩu hiện tại.',
'adm.changeAvatar': 'ĐỔI ẢNH ĐẠI DIỆN',
'adm.currentPassword': 'Mật khẩu hiện tại',
'adm.newEmail': 'Email mới',
'adm.newPassword': 'Mật khẩu mới',
'adm.save': 'LƯU',
'adm.saved': 'Đã lưu hồ sơ.',
'adm.userActions': 'Thao tác',
'adm.userBlock': 'KHOÁ',
'adm.userUnblock': 'MỞ KHOÁ',
'adm.userRemove': 'GỠ',
'adm.userRestore': 'KHÔI PHỤC',
'adm.userDelete': 'XOÁ HẲN',
'adm.userBlocked': 'Đã khoá',
'adm.userRemoved': 'Đã gỡ',
'adm.userDeleteConfirm': 'Xoá vĩnh viễn tài khoản {email} cùng toàn bộ ảnh và công thức của nó? Không hoàn tác được.',
'adm.selectAll': 'CHỌN TẤT CẢ',
'adm.selectNone': 'BỎ CHỌN',
'adm.deleteSelected': 'XOÁ ĐÃ CHỌN ({n})',
'adm.userDeleteSelectedConfirm': 'Xoá vĩnh viễn {n} tài khoản đã chọn cùng toàn bộ ảnh và công thức của chúng? Không hoàn tác được.',
'adm.tabStats': 'THỐNG KÊ',
// The traffic screen. Bucket names ('/app', a control's data-key, a browser
// version) are machine values and stay as they are.
'stats.days': '{n} NGÀY',
'stats.hint': 'Lưu lượng trong khoảng đã chọn.',
'stats.views': 'LƯỢT XEM',
'stats.clicks': 'LƯỢT NHẤN',
'stats.visitors': 'KHÁCH TRUY CẬP',
'stats.timeline': 'LƯU LƯỢNG THEO NGÀY',
'stats.peak': 'đỉnh',
'stats.pages': 'TRANG ĐƯỢC XEM',
'stats.targets': 'TÍNH NĂNG ĐƯỢC NHẤN',
'stats.countries': 'QUỐC GIA',
'stats.regions': 'VÙNG MIỀN',
'stats.cities': 'THÀNH PHỐ',
'stats.browsers': 'TRÌNH DUYỆT',
'stats.systems': 'HỆ ĐIỀU HÀNH',
'stats.devices': 'THIẾT BỊ',
'stats.empty': 'Chưa có dữ liệu.',
'profile.title': 'Hồ sơ của tôi',
'err.noWebgl':
'Trình duyệt không cấp được WebGL — bản dựng này cần WebGL để xử lý ảnh (chạy CPU chậm gấp hàng nghìn lần).',
'err.decode': 'Không đọc được ảnh này. Thử JPG hoặc PNG khác.',
'err.generic': 'Có lỗi xảy ra: {msg}',
'err.offline': 'Không gọi được máy chủ.',
'meta.exported': 'Đã tải {name} ({kb}KB, 300 ppi)',
} as const;
// `vi` is `as const` so the key set is exact; `Dict` widens the values to
// string, which is what lets `en.ts` hold different text under the same keys
// while a missing or misspelled key stays a typecheck error.
export type Dict = { [K in keyof typeof vi]: string };
export type MsgKey = keyof Dict;