Merge PR #1484: Add Vietnamese interface translation

Conflicts:
- crates/ui-egui/src/i18n.rs, crates/ui-egui/src/menus.rs: language table and commands keep every language plus vi
- docs/localization.md: language list, note links and table excerpt keep every language plus vi
- docs/localization-parity.md: header keeps the newer date and both changes; the newer per-language measurements are kept, Vietnamese row from the PR; links and revision rows are the union
This commit is contained in:
aiongg
2026-10-22 02:38:28 +00:00
7 changed files with 2748 additions and 9 deletions
+251
View File
@@ -0,0 +1,251 @@
{
" ({unsure} with several look-alike files: use Locate)": " ({unsure} có nhiều tệp trông giống nhau: dùng Định vị)",
" ({} in Recently Deleted)": " ({} trong Đã xóa gần đây)",
" · {:.0}% clipped": " · {:.0}% bị cắt",
" · {failed} couldn't be rendered": " · {failed} không thể kết xuất",
" · {queued} waiting": " · {queued} đang chờ",
" · {skipped} skipped (file exists)": " · {skipped} đã bỏ qua (tệp đã tồn tại)",
" · {} duplicate{} skipped": " · {} bản trùng{:.0} đã bỏ qua",
" · {} failed: {}": " · {} thất bại: {}",
" · {} moved": " · {} đã di chuyển",
" · {} not readable": " · {} không đọc được",
" · {} original{} left at the source": " · {} tệp gốc{:.0} còn ở nguồn",
" · {} restored from Recently Deleted": " · {} đã khôi phục từ Đã xóa gần đây",
" — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One không nhập danh sách có từ khóa chứa ; , < hoặc >: {names}",
" — left out, as a keyword list can't hold them: {names}": " — bỏ qua vì danh sách từ khóa không chứa được: {names}",
" — not carried over: {}{}": " — không được chuyển sang: {}{}",
"+{n} more": "+{n} nữa",
"+{} more": "+{} nữa",
", {failed} file{} not readable": ", {failed} tệp{:.0} không đọc được",
", {} label": ", nhãn {}",
", {} star{}": ", {} sao{:.0}",
"1 task is still running: {names}. Quitting stops it.": "Còn 1 tác vụ đang chạy: {names}. Thoát sẽ dừng tác vụ.",
"; {kept} already had a location": "; {kept} đã có vị trí",
"; {n} gained synonyms": "; {n} có thêm từ đồng nghĩa",
"; {outside} outside its time range": "; {outside} ngoài khoảng thời gian của tuyến",
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}": "@{}: {} PR, {} commit, +{} / −{} dòng (Δ {}), +{} / −{} tài nguyên nhị phân, {} – {}",
"Add {local_n} to My Photos": "Thêm {local_n} vào Ảnh của tôi",
"Add {} Photo{}": "Thêm {} ảnh{:.0}",
"Add {} Photo{}, restore {}": "Thêm {} ảnh{:.0}, khôi phục {}",
"Add “{}” to Favorites": "Thêm «{}» vào Yêu thích",
"Added {n} keyword{}": "Đã thêm {n} từ khóa{:.0}",
"Added {n} photo{} to My Photos": "Đã thêm {n} ảnh{:.0} vào Ảnh của tôi",
"Added {n} photo{} to “{}”": "Đã thêm {n} ảnh{0:.0} vào «{1}»",
"Added {what} to {name}": "Đã thêm {what} vào {name}",
"Added “{keyword}” to {n} photo{}": "Đã thêm «{keyword}» vào {n} ảnh{:.0}",
"Adding photos… {} of {}": "Đang thêm ảnh… {} / {}",
"All {n} selected photos move by the same amount.": "Tất cả {n} ảnh đã chọn dịch chuyển cùng một lượng.",
"Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Đã có trong thư viện, trong Đã xóa gần đây: đã chọn {} ảnh{:.0} · nhấp chuột phải → Khôi phục, hoặc Xóa vĩnh viễn để nhập lại",
"Already in the library: {} photo{} selected in All Photos": "Đã có trong thư viện: đã chọn {} ảnh{:.0} trong Tất cả ảnh",
"Auto ({})": "Tự động ({})",
"Auto Import: added {} photo{}": "Tự động nhập: đã thêm {} ảnh{:.0}",
"Auto Sync: edits apply to {n} photos": "Đồng bộ tự động: chỉnh sửa áp dụng cho {n} ảnh",
"Automatic ({} MB)": "Tự động ({} MB)",
"Build 1:1 Previews ({scope})": "Tạo xem trước 1:1 ({scope})",
"Build Smart Previews ({scope})": "Tạo xem trước thông minh ({scope})",
"Build Standard-Sized Previews ({scope})": "Tạo xem trước cỡ chuẩn ({scope})",
"Building previews for {} photos…": "Đang tạo xem trước cho {} ảnh…",
"Click {}:1": "Nhấp {}:1",
"Couldn't open the editor: {e}": "Không thể mở trình chỉnh sửa: {e}",
"Create Keyword Tag Inside “{name}”…": "Tạo từ khóa trong «{name}»…",
"Create {n} Virtual Copies": "Tạo {n} bản sao ảo",
"Creates {} stacks from {} of {scope}": "Tạo {} chồng từ {} trong {scope}",
"Decoder: {reason}": "Bộ giải mã: {reason}",
"Delete \"{label}\"": "Xóa «{label}»",
"Delete \"{label}\" (and the mask)": "Xóa «{label}» (và mặt nạ)",
"Delete the keyword “{name}”?": "Xóa từ khóa «{name}»?",
"Delete {n} Photos": "Xóa {n} ảnh",
"Delete “{current}”": "Xóa «{current}»",
"Denoising {name}: {done} of {total} tiles{}": "Khử nhiễu {name}: {done} / {total} ô{}",
"Discard Smart Previews ({scope})": "Hủy xem trước thông minh ({scope})",
"Done ({what}): {done} photo{plural}": "Xong ({what}): {done} ảnh{plural:.0}",
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Tải từ {host}. Sau khi tải xong và kiểm tra, LightCraft cài đặt, {after}.",
"Duplicate \"{label}\"": "Nhân bản «{label}»",
"Example: {example}": "Ví dụ: {example}",
"Exported {n} keyword{}": "Đã xuất {n} từ khóa{:.0}",
"Exported {n} photo{}": "Đã xuất {n} ảnh{:.0}",
"Exported {ok} of {total} photo{}": "Đã xuất {ok} / {total} ảnh{:.0}",
"Exported {} point curve preset{}": "Đã xuất {} thiết lập sẵn đường cong điểm{:.0}",
"Exported {} preset{}": "Đã xuất {} thiết lập sẵn{:.0}",
"Exporting {} of {total}": "Đang xuất {} / {total}",
"Filter bar — {active} active filter{}": "Thanh lọc — {active} bộ lọc đang bật{:.0}",
"Filtered: {label}": "Đã lọc: {label}",
"Focus {:.0}": "Tiêu điểm {:.0}",
"Folder “{}” (and its subfolders)": "Thư mục «{}» (và thư mục con)",
"Found {n} missing photo{}; {left} still missing{unsure}": "Tìm thấy {n} ảnh thiếu{:.0}; còn thiếu {left}{unsure}",
"Healed {n} dust spot{}": "Đã xử lý {n} vết bụi{:.0}",
"Import cancelled · {} photo{} added": "Đã hủy nhập · đã thêm {} ảnh{:.0}",
"Import failed: {e}": "Nhập thất bại: {e}",
"Import warnings ({n}):\n{warnings}": "Cảnh báo khi nhập ({n}):\n{warnings}",
"Imported {n} point curve preset{}": "Đã nhập {n} thiết lập sẵn đường cong điểm{:.0}",
"Imported {n} preset{}": "Đã nhập {n} thiết lập sẵn{:.0}",
"Imported {n} preset{}, {f} file{} not readable": "Đã nhập {n} thiết lập sẵn{:.0}, {f} tệp{:.0} không đọc được",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "Đã nhập {profiles} hồ sơ{:.0} (Trình duyệt hồ sơ → nhóm của chúng)",
"Imported {} photo{}": "Đã nhập {} ảnh{:.0}",
"In line for AI Denoise ({ahead} ahead)…": "Đang xếp hàng Khử nhiễu AI (trước bạn: {ahead})…",
"Insert at the cursor (also written {})": "Chèn tại con trỏ (cũng ghi là {})",
"It is taken off {count} photo{}, with the keywords below it. Undo brings it back.": "Sẽ gỡ khỏi {count} ảnh{:.0}, kèm từ khóa con. Hoàn tác sẽ khôi phục.",
"Label Set: {name}": "Bộ nhãn: {name}",
"LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft chưa giải mã được biến thể raw này ({}). Bạn đang chỉnh JPEG xem trước nhúng của máy ảnh, đã gồm style ảnh (ví dụ Monochrome) và cân bằng trắng.",
"LightCraft could not open its window: {e}": "LightCraft không thể mở cửa sổ: {e}",
"LightCraft could not open its window: {e}\n\nThe log file has the details: {path}": "LightCraft không thể mở cửa sổ: {e}\n\nChi tiết trong tệp nhật ký: {path}",
"Lightroom catalog inspected: {photos} photos, {collections} collections, {missing} missing": "Đã kiểm tra catalog Lightroom: {photos} ảnh, {collections} bộ sưu tập, {missing} thiếu",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "Hình học Lightroom cho {path} không được giữ; biến đổi Guided Upright đã lưu không được hỗ trợ.",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "Hình học Lightroom cho {path} không được giữ; biến đổi Upright đã lưu không được hỗ trợ.",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Hình học Lightroom cho {path} không được giữ; biến đổi đã lưu bị bỏ qua vì Upright đang tắt.",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Hình học Lightroom cho {path} không được giữ; {mode} sẽ được tính lại.",
"Lightroom import complete: {imported} photos added": "Nhập Lightroom xong: đã thêm {imported} ảnh",
"Lightroom import complete: {imported} photos added, {failed} failed": "Nhập Lightroom xong: đã thêm {imported} ảnh, {failed} thất bại",
"Lightroom import complete: {imported} photos added; index warning: {warning}": "Nhập Lightroom xong: đã thêm {imported} ảnh; cảnh báo chỉ mục: {warning}",
"Lightroom import failed: {e}{warning}": "Nhập Lightroom thất bại: {e}{warning}",
"Lightroom inspection failed: {e}{warning}": "Kiểm tra Lightroom thất bại: {e}{warning}",
"MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0. Phông: {} (OFL). Biểu tượng: tác phẩm gốc.",
"Making the denoised picture… {}%": "Đang tạo ảnh đã khử nhiễu… {}%",
"Matches: {}": "Khớp: {}",
"Merging… {stage} {:.0}%": "Đang ghép… {stage} {:.0}%",
"Move {what} to Recently Deleted?": "Chuyển {what} vào Đã xóa gần đây?",
"Moved {} photo(s)": "Đã di chuyển {} ảnh",
"Moved; {} photo(s) relinked": "Đã di chuyển; liên kết lại {} ảnh",
"Named ({named_n})": "Có tên ({named_n})",
"New Folder in “{name}”": "Thư mục mới trong «{name}»",
"On {have} of {of} selected photos": "Trên {have} / {of} ảnh đã chọn",
"Painting “{k}”: click photos to add or remove it · Esc stops": "Gán «{k}»: nhấp ảnh để thêm hoặc gỡ · Esc dừng",
"Paste into {n} photo{} — only settings that were copied are pasted": "Dán vào {n} ảnh{:.0} — chỉ dán các thiết lập đã sao chép",
"Preset: {name}": "Thiết lập sẵn: {name}",
"Preview only — editing the camera's embedded JPEG ({})": "Chỉ xem trước — chỉnh sửa JPEG nhúng của máy ảnh ({})",
"Preview only — {}": "Chỉ xem trước — {}",
"Previews ready for {done} photo{plural}": "Xem trước sẵn sàng cho {done} ảnh{plural:.0}",
"Profile: {}": "Hồ sơ: {}",
"Proof Copy ({})": "Bản sao thử ({})",
"Proof Preview · {}": "Xem trước thử · {}",
"Put inside “{parent}”": "Đặt trong «{parent}»",
"Quick Develop · {n} photos": "Phát triển nhanh · {n} ảnh",
"Quick Develop: {name}": "Phát triển nhanh: {name}",
"Rated {}": "Đã xếp hạng {}",
"Reading Lightroom catalog… {done} of {total}": "Đang đọc catalog Lightroom… {done} / {total}",
"Reading photos… {done} of {total}": "Đang đọc ảnh… {done} / {total}",
"Reading photos… {} of {}": "Đang đọc ảnh… {} / {}",
"Redo {}": "Làm lại {}",
"Remove “{name}”": "Gỡ «{name}»",
"Remove “{name}” and its {count} photo{} from the library?": "Gỡ «{name}» và {count} ảnh{:.0} khỏi thư viện?",
"Remove “{}” from Favorites": "Gỡ «{}» khỏi Yêu thích",
"Removed from {}": "Đã gỡ khỏi {}",
"Removed {what} from {name}": "Đã gỡ {what} khỏi {name}",
"Rename {n} Photos…": "Đổi tên {n} ảnh…",
"Rename “{name}”": "Đổi tên «{name}»",
"Renames “{from}” on {n} photo{} (keywords below it too). Use | for levels, e.g. Travel|Italy. An existing name merges the two.": "Đổi tên «{from}» trên {n} ảnh{:.0} (cả từ khóa con). Dùng | cho cấp, ví dụ Du lịch|Ý. Tên đã có sẽ gộp hai mục.",
"Rendering on the CPU: {why}": "Đang kết xuất trên CPU: {why}",
"Replace {} with:": "Thay {} bằng:",
"Reset {name}": "Đặt lại {name}",
"Reset {} Channel": "Đặt lại kênh {}",
"Restore {}": "Khôi phục {}",
"Restore {} Photo{}": "Khôi phục {} ảnh{:.0}",
"Restored {} photo{} from Recently Deleted": "Đã khôi phục {} ảnh{:.0} từ Đã xóa gần đây",
"Running on the processor: {}.": "Chạy trên bộ xử lý: {}.",
"Running on {}.": "Chạy trên {}.",
"Saved export preset “{}”": "Đã lưu thiết lập sẵn xuất «{}»",
"Scores {scope} for focus and exposure and finds similar shots taken within seconds of each other (bursts).": "Chấm điểm {scope} theo tiêu điểm và phơi sáng, tìm ảnh tương tự chụp trong vài giây (burst).",
"Show {hidden} hidden location{}": "Hiện {hidden} vị trí ẩn{:.0}",
"Showing {n} of {}": "Hiển thị {n} / {}",
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Hiện điều khoản mô hình, rồi tải {} từ {host}, cài và bắt đầu dùng. Không gửi gì khác.",
"Smart album: {}": "Album thông minh: {}",
"Tagged {n} photo{} from the tracklog": "Đã gắn thẻ {n} ảnh{:.0} từ nhật ký tuyến",
"The SAM 3 download failed: {e}": "Tải SAM 3 thất bại: {e}",
"There is a keyword set “{name}” already": "Đã có bộ từ khóa «{name}»",
"There is a “{name}” at the top level already. Merge the two?": "Đã có «{name}» ở cấp cao nhất. Gộp hai mục?",
"Trained on: {provenance}": "Huấn luyện trên: {provenance}",
"Undo {}": "Hoàn tác {}",
"Unknown tag{s} {} stay{} as typed (see Tags)": "Thẻ{s:.0} không xác định {} giữ nguyên như đã gõ{:.0} (xem Thẻ)",
"Updated “{name}”": "Đã cập nhật «{name}»",
"Version {} — a clean-room, pure-Rust photo library and raw developer.": "Phiên bản {} — thư viện ảnh và trình phát triển raw thuần Rust, clean-room.",
"Version: {}": "Phiên bản: {}",
"Virtual copy “{name}” of {of}": "Bản sao ảo «{name}» của {of}",
"Working on {what} for {} photos…": "Đang xử lý {what} cho {} ảnh…",
"XMP adjustments were not imported for {path}: {fields}.": "Điều chỉnh XMP không được nhập cho {path}: {fields}.",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Hồ sơ XMP «{profile}» không được áp dụng cho {path}; màu có thể khác Lightroom.",
"none (CPU rendering): {why}": "không (kết xuất CPU): {why}",
"not added to Apple Photos: {e}": "không thêm được vào Apple Photos: {e}",
"the {n} selected photos": "{n} ảnh đã chọn",
"the {} photos in view": "{} ảnh đang hiển thị",
"these {count} photos": "{count} ảnh này",
"{:.1} MB": "{:.1} MB",
"{count} smart preview{} · {:.1} MB": "{count} xem trước thông minh{:.0} · {:.1} MB",
"{done} of {total}": "{done} / {total}",
"{done} of {total} MB": "{done} / {total} MB",
"{file} · {size}": "{file} · {size}",
"{f} folders (and their subfolders)": "{f} thư mục (và thư mục con)",
"{label} Grading": "Phân cấp màu {label}",
"{label} {d:+} on every selected photo": "{label} {d:+} trên mọi ảnh đã chọn",
"{label} {index} of {n}": "{label} {index} / {n}",
"{label}, {n} photos": "{label}, {n} ảnh",
"{matching} of {t} photos": "{matching} / {t} ảnh",
"{matching} photos": "{matching} ảnh",
"{mode} spot {} of {}": "Vết {mode} {} / {}",
"{month}": "Tháng {month}",
"{month} {year}": "Tháng {month} năm {year}",
"{msg} ({failed} failed)": "{msg} ({failed} thất bại)",
"{name} Label": "Nhãn {name}",
"{name} Off": "{name} tắt",
"{name} On": "{name} bật",
"{name} opened for editing; it is stacked with the original": "{name} đã mở để chỉnh sửa; được xếp chồng với bản gốc",
"{name} opened in the other editor": "Đã mở {name} trong trình chỉnh sửa khác",
"{name} · {terms}": "{name} · {terms}",
"{name} — double-click to reset it": "{name} — nhấp đúp để đặt lại",
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Đã bỏ qua {n} ảnh cục bộ{0:.0}: album chỉ chứa ảnh thư viện (hãy thêm vào Ảnh của tôi trước)",
"{n} added to Apple Photos": "Đã thêm {n} vào Apple Photos",
"{n} added to Apple Photos album “{album}”": "Đã thêm {n} vào album Apple Photos «{album}»",
"{n} change{} couldn't be written to disk: {e}\nIf you quit now, {} lost.": "Không ghi được {n} thay đổi{0:.0} lên đĩa: {e}\nNếu thoát ngay, sẽ mất.{1:.0}",
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "Đã lưu {n} thay đổi{:.0} trong bộ nhớ nhưng chưa ghi đĩa: {e}\nLightCraft sẽ thử lại tự động; thoát ngay sẽ mất.{:.0}",
"{n} change{} saved in memory but not written to disk: {e} — LightCraft will retry": "Đã lưu {n} thay đổi{:.0} trong bộ nhớ nhưng chưa ghi đĩa: {e} — LightCraft sẽ thử lại",
"{n} found · {sel} selected": "Tìm thấy {n} · đã chọn {sel}",
"{n} originals opened in the other editor": "Đã mở {n} bản gốc trong trình chỉnh sửa khác",
"{n} photos": "{n} ảnh",
"{n} photos in {} are hidden by the filters above": "{n} ảnh trong «{}» bị ẩn bởi bộ lọc phía trên",
"{n} photos opened for editing; each is stacked with its original": "Đã mở {n} ảnh để chỉnh sửa; mỗi ảnh xếp chồng với bản gốc",
"{n} photo{}": "{n} ảnh{:.0}",
"{n} photo{} match · updates automatically as photos change": "{n} ảnh{:.0} khớp · tự cập nhật khi ảnh thay đổi",
"{n} photo{} now · updates automatically as photos change": "Hiện {n} ảnh{:.0} · tự cập nhật khi ảnh thay đổi",
"{n} similar photo{} · View ▸ Clear Filters to see all": "{n} ảnh tương tự{:.0} · Xem → Xóa bộ lọc để thấy tất cả",
"{n} tasks are still running: {names}. Quitting stops them.": "Còn {n} tác vụ đang chạy: {names}. Thoát sẽ dừng chúng.",
"{n} {noun}": "{noun}: {n}",
"{pct} %": "{pct} %",
"{pid} (missing)": "{pid} (thiếu)",
"{sel_n} selected · {counted}": "Đã chọn {sel_n} · {counted}",
"{size} GB": "{size} GB",
"{stage}… {:.0}%": "{stage}… {:.0}%",
"{verb} {n} Photo{}": "{verb} {n} ảnh{:.0}",
"{weekday}, {day}": "{weekday}, {day}",
"{weekday}, {day} {month} {year}": "{weekday}, {day}/{month}/{year}",
"{what} merge added": "Đã thêm ghép {what}",
"{what} merge cancelled": "Đã hủy ghép {what}",
"{what} merge failed: {e}": "Ghép {what} thất bại: {e}",
"{year}": "{year}",
"{{date:…}} directives: {}": "Chỉ thị {{date:…}}: {}",
"{} KB": "{} KB",
"{} MB": "{} MB",
"{} and higher": "{} trở lên",
"{} contributors · {} commits": "{} người đóng góp · {} commit",
"{} failed": "{} thất bại",
"{} files": "{} tệp",
"{} files and {f} folder{}": "{} tệp và {f} thư mục{:.0}",
"{} item{}": "{} mục{:.0}",
"{} label (click more labels to show any of them)": "Nhãn «{}» (nhấp thêm nhãn để hiện ảnh có bất kỳ nhãn nào)",
"{} of 4 guides — drag along lines that should be vertical or horizontal.": "{} / 4 đường dẫn — kéo dọc các đường cần thẳng đứng hoặc nằm ngang.",
"{} of {} from {host}": "{} / {} từ {host}",
"{} photos": "{} ảnh",
"{} photos · exposures {} EV": "{} ảnh · phơi sáng {} EV",
"{} photos · {} · {:.0}° × {:.0}°": "{} ảnh · {} · {:.0}° × {:.0}°",
"{} photo{} not readable": "{} ảnh{:.0} không đọc được",
"{} sample{}": "{} mẫu{:.0}",
"{} sec": "{} giây",
"{} spot(s) on this photo": "{} vùng trên ảnh này",
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Giữ trong thư mục «denoise» của thư viện; khi đạt giới hạn, xóa trước bản cũ nhất không ảnh nào dùng.",
"{} — show their photos": "{} — hiện ảnh của họ",
"· {dups} already in the library": "· {dups} đã có trong thư viện",
"· {in_source} identical copies": "· {in_source} bản sao giống hệt",
"· {n} photo{}": "· {n} ảnh{:.0}",
"· {trashed} in Recently Deleted": "· {trashed} trong Đã xóa gần đây",
"“{parent}” has a “{name}” already. Merge the two?": "«{parent}» đã có «{name}». Gộp hai mục?",
"• {assumption}": "• {assumption}",
"… and {more} more": "… và {more} nữa"
}
File diff suppressed because it is too large Load Diff
+184
View File
@@ -133,6 +133,7 @@ language_table! {
Sv, "sv", "Svenska", "Latn", include_str!("../locales/sv.json");
Fi, "fi", "Suomi", "Latn", include_str!("../locales/fi.json");
Id, "id", "Bahasa Indonesia", "Latn", include_str!("../locales/id.json");
Vi, "vi", "Tiếng Việt", "Latn", include_str!("../locales/vi.json");
}
// The settings file stores the BCP-47 code (`"zh-hans"`), never the Rust variant name, so a
@@ -737,6 +738,9 @@ mod tests {
for tag in ["es", "es-ES", "es_MX.UTF-8", "es-419"] {
assert_eq!(Locale::parse_tag(tag), Some(Locale::Es), "{tag}");
}
for tag in ["vi", "vi-VN", "vi_VN", "vi_VN.UTF-8"] {
assert_eq!(Locale::parse_tag(tag), Some(Locale::Vi), "{tag}");
}
assert_eq!(Locale::parse_tag("xx"), None);
}
@@ -911,6 +915,7 @@ mod tests {
("app.language.swedish", Locale::Sv),
("app.language.finnish", Locale::Fi),
("app.language.indonesian", Locale::Id),
("app.language.vietnamese", Locale::Vi),
];
// One command per language, and every command reachable from the menu table.
assert_eq!(commands.len(), Locale::ALL.len());
@@ -1825,4 +1830,183 @@ mod tests {
assert_eq!(history_label("Preset: Warm Glow", &app.session.presets), "預設集:Warm Glow");
set_language(Locale::En);
}
/// English catalog values Vietnamese keeps on purpose. Grouped by reason:
/// standards/acronyms (`ISO`, `HDR`, …), product names (`LightCraft`, `PhotoCraft`),
/// file-type tokens (`Raw`/`raw`, `video`), look names left as established labels
/// (`Bleach Bypass`, `Vintage`), and machine ids that must stay byte-identical
/// (`publicDomain`, `denoiseModel.accept`). `Album` stays as the common loanword.
/// Every other required label must differ from its English source.
const VI_INTENTIONAL_ENGLISH: &[&str] = &[
"OK",
"ISO",
"RGB",
"HDR",
"JPEG",
"PNG",
"TIFF",
"WebP",
"AVIF",
"HEIF",
"HEIC",
"DNG",
"ICC",
"EXIF",
"XMP",
"GPU",
"MCP",
"SAM",
"RAW",
"Raw",
"raw",
"B&W",
"LUT",
"LightCraft",
"ArtCraft",
"GitHub",
"Discord",
"English",
"日本語",
"繁體中文(台灣)",
"Display P3",
"Bleach Bypass",
"Cross Process",
"HDR…",
"video",
"Video",
"true",
"false",
"Album",
"Vintage",
"publicDomain",
"denoiseModel.accept",
"Panorama",
"Panorama…",
"PhotoCraft",
"SDR",
"Matrix/TRC",
"LUT-based",
];
/// Vietnamese covers every current catalog message and every required display label.
#[test]
fn vietnamese_covers_catalogs_commands_controls_and_rules() {
let catalog = Locale::Vi.catalog();
assert!(!catalog.is_empty());
let keys: std::collections::BTreeSet<&String> = Locale::ALL.iter().flat_map(|language| language.catalog().keys()).collect();
let lacking: Vec<_> = keys.into_iter().filter(|key| !catalog.contains_key(key.as_str())).collect();
assert!(lacking.is_empty(), "vi lacks {} catalog message(s): {lacking:?}", lacking.len());
let mut labels: Vec<&str> = lightcraft_engine::command_specs().iter().map(|spec| spec.label).collect();
labels.extend(crate::menus::ui_commands().map(|command| command.1).filter(|label| !Locale::ALL.iter().any(|locale| locale.name() == *label)));
labels.extend(lightcraft_develop::CONTROLS.iter().map(|control| control.label));
labels.extend(crate::panels::settings::TABS.iter().map(|(_, label)| *label));
for (_, label, kind) in lightcraft_catalog::rules::FIELDS {
labels.push(label);
labels.extend(lightcraft_catalog::rules::ops_for(*kind).iter().map(|(_, label)| *label));
}
labels.extend(lightcraft_catalog::rules::FIELD_GROUPS.iter().map(|group| group.0));
labels.extend(lightcraft_engine::rename::TOKENS.iter().map(|token| token.meaning));
labels.extend(lightcraft_engine::rename::TEMPLATE_NOTES);
for word in crate::date_picker::WEEKDAY_SHORT.iter().chain(&["Pick a date", "Year", "Month", "Day"]) {
labels.push(*word);
}
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] {
labels.push(day);
}
let missing: Vec<_> = labels.into_iter().filter(|label| !catalog.contains_key(*label)).collect();
assert!(missing.is_empty(), "vi lacks {} display label(s): {missing:?}", missing.len());
for (key, value) in catalog {
if key.contains('|') {
continue;
}
if VI_INTENTIONAL_ENGLISH.contains(&key.as_str()) {
continue;
}
assert!(!value.is_empty(), "vi empty for {key:?}");
// Identical values outside the allowlist are accidental English fallbacks.
if value == key && key.chars().any(|ch| ch.is_ascii_alphabetic()) && key.len() > 1 {
// Single-letter ops / symbols may match; longer shared English must be listed.
assert!(
key.len() <= 2 || key.chars().all(|ch| !ch.is_ascii_alphabetic()),
"vi keeps English for {key:?}; add it to VI_INTENTIONAL_ENGLISH if intentional"
);
}
}
}
#[test]
fn vietnamese_switches_persists_formats_dates_and_preserves_values() {
use crate::control::{ControlRequest, Outcome};
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default());
app.run("app.language.vietnamese", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Locale::Vi);
assert_eq!(crate::menubar::checked(&app, "app.language.vietnamese"), Some(true));
assert_eq!(tr("Light"), "Ánh sáng");
assert_eq!(tr("Exposure"), "Phơi sáng");
assert_eq!(Locale::Vi.name(), "Tiếng Việt");
assert_eq!(tr("Settings"), "Cài đặt");
assert_eq!(tr("Appearance"), "Giao diện");
assert_eq!(tr("My Photos"), "Ảnh của tôi");
assert_eq!(tr("Candidate"), "Ảnh ứng viên");
assert_eq!(tr("Subject"), "Chủ thể");
assert_eq!(date_group_label("2026-09-20", false), "Chủ Nhật, 20/9/2026");
assert_eq!(date_group_label("2026-09", false), "Tháng 9 năm 2026");
assert_eq!(date_group_label("2026-09", true), "Tháng 9");
assert_eq!(display_time("2026-09-20T16:04:05"), "Chủ Nhật, 20/9/2026 16:04:05");
for n in [0, 1, 2, 5, 22] {
let text = tr_format!("Imported {} photo{}", n, if n == 1 { "" } else { "s" });
assert!(text.contains(&n.to_string()) && text.contains("ảnh"), "{n}: {text}");
assert!(!text.contains("photos") && !text.contains("photo"), "{n}: {text}");
}
let name = "Color {title} 日本語";
assert_eq!(tr_format!("Added {n} photo{} to “{}”", "s", name, n = 22), format!("Đã thêm 22 ảnh vào «{name}»"));
assert_eq!(builtin_label("Warm Glow", false), "Warm Glow");
assert_ne!(builtin_label("Warm Glow", true), "Warm Glow");
assert_eq!(tr("photo-{title}.jpg"), "photo-{title}.jpg");
assert_eq!(tr("develop.set"), "develop.set");
let saved = serde_json::to_string(&app.ui).unwrap();
assert!(saved.contains(r#""language":"vi""#), "{saved}");
assert_eq!(serde_json::from_str::<crate::state::UiState>(&saved).unwrap().language, Locale::Vi);
let ctx = egui::Context::default();
for code in ["vi", "vi-VN", "vi_VN", "vi_VN.UTF-8"] {
assert_eq!(Locale::parse_tag(code), Some(Locale::Vi));
let (request, _) = ControlRequest::new("ui.set", serde_json::json!({"language": code}));
let Outcome::Done(reply) = crate::control::handle(&mut app, &ctx, &request) else { panic!("expected reply") };
assert_eq!(reply["ok"], true);
assert_eq!(language(), Locale::Vi);
}
set_language(Locale::En);
}
#[test]
fn vietnamese_panels_dialogs_and_diacritics_paint_with_bundled_fonts() {
let ctx = fonts_ctx(&[]);
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default());
app.ui.left_panel = true;
let text = painted_text(&ctx, &mut app, Locale::Vi);
assert!(text.contains("Ảnh của tôi") && text.contains("Tất cả ảnh"), "{text}");
for (command, title) in [("app.about", "Giới thiệu LightCraft"), ("app.shortcuts", "Phím tắt"), ("app.settings", "Cài đặt")] {
app.ui.dialog = None;
app.run(command, serde_json::json!({})).unwrap();
let text = painted_text(&ctx, &mut app, Locale::Vi);
assert!(text.contains(title), "{command}: {text}");
}
ctx.fonts_mut(|fonts| {
for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] {
let font = egui::FontId::new(13.0, family);
for ch in "ĂăÂâÊêÔôƠơƯưĐđáàảãạéèẻẽẹíìỉĩịóòỏõọúùủũụýỳỷỹỵ".chars().chain(
Locale::Vi
.catalog()
.values()
.flat_map(|text| text.chars())
.filter(|ch| ('\u{00c0}'..='\u{024f}').contains(ch) || ('\u{1ea0}'..='\u{1ef9}').contains(ch)),
) {
assert!(fonts.has_glyph(&font, ch), "missing Vietnamese glyph {ch}");
}
}
});
set_language(Locale::En);
}
}
+2
View File
@@ -31,6 +31,7 @@ pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
("app.language.swedish", crate::i18n::Locale::Sv.name(), None, "Edit>Language"),
("app.language.finnish", crate::i18n::Locale::Fi.name(), None, "Edit>Language"),
("app.language.indonesian", crate::i18n::Locale::Id.name(), None, "Edit>Language"),
("app.language.vietnamese", crate::i18n::Locale::Vi.name(), None, "Edit>Language"),
];
/// Every UI command: the languages, then everything else. `xtask parity` reads both tables from
@@ -60,6 +61,7 @@ pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {
"app.language.swedish" => Some(crate::i18n::Locale::Sv),
"app.language.finnish" => Some(crate::i18n::Locale::Fi),
"app.language.indonesian" => Some(crate::i18n::Locale::Id),
"app.language.vietnamese" => Some(crate::i18n::Locale::Vi),
_ => None,
}
}
+8 -7
View File
@@ -1,12 +1,12 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Indonesian and Finnish interface catalogs added; Italian: every interface string translated, native-speaker corrections; previously minor: de, es, fr, ja, uk catalogs filled; re-measured; Italian and Georgian interface catalogs added) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-19 · **Change:** minor (Indonesian and Finnish interface catalogs added; Italian: every interface string translated, native-speaker corrections; Vietnamese shipped; previously minor: de, es, fr, ja, uk catalogs filled; re-measured; Italian and Georgian interface catalogs added) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
How far each interface language is from `full`. How to add or maintain a language is in
[localization.md](localization.md); per-language notes are in `localization-<code>.md`
([zh-hans](localization-zh-hans.md), [zh-hant](localization-zh-hant.md), [ja](localization-ja.md),
[pt-br](localization-pt-br.md), [es](localization-es.md), [de](localization-de.md),
[ru](localization-ru.md), [fi](localization-fi.md), [ka](localization-ka.md), [id](localization-id.md)).
[ru](localization-ru.md), [fi](localization-fi.md), [ka](localization-ka.md), [id](localization-id.md), [vi](localization-vi.md)).
## The target
@@ -18,12 +18,12 @@ go beyond the target.
## How the numbers are measured
**Measured** (a Python count over `crates/ui-egui/locales/*.json`, 2026-10-10):
**Measured** (a Python count over `crates/ui-egui/locales/*.json`, 2026-10-13):
- The denominator is every message any catalog knows: the union of the plain catalogs' keys (2,074
- The denominator is every message any catalog knows: the union of the plain catalogs' keys (2,222
English source strings: menus, commands, controls, panels, dialogs, tooltips, presets, What's New
lines) plus the 230 messages of the formats catalogs (`*-formats.json`: text with runtime values and
date patterns), 2,304 messages in all.
lines) plus the 249 messages of the formats catalogs (`*-formats.json`: text with runtime values and
date patterns), 2,471 messages in all.
- A message counts as translated when the catalog has a non-empty value that differs from the English
key. Some identical values are correct (`ISO`, `OK` in German), so the figure slightly understates
coverage; on the other hand, strings that never reach a catalog (technical errors raised in lower
@@ -56,7 +56,7 @@ that is promising but is not recorded as review, so the column says `no (communi
| 9 | Japanese | `ja` | 2,383 / 2,398 (99%; 2026-10-12) | as zh-hans; vertical export watermarks | Jpan via craft-fonts (BIZ UDPGothic / UDMincho); IME through winit | no (community edits) | partial | 2–4 h (native review, lower-layer errors) |
| 10 | German | `de` | 2,338 / 2,398 (97%; 2026-10-12, the rest are words spelled the same) | as zh-hans; What's New translated line by line | Latin | no (community edits) | partial | 2–4 h (native review, lower-layer errors) |
| 11 | Korean | `ko` | 0 (0%) | none | **no Korean (Hangul) face**: needs a non-Adobe OFL face in craft-fonts (Noto CJK is allowed only for Chinese) | no | none | 14–22 h |
| 12 | Vietnamese | `vi` | 0 (0%) | none | Latin with stacked diacritics (Inter covers it; check combining marks) | no | none (target: not shipped) | 10–16 h |
| 12 | Vietnamese | `vi` | 2,471 / 2,471 (100% key coverage of current union; intentional English like `ISO`/`OK` kept) | menus, panels, dialogs, presets, profiles, dates, formats; lower-layer errors English | Latin with stacked diacritics (Inter) | no (community contribution) | partial (catalog complete; lower-layer errors English) | 1–3 h |
## Other languages LightCraft ships (4)
@@ -97,6 +97,7 @@ Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bok
| 2026-10-19 | minor | Indonesian interface catalog (`id`): every plain and formats message any catalog has, plus 69 command labels; What's New left in English |
| 2026-10-17 | minor | Finnish interface catalog (`fi`): 2,610 of 3,308 messages (2,313 plain + 297 formats), What's New left in English |
| 2026-10-15 | trivial | Italian (`it`): 339 interface messages and 60 format strings that were missing or still in English translated (2,628 of 3,278; only What's New left in English); native-speaker corrections: Exposure → *Esposizione*, Sharpening → *Nitidezza*, Preset → *Preset*, blurry → *sfocate* |
| 2026-10-13 | minor | Remeasured denominator (2,222 plain + 249 formats = 2,471); Vietnamese catalog complete at 2,471/2,471 (status partial: lower-layer errors still English) |
| 2026-10-12 | minor | Italian interface catalog (`it`): 2,201 of 2,398 messages, What's New left in English |
| 2026-10-11 | minor | Shared work: the interface decides on error kinds, not English error text (#1046) |
| 2026-10-12 | minor | Georgian (`ka`) row: every catalog message |
+76
View File
@@ -0,0 +1,76 @@
# LightCraft bằng tiếng Việt
Chọn **Chỉnh sửa → Ngôn ngữ → Tiếng Việt** hoặc **Cài đặt → Chung → Ngôn ngữ**.
Lựa chọn áp dụng ngay và được lưu cho lần mở sau (`language: "vi"` trong `ui.json`).
Các tag locale hệ thống sau cũng chọn tiếng Việt: `vi`, `vi-VN`, `vi_VN`, `vi_VN.UTF-8`.
## Phạm vi
Catalog tiếng Việt phủ **toàn bộ** bề mặt localization hiện tại của LightCraft
(đo 2026-10-13: union plain **2 222** + formats **249** = **2 471** thông điệp):
- `crates/ui-egui/locales/vi.json` — thông điệp plain / nhãn hiển thị (2 222 khóa)
- `crates/ui-egui/locales/vi-formats.json` — thông điệp có giá trị runtime và mẫu ngày (249 khóa)
Điều này là **100% bề mặt localizable/runtime hiện tại**, không phải “mọi chuỗi tiếng Anh
trong toàn bộ repository”. Lỗi kỹ thuật từ crate tầng dưới vẫn có thể là tiếng Anh, giống
các ngôn ngữ khác.
## Quy ước thuật ngữ
| English | Vietnamese |
|---|---|
| File / Folder | Tệp / Thư mục |
| Library / Album / Smart Album | Thư viện / Album / Album thông minh |
| Import / Export | Nhập / Xuất |
| Exposure / Contrast | Phơi sáng / Tương phản |
| Highlights / Shadows / Whites / Blacks | Vùng sáng / Vùng tối / Vùng trắng / Vùng đen |
| White Balance / Temperature / Tint | Cân bằng trắng / Nhiệt độ / Tông |
| Vibrance / Saturation | Độ rực rỡ / Độ bão hòa |
| Texture / Clarity / Dehaze / Grain / Vignette | Kết cấu / Độ rõ / Khử sương mù / Hạt / Tối góc |
| Tone Curve | Đường cong tông màu |
| Sharpening / Noise Reduction / AI Denoise | Làm nét / Giảm nhiễu / Khử nhiễu AI |
| Masking / Brush / Linear Gradient | Mặt nạ / Cọ / Chuyển sắc tuyến tính |
| Preset / Profile / History / Version | Thiết lập sẵn / Hồ sơ / Lịch sử / Phiên bản |
| Appearance (theme) | Giao diện |
| Subject (AI) | Chủ thể |
| Candidate / Select | Ảnh ứng viên / Chọn |
| Flag / Picked / Rejected / Unflagged | Gắn cờ / Đã chọn / Đã loại / Không gắn cờ |
| Shutter Speed | Tốc độ màn trập |
| Smart Previews | Xem trước thông minh |
Tên sản phẩm và chuẩn giữ nguyên khi là định danh: LightCraft, ArtCraft, GitHub, Discord,
JPEG, PNG, TIFF, WebP, AVIF, HEIF, HEIC, RAW, DNG, RGB, HDR, ICC, EXIF, XMP, GPU, MCP, SAM, ISO, OK.
Tên album / preset / profile do người dùng tạo, tên tệp, command ID và metadata nhập tay
không được dịch.
## Ngày và số lượng
- Ngày đầy đủ: `{weekday}, {day}/{month}/{year}` (ví dụ `Chủ Nhật, 20/9/2026`)
- Tháng/năm: `Tháng {month} năm {year}`
- Thứ viết tắt trên date picker: T2…T7, CN
- Thông điệp đếm dùng dạng trung tính (`22 ảnh`), không tạo số ít/số nhiều nhân tạo như tiếng Anh
## Font
Tiếng Việt dùng Inter (Latin + dấu). Không cần craft-fonts CJK. Kiểm tra glyph trong
`vietnamese_panels_dialogs_and_diacritics_paint_with_bundled_fonts`.
## Kiểm tra
```sh
cargo test -p lightcraft-ui-egui i18n::tests -- --nocapture
LIGHTCRAFT_LANGUAGE=vi lightcraft-cli snapshot --demo -o vi.png --size 1600x1000
LIGHTCRAFT_LANGUAGE=vi lightcraft-cli snapshot --demo -o vi-narrow.png --size 1280x800
```
Các test tiếng Việt bắt buộc phủ catalog union, nhãn lệnh/control/Settings/Smart Album,
đổi ngôn ngữ, lưu `vi`, formats, ngày, dữ liệu người dùng, và glyph dấu.
## Nguồn dịch (clean-room)
Dịch từ chuỗi tiếng Anh nguồn của LightCraft. PhotoCraft / VectorCraft / PDFCraft chỉ dùng
làm bộ nhớ thuật ngữ khi cùng nguồn tiếng Anh và cùng ngữ cảnh UI. Không dùng gói ngôn ngữ
Adobe Lightroom hay tài nguyên độc quyền.
+3 -2
View File
@@ -2,11 +2,11 @@
The interface ships in the language it is written in (English) plus every language in the table in
`crates/ui-egui/src/i18n.rs`. Today that is English, Simplified Chinese (`zh-hans`), Traditional
Chinese (`zh-hant`, Taiwan), Japanese (`ja`), Brazilian Portuguese (`pt-br`), Spanish (`es`), German (`de`), Russian (`ru`), French (`fr`), Ukrainian (`uk`), Czech (`cs`), Georgian (`ka`), Swedish (`sv`), Finnish (`fi`) and Indonesian (`id`). This file is the reference for **adding or maintaining a language**; the per-language notes
Chinese (`zh-hant`, Taiwan), Japanese (`ja`), Brazilian Portuguese (`pt-br`), Spanish (`es`), German (`de`), Russian (`ru`), French (`fr`), Ukrainian (`uk`), Czech (`cs`), Georgian (`ka`), Swedish (`sv`), Finnish (`fi`), Indonesian (`id`) and Vietnamese (`vi`). This file is the reference for **adding or maintaining a language**; the per-language notes
are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-hant.md`](localization-zh-hant.md),
[`localization-ja.md`](localization-ja.md), [`localization-pt-br.md`](localization-pt-br.md), [`localization-es.md`](localization-es.md),
[`localization-de.md`](localization-de.md), [`localization-ru.md`](localization-ru.md),
[`localization-cs.md`](localization-cs.md), [`localization-fi.md`](localization-fi.md), [`localization-ka.md`](localization-ka.md) and [`localization-id.md`](localization-id.md).
[`localization-cs.md`](localization-cs.md), [`localization-fi.md`](localization-fi.md), [`localization-ka.md`](localization-ka.md), [`localization-id.md`](localization-id.md) and [`localization-vi.md`](localization-vi.md).
## Adding a language
@@ -35,6 +35,7 @@ are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-h
It, "it", "Italiano", "Latn", include_str!("../locales/it.json");
Ka, "ka", "ქართული", "Geor", include_str!("../locales/ka.json");
Fi, "fi", "Suomi", "Latn", include_str!("../locales/fi.json");
Vi, "vi", "Tiếng Việt", "Latn", include_str!("../locales/vi.json");
}
```