Browse / github.com/storytold/craft-launcher / patch
i18n: add Vietnamese (vi) interface language
i18n: add Vietnamese (vi) interface language <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added Vietnamese as an available interface language, with translated menus and interface text. * Vietnamese translations are available for grid and table labels, dates, status messages, and update messages. Product names remain in English. * Vietnamese plural translations support different item counts, with English fallback when a translation is unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- sonth87 (imported from pull request #755, open)
- Base
bf39e92f30bcf1f86d07d7aae1e877632f74bce6- Size
- +609 −2 in 4 files
- Review
- pending
- Shared
- 2026-10-28
- Id
eff7a436109ec72ca2e090ee
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/i18n/lang.rs crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/tests/vietnamese.rs
@@ -22,7 +22,7 @@ impl Script { /// One supported UI language. pub struct LangInfo {- /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`).+ /// Canonical BCP 47 code (`en`, `ru`, `zh-CN`, `de`, `fr`, `pt-BR`, `es`, `cs`, `pl`, `hu`, `ja`, `vi`). /// Also the `interfaceLanguage` value. pub code: &'static str, /// The language's name in itself, shown in the Preferences dropdown.@@ -94,7 +94,7 @@ pub(super) fn plural_polish(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language.-pub static LANGUAGES: [LangInfo; 11] = [+pub static LANGUAGES: [LangInfo; 12] = [ LangInfo { code: "en", name: "English",@@ -216,6 +216,17 @@ pub static LANGUAGES: [LangInfo; 11] = [ complete_menus: true, catalog: OnceLock::new(), },+ LangInfo {+ code: "vi",+ name: "Tiếng Việt",+ english_name: "Vietnamese",+ script: None,+ source: include_str!("vi.tsv"),+ plural: plural_none,+ plural_forms: 1,+ complete_menus: true,+ catalog: OnceLock::new(),+ }, ]; impl LangInfo {@@ -254,6 +265,7 @@ impl Lang { pub const PL: Lang = Lang(&LANGUAGES[8]); pub const HU: Lang = Lang(&LANGUAGES[9]); pub const JA: Lang = Lang(&LANGUAGES[10]);+ pub const VI: Lang = Lang(&LANGUAGES[11]); pub fn code(self) -> &'static str { self.0.code@@ -386,6 +386,24 @@ fn japanese_locales_and_counts_work() { assert_eq!(message(Lang::JA, "PhotoCraft updated to 0.5.0"), "PhotoCraft を 0.5.0 に更新しました"); } +#[test]+fn vietnamese_locales_and_counts_work() {+ for tag in ["vi", "vi-VN", "vi_VN.UTF-8", "VI_vn"] {+ assert_eq!(lang_from_tag(tag), Some(Lang::VI), "{tag}");+ }+ assert_eq!(super::detect::first_supported(["xx-XX", "vi-VN", "en-US"]), Some(Lang::VI));+ // Vietnamese has one plural form.+ for n in [0, 1, 2, 11, 100] {+ assert_eq!(trn(Lang::VI, n, "{n} app", "{n} apps"), format!("{n} ứng dụng"));+ assert_eq!(trn(Lang::VI, n, "{n} day ago", "{n} days ago"), format!("{n} ngày trước"));+ }+ assert_eq!(trn(Lang::VI, 2, "{n} unknown item", "{n} unknown items"), "2 unknown items");+ assert_eq!(tr(Lang::VI, "Untranslated diagnostic"), "Untranslated diagnostic");+ assert_eq!(super::tr_ctx(Lang::VI, "month", "Mar"), "Thg 3");+ assert_eq!(tr(Lang::VI, "{month} {day}, {year}"), "{day} {month}, {year}");+ assert_eq!(message(Lang::VI, "PhotoCraft updated to 0.5.0"), "Đã cập nhật PhotoCraft lên 0.5.0");+}+ /// With the system's Chinese font installed, every UI family can draw the whole Chinese catalog. /// Machines without one (CI, the web) skip this; the UI then stays in English. #[test]new file mode 100644@@ -0,0 +1,382 @@+# Vietnamese (vi) catalog for ArtCraft Launcher.+# Format: context<TAB>source<TAB>translation+# Empty context = plain UI string (tl!, t).+# @id = command-id keyed (tr_id).+# @plural = plural forms: one|other in English, one form for Vietnamese.+# @msg = status/error message template (message, msg).+# Other context = disambiguation for tr_ctx.+#+# Clean-room: written from the meaning of ArtCraft Launcher's English labels in natural, concise+# Vietnamese, using standard Vietnamese creative and computing terminology.+#+# Coverage: the whole interface — every menu string and tl! literal (complete_menus = true).+# Left in English on purpose: product name "ArtCraft Launcher" (and its "Launcher" tag), suite+# name "Craft App Suite",+# app names (PhotoCraft, LightCraft, FilmCraft, DesignCraft, VectorCraft, PdfCraft, EffectCraft,+# WordCraft, DeckCraft, GridCraft, CADCraft, SoundCraft, FigCraft), "GitHub", "Discord".+# Shortcut keys keep their key-cap names (Shift, Alt, Ctrl, Cmd, Esc).+# Trailing "…" (U+2026) must be preserved. Placeholders {name}, {n}, {version}, {count}, {v},+# {what}, {unit}, {real}, {e} must appear in translation.+# Vietnamese nouns do not inflect for grammatical number (one plural form).++ Launcher Launcher+ Craft App Suite Craft App Suite+ Open Source Tools for Creatives, Makers, and Builders Bộ công cụ mã nguồn mở cho nhà sáng tạo, người chế tác và nhà phát triển+ Settings Tùy chọn+ Check for updates Kiểm tra cập nhật+ Grid Lưới+ Table Bảng+ Show apps as cards Hiển thị ứng dụng dạng thẻ+ Show apps as a table Hiển thị ứng dụng dạng bảng+ All Tất cả+ Updates Cập nhật+ Not installed Chưa cài đặt+ Search apps Tìm ứng dụng+ Sort Sắp xếp+ Suite order Thứ tự bộ ứng dụng+ Name Tên+ Inspired by Lấy cảm hứng từ+ Status Trạng thái+ Installed version Phiên bản đã cài+ Latest version Phiên bản mới nhất+ Newest release Bản phát hành mới nhất+ Discord Discord+ GitHub GitHub+ Website Trang web+ Classic apps Ứng dụng cổ điển+ Classic Cổ điển+ New apps Ứng dụng mới+ NEW MỚI+ Install Cài đặt+ Update to {v} Cập nhật lên {v}+ Open Mở+ Download for {os} Tải về cho {os}+ Download Tải về+ Coming soon Sắp ra mắt+ Checking… Đang kiểm tra…+ Not available Không khả dụng+ Offline Ngoại tuyến+ Downloading {done} of {total} Đang tải {done} / {total}+ Downloading… Đang tải về…+ Reconnecting… Đang kết nối lại…+ Verifying… Đang xác minh…+ Installing, then restarting… Đang cài đặt, sau đó khởi động lại…+ Installing… Đang cài đặt…+ Removing… Đang gỡ bỏ…+ Pause download Tạm dừng tải về+ Cancel download Hủy tải về+ Resume download Tiếp tục tải về+ Discard download Hủy bỏ bản tải về+ Release notes Ghi chú phát hành+ Show in Finder Hiển thị trong Finder+ Show in Explorer Hiển thị trong Explorer+ Show in Files Hiển thị trong Trình quản lý tệp+ Reinstall Cài đặt lại+ Uninstall… Gỡ cài đặt…+ All releases Tất cả bản phát hành+ ArtCraft Launcher {version} is available. Đã có ArtCraft Launcher {version}.+ Dismiss Bỏ qua+ Update and restart Cập nhật và khởi động lại+ A new version is available. Would you like to relaunch with the updated version? Đã có phiên bản mới. Bạn có muốn khởi động lại với phiên bản đã cập nhật không?+ A new version is available. Download it to replace this version? Đã có phiên bản mới. Tải về để thay thế phiên bản này?+ Later Để sau+ What's new Có gì mới+ No notes were published with this release. Không có ghi chú phát hành nào cho phiên bản này.+ Released {date} Phát hành {date}+ Open on GitHub Mở trên GitHub+ Close Đóng+ ArtCraft Launcher {version} ArtCraft Launcher {version}+ Only when I ask Chỉ khi tôi yêu cầu+ Automatically download and install updates Tự động tải về và cài đặt bản cập nhật+ Notify me when an installed app has an update Thông báo khi ứng dụng đã cài có bản cập nhật+ Include pre-releases (betas and release candidates) Bao gồm bản phát hành trước (beta và RC)+ Installing on Linux Cài đặt trên Linux+ Installing on Windows Cài đặt trên Windows+ Install new apps as Cài đặt ứng dụng mới dưới dạng+ System package (recommended) Gói hệ thống (khuyến nghị)+ Updates always use the format an app was installed with. Các bản cập nhật luôn dùng định dạng mà ứng dụng đã được cài đặt.+ Running in the background Chạy trong nền+ Keep running in the tray when the window is closed Tiếp tục chạy ở khay hệ thống khi đóng cửa sổ+ Show the window on the current desktop Hiển thị cửa sổ trên màn hình nền hiện tại+ Start ArtCraft Launcher when I log in Khởi động ArtCraft Launcher khi đăng nhập+ Community Cộng đồng+ Questions, ideas and news from the ArtCraft team Câu hỏi, ý tưởng và tin tức từ đội ngũ ArtCraft+ Appearance Giao diện+ Quit ArtCraft Launcher Thoát ArtCraft Launcher+ Done Xong+ Uninstall {name}? Gỡ cài đặt {name}?+ The app will be moved to the Trash. Your documents and settings stay. Ứng dụng sẽ được chuyển vào Thùng rác. Tài liệu và tùy chọn của bạn vẫn được giữ nguyên.+ Windows will remove it like any installed program. Your documents and settings stay. Windows sẽ gỡ bỏ ứng dụng như mọi chương trình đã cài đặt khác. Tài liệu và tùy chọn của bạn vẫn được giữ nguyên.+ Your system's package manager will remove it, and may ask for your password. Your documents and settings stay. Trình quản lý gói của hệ thống sẽ gỡ bỏ ứng dụng và có thể hỏi mật khẩu của bạn. Tài liệu và tùy chọn của bạn vẫn được giữ nguyên.+ The app will be removed. Your documents and settings stay. Ứng dụng sẽ được gỡ bỏ. Tài liệu và tùy chọn của bạn vẫn được giữ nguyên.+ Uninstall Gỡ cài đặt+ Cancel Hủy+ Checked {ago} Đã kiểm tra {ago}+ just now vừa xong+ yesterday hôm qua+ {month} {day}, {year} {day} {month}, {year}++ Automatically Tự động+ Every Craft app Mọi ứng dụng Craft+ Apps on this computer Ứng dụng trên máy tính này+ Installed apps with a newer version Ứng dụng đã cài có phiên bản mới hơn+ Apps you can install Ứng dụng có thể cài đặt+ Hide App Ẩn ứng dụng+ Unhide App Hiện lại ứng dụng+ Hidden Apps Ứng dụng đã ẩn+ Apps you hid Các ứng dụng bạn đã ẩn+ Checked {ago} · using {sources} Đã kiểm tra {ago} · qua {sources}+ {a} and {b} {a} và {b}+ Join the Discord Tham gia Discord+ Learn more Tìm hiểu thêm+ Minimize Thu nhỏ+ Maximize Phóng to+ Restore Khôi phục+ New Mới+ OK OK+ Open link Mở liên kết+ Update Cập nhật+ {name} icon Biểu tượng {name}+ Loading ArtCraft Launcher… Đang tải ArtCraft Launcher…+ Cancel update Hủy cập nhật+ Stop waiting; the update can be started again later Dừng chờ; có thể bắt đầu cập nhật lại sau+ Download again Tải lại+ Install cancelled Đã hủy cài đặt+ Install cancelled. The download is kept for the next try. Đã hủy cài đặt. Bản tải về được giữ lại cho lần thử tiếp theo.+ Update cancelled Đã hủy cập nhật+ Uninstall cancelled Đã hủy gỡ cài đặt+ Retry install Thử cài đặt lại+ Retry update to {version} Thử cập nhật lại lên {version}+ The download is still here. It's checked again, then installed. Bản tải về vẫn còn đây. Tệp sẽ được kiểm tra lại rồi cài đặt.+ Waiting for another app… Đang chờ ứng dụng khác…+ Waiting for other installs… Đang chờ các tiến trình cài đặt khác…++# Package formats offered for download (PackageKind::label)+ Installer (.msi) Trình cài đặt (.msi)+ Portable (.zip) Bản di động (.zip)+ Disk image (.dmg) Ảnh đĩa (.dmg)+ Debian package (.deb) Gói Debian (.deb)+ RPM package (.rpm) Gói RPM (.rpm)+ Arch package (.pkg.tar.zst) Gói Arch (.pkg.tar.zst)+ Flatpak bundle Gói Flatpak+ Tarball (.tar.gz) Gói nén (.tar.gz)++# Contextual: each app's tagline, keyed by app id+photocraft Image editing Chỉnh sửa ảnh+vectorcraft Vector illustration Minh họa vector+filmcraft Video editing, color and sound Dựng phim, chỉnh màu và âm thanh+lightcraft Photo library and raw development Thư viện ảnh và xử lý ảnh RAW+pdfcraft Reading, organizing and protecting PDFs Đọc, sắp xếp và bảo vệ tài liệu PDF+effectcraft Motion graphics and visual effects Đồ họa chuyển động và kỹ xảo hình ảnh+designcraft Page layout and publishing Bố cục trang và chế bản+wordcraft Writing and document design Soạn thảo và thiết kế tài liệu+deckcraft Presentations and slide shows Bài thuyết trình và trình chiếu+gridcraft Spreadsheets and data Bảng tính và dữ liệu+cadcraft Computer-aided design and drafting Thiết kế và vẽ kỹ thuật CAD+soundcraft Recording, editing and mixing audio Thu âm, biên tập và hòa âm+figcraft Interface design and prototyping Thiết kế giao diện và tạo mẫu thử++# Contextual: month abbreviations for date rendering+month Jan Thg 1+month Feb Thg 2+month Mar Thg 3+month Apr Thg 4+month May Thg 5+month Jun Thg 6+month Jul Thg 7+month Aug Thg 8+month Sep Thg 9+month Oct Thg 10+month Nov Thg 11+month Dec Thg 12+ Not checked yet Chưa kiểm tra+ GitHub's hourly limit was reached; full details return in {min} min Đã đạt giới hạn theo giờ của GitHub; chi tiết đầy đủ sẽ có lại sau {min} phút+ GitHub limit hit; back in {min} min Đã chạm giới hạn GitHub; thử lại sau {min} phút+ Installing for {platform} Đang cài đặt cho {platform}+ Downloads for {platform} Bản tải về cho {platform}+ No downloads for this system Không có bản tải về cho hệ thống này+ Join the ArtCraft community on Discord Tham gia cộng đồng ArtCraft trên Discord+ ArtCraft on {name} ArtCraft trên {name}+ Couldn't open {name}: {e} Không thể mở {name}: {e}+ Download cancelled Đã hủy tải về+ Available to install Có thể cài đặt+ Available Khả dụng+ Installed · up to date Đã cài đặt · bản mới nhất+ Update available Có bản cập nhật+ Installed Đã cài đặt+ installed đã cài đặt+ Not available here Không khả dụng trên hệ thống này+ Updating… Đang cập nhật…+ Update paused Đã tạm dừng cập nhật+ Download paused Đã tạm dừng tải về+ {version} installed → {latest} released Đã cài {version} → đã phát hành {latest}+ Version {version} Phiên bản {version}+ {installed} installed · latest {latest} Đã cài {installed} · mới nhất {latest}+ Installed · latest {latest} Đã cài · mới nhất {latest}+ Version {version} installed Đã cài phiên bản {version}+ Version {latest} Phiên bản {latest}+ No release published yet Chưa có bản phát hành nào+ ArtCraft Launcher ArtCraft Launcher+ App Ứng dụng+ Latest Mới nhất+ Released Phát hành+ Auto Tự động+ Language Ngôn ngữ+ Ascending Tăng dần+ Descending Giảm dần+ Click to reverse the order Nhấp để đảo ngược thứ tự+ Click to sort by this column Nhấp để sắp xếp theo cột này+ Like {name} Tương tự {name}+ {done} of {total} {done} / {total}+ Downloading Đang tải về+ Verifying checksum… Đang kiểm tra mã kiểm…+ Resume Tiếp tục+ {done} of {total} downloaded Đã tải {done} / {total}+ {done} downloaded Đã tải {done}+ Checking for updates… Đang kiểm tra cập nhật…+ Downloads the new version, replaces this one and opens it again Tải phiên bản mới, thay thế phiên bản hiện tại và mở lại ứng dụng+ Couldn't check for updates: {f} Không thể kiểm tra cập nhật: {f}+ Website (coming soon) Trang web (sắp ra mắt)+ GitHub repository Kho mã nguồn GitHub+ ArtCraft ArtCraft+ Update 1 app Cập nhật 1 ứng dụng+ Update all ({count}) Cập nhật tất cả ({count})+ ⊞ Grid ⊞ Lưới+ ☰ Table ☰ Bảng+ 🔍 Search apps 🔍 Tìm ứng dụng+ {name} {version} {name} {version}+ Match system Theo hệ thống+ Dark Tối+ Light Sáng+ Full changelog Nhật ký thay đổi đầy đủ+ link liên kết+ No apps match Không có ứng dụng phù hợp+ Open ArtCraft Launcher to update. Mở ArtCraft Launcher để cập nhật.+ {name} {v} is available Đã có {name} {v}+ {count} updates are available Có {count} bản cập nhật+ These notes weren't fetched (GitHub's request limit was reached). They're on the release page. Không tải được ghi chú phát hành (đã đạt giới hạn yêu cầu của GitHub). Bạn có thể xem trên trang phát hành.+ When on, opening the launcher from the menu bar brings its window to the desktop you're on. When off, macOS switches you to the desktop the window was on. Khi bật, mở trình khởi chạy từ thanh menu sẽ đưa cửa sổ tới màn hình nền hiện tại. Khi tắt, macOS sẽ chuyển sang màn hình nền nơi đặt cửa sổ.+ Couldn't change the login item: {e} Không thể thay đổi mục khởi động cùng hệ thống: {e}++# Contextual: table column headers+column App Ứng dụng+column Installed Đã cài+column Latest Mới nhất+column Released Phát hành++# Contextual: the filter tab names a group (the status pill says "Installed")+filter Installed Đã cài đặt+ App details Chi tiết ứng dụng+ Close (Esc) Đóng (Esc)+ Features Tính năng+ Latest release Bản phát hành mới nhất+ Version Phiên bản+ Platforms Nền tảng+ Releases Bản phát hành+ Enlarge picture Phóng to ảnh+ Click to enlarge Nhấp để phóng to+ Previous picture (←) Ảnh trước (←)+ Next picture (→) Ảnh tiếp (→)+ Loading pictures… Đang tải ảnh…+ Pictures couldn't be loaded. Reopen this page to try again. Không thể tải ảnh. Hãy mở lại trang này để thử lại.+ {name} details Chi tiết {name}++# Plural forms for Vietnamese (one form: Vietnamese nouns don't inflect for number)+@plural {n} app|{n} apps {n} ứng dụng+@plural {n} min ago|{n} min ago {n} phút trước+@plural {n} h ago|{n} h ago {n} giờ trước+@plural {n} day ago|{n} days ago {n} ngày trước++# Message templates (@msg): toasts from the backend. {placeholders} match any text.+@msg Couldn't open {name}: {e} Không thể mở {name}: {e}+@msg Download cancelled Đã hủy tải về+@msg {name} updated to {version} Đã cập nhật {name} lên {version}+@msg {name} installed: {version} Đã cài đặt {name}: {version}+@msg {name} updated to {version} (this release publishes no checksum, so only its size was checked) Đã cập nhật {name} lên {version} (bản phát hành này không có mã kiểm nên chỉ kiểm tra kích thước tệp)+@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) Đã cài đặt {name}: {version} (bản phát hành này không có mã kiểm nên chỉ kiểm tra kích thước tệp)+@msg {name} was removed Đã gỡ cài đặt {name}+@msg {name} is running. Quit it, then try again. {name} đang chạy. Hãy thoát ứng dụng rồi thử lại.+@msg {name} was installed from the {repo} repository or the AUR ({pkg}). Update it with {cmd} or sudo pacman -Syu {name} được cài đặt từ kho {repo} hoặc AUR ({pkg}). Hãy cập nhật bằng {cmd} hoặc sudo pacman -Syu+@msg {name} was installed from the {repo} repository or the AUR ({pkg}). Update it with your AUR helper (for example: yay -S {pkg}) or sudo pacman -Syu {name} được cài đặt từ kho {repo} hoặc AUR ({pkg}). Hãy cập nhật bằng trình trợ giúp AUR (ví dụ: yay -S {pkg}) hoặc sudo pacman -Syu+@msg {name} was installed from the AUR ({pkg}). Update it with {cmd} {name} được cài đặt từ AUR ({pkg}). Hãy cập nhật bằng {cmd}+@msg {name} was installed from the AUR ({pkg}). Update it with your AUR helper (for example: yay -S {pkg}) {name} được cài đặt từ AUR ({pkg}). Hãy cập nhật bằng trình trợ giúp AUR (ví dụ: yay -S {pkg})++# Command IDs for menu items (@id)+# Format: @id<TAB>command_id<TAB>translation+@id app.install Cài đặt+@id app.update Cập nhật lên {v}+@id app.open Mở+@id app.download Tải về cho {os}+@id app.reveal Hiển thị trong Finder+@id app.reinstall Cài đặt lại+@id app.uninstall Gỡ cài đặt…+@id app.notes Ghi chú phát hành+@id app.website Trang web+@id app.github Kho mã nguồn GitHub+@id app.releases Tất cả bản phát hành+@id launcher.update Cập nhật và khởi động lại+@id launcher.download Tải về+@id launcher.notes Có gì mới+@id launcher.dismiss Bỏ qua+@id settings.open Tùy chọn+@id settings.quit Thoát ArtCraft Launcher+@id view.grid Lưới+@id view.table Bảng+@id filter.all Tất cả+@id filter.installed Đã cài đặt+@id filter.updates Cập nhật+@id filter.notinstalled Chưa cài đặt+@id sort.suite Thứ tự bộ ứng dụng+@id sort.name Tên+@id sort.inspired Lấy cảm hứng từ+@id sort.status Trạng thái+@id sort.installed Phiên bản đã cài+@id sort.latest Phiên bản mới nhất+@id sort.released Bản phát hành mới nhất+@id help.discord Discord+@id help.github GitHub+@id help.website Trang web+@id update.check Kiểm tra cập nhật+@id update.check_auto Tự động kiểm tra cập nhật+@id update.notify Thông báo khi ứng dụng đã cài có bản cập nhật+@id update.prereleases Bao gồm bản phát hành trước (beta và RC)+@id linux.preferred_kind Cài đặt ứng dụng mới dưới dạng+@id tray.close_to_tray Tiếp tục chạy ở khay hệ thống khi đóng cửa sổ+@id tray.show_on_current_desktop Hiển thị cửa sổ trên màn hình nền hiện tại+@id tray.login_item Khởi động ArtCraft Launcher khi đăng nhập+@id appearance.theme Giao diện++# Tray (menu bar) menu and tooltip+ Show ArtCraft Launcher Hiển thị ArtCraft Launcher+ Check for Updates Kiểm tra cập nhật+ Visit getartcraft.com Truy cập getartcraft.com+ ArtCraft on GitHub ArtCraft trên GitHub+ Join the ArtCraft Discord Tham gia Discord của ArtCraft+@plural ArtCraft Launcher · {n} update available|ArtCraft Launcher · {n} updates available ArtCraft Launcher · Có {n} bản cập nhật+ Install folder Thư mục cài đặt+ Default install folder Thư mục cài đặt mặc định+ System default Mặc định hệ thống+ Browse… Duyệt…+ Use default Dùng mặc định+ New installs use this folder. Updates stay in their current location. Các cài đặt mới sẽ dùng thư mục này. Bản cập nhật giữ nguyên vị trí hiện tại.+ Custom folders use AppImage on Linux. Trên Linux, thư mục tùy chỉnh dùng định dạng AppImage.+ On Linux, custom folders use AppImage format. Trên Linux, thư mục tùy chỉnh dùng định dạng AppImage.+ The system manages install locations here. Hệ thống quản lý vị trí cài đặt tại đây.+@msg couldn't use install folder {path}: {reason} không thể dùng thư mục cài đặt {path}: {reason}+@msg this release has no AppImage for a custom install folder bản phát hành này không có AppImage cho thư mục cài đặt tùy chỉnh+@msg custom install folders aren't supported on this system hệ thống này không hỗ trợ thư mục cài đặt tùy chỉnh+@msg {path} already exists and wasn't installed by ArtCraft Launcher {path} đã tồn tại và không được cài đặt bởi ArtCraft Launcher+@msg couldn't move {path} to Trash ({reason}). Remove it in Finder instead. không thể chuyển {path} vào Thùng rác ({reason}). Hãy xóa trong Finder.+ Apps are placed inside this folder. Các ứng dụng được đặt bên trong thư mục này.+ Replace what's there? Thay thế mục hiện có?+ {path} already exists. Installing {name} will overwrite it. {path} đã tồn tại. Cài đặt {name} sẽ ghi đè lên mục đó.+ Replace Thay thế+ Recently updated Mới cập nhật gần đây+ Name (A–Z) Tên (A–Z)+ Name (Z–A) Tên (Z–A)+ Frequently used Thường dùng+ Next app Ứng dụng tiếp theo+ Previous app Ứng dụng trước+ Show {n} more changes Hiển thị thêm {n} thay đổi+ Last updated: {date} Cập nhật lần cuối: {date}+column Version Phiên bảnnew file mode 100644@@ -0,0 +1,195 @@+//! The Vietnamese UI, in its own test binary: the UI language is process-wide, so it can't share one+//! with the English UI tests. `ARTCRAFT_LAUNCHER_SNAPSHOTS=<dir>` also saves screenshots (run with+//! `-- --test-threads=1`, see `ui.rs`).++use std::cell::Cell;+use std::rc::Rc;++use artcraft_launcher_core::protocol::{Backend, Capabilities, Command, Event, JobPhase, LatestRelease, Origin, Source};+use artcraft_launcher_core::release::{Asset, Release};+use artcraft_launcher_core::status::InstallSource;+use artcraft_launcher_core::{Arch, Installed, Os, Platform, SUITE, version};+use artcraft_launcher_ui_egui::settings::ViewMode;+use artcraft_launcher_ui_egui::{Host, LauncherUi, Settings};+use egui_kittest::Harness;+use egui_kittest::kittest::Queryable;++const NOW: i64 = 1_791_500_000;++struct Fake {+ events: Vec<Event>,+ clock: Rc<Cell<i64>>,+ os: Os,+}++impl Backend for Fake {+ fn send(&mut self, _command: Command) {}+ fn poll(&mut self) -> Vec<Event> {+ std::mem::take(&mut self.events)+ }+ fn capabilities(&self) -> Capabilities {+ Capabilities { manage_apps: true, reveal_label: "Show in Finder", self_update: true }+ }+ fn platform(&self) -> Option<Platform> {+ Some(Platform { os: self.os, arch: Arch::Arm64 })+ }+ fn now(&self) -> i64 {+ self.clock.get()+ }+}++struct DesktopHost;+impl Host for DesktopHost {+ fn has_tray(&self) -> bool {+ true+ }+ fn login_item(&self) -> Option<bool> {+ Some(false)+ }+}++fn release(id: &str, v: &str, at: i64) -> Release {+ let name = format!("{id}-{v}-macos-universal.dmg");+ Release {+ tag: format!("v{v}"),+ version: version::parse(v),+ name: format!("v{v}"),+ published_at: Some(at),+ html_url: format!("https://github.com/storytold/{id}/releases/tag/v{v}"),+ notes: String::new(),+ prerelease: false,+ assets: vec![Asset {+ name: name.clone(),+ size: 150 << 20,+ url: format!("https://github.com/storytold/{id}/releases/download/v{v}/{name}"),+ sha256: None,+ }],+ }+}++fn events() -> Vec<Event> {+ let mut ev = Vec::new();+ for (i, app) in SUITE.iter().enumerate() {+ let latest = LatestRelease::Found(release(app.id, "0.5.0", NOW - 40 * 86_400 - i as i64 * 600));+ ev.push(Event::Latest { app: app.id.into(), latest, from: Origin::ArtCraftApi });+ let installed = matches!(app.id, "photocraft" | "lightcraft").then(|| Installed {+ version: version::parse(if app.id == "photocraft" { "0.4.1" } else { "0.5.0" }),+ source: InstallSource::MacApp,+ location: format!("/Applications/{}.app", app.name),+ package: None,+ });+ ev.push(Event::Detected { app: app.id.into(), installed });+ }+ ev.push(Event::Progress { app: "filmcraft".into(), phase: JobPhase::Verifying });+ ev.push(Event::CheckFinished { at: NOW - 180, source: Source::Feed, rate_limited_until: None });+ // A backend message, translated through its `@msg` template where it's shown.+ ev.push(Event::JobFinished { app: "lightcraft".into(), result: Ok("LightCraft updated to 0.5.0".into()) });+ ev+}++fn harness(view: ViewMode) -> Harness<'static, Option<LauncherUi>> {+ harness_on(view, Os::MacOs)+}++fn harness_on(view: ViewMode, os: Os) -> Harness<'static, Option<LauncherUi>> {+ artcraft_launcher_ui_egui::i18n::set_test_lang(artcraft_launcher_ui_egui::i18n::Lang::EN);+ let mut backend = Some(Fake { events: events(), clock: Rc::new(Cell::new(NOW)), os });+ let settings =+ Settings { view, notify: false, language: "vi".into(), theme: artcraft_launcher_ui_egui::theme::ThemeChoice::Dark, ..Settings::default() };+ Harness::builder().with_size(egui::vec2(1180.0, 900.0)).with_theme(egui::Theme::Dark).build_ui_state(+ move |ui, state: &mut Option<LauncherUi>| {+ if state.is_none()+ && let Some(b) = backend.take()+ {+ *state = Some(LauncherUi::new(ui.ctx(), Box::new(b), settings.clone()));+ }+ if let Some(l) = state.as_mut() {+ l.logic(&ui.ctx().clone(), &mut DesktopHost);+ l.ui(ui, &mut DesktopHost);+ }+ },+ None,+ )+}++fn snapshot(h: &mut Harness<'_, Option<LauncherUi>>, name: &str) {+ let Some(dir) = std::env::var_os("ARTCRAFT_LAUNCHER_SNAPSHOTS") else { return };+ let img = h.render().unwrap();+ img.save(std::path::Path::new(&dir).join(format!("{name}.png"))).unwrap();+}++// One test: the UI language is process-wide.+#[test]+fn vietnamese_ui_translates_labels_dates_and_messages() {+ let mut h = harness(ViewMode::Grid);+ h.run_steps(8);+ snapshot(&mut h, "grid-vi-dark");+ // Product names stay in English.+ assert!(h.query_by_label("Launcher").is_none());+ h.get_by_label("Craft App Suite");+ h.get_by_label("Chỉnh sửa ảnh");+ h.get_by_label("Cập nhật lên 0.5.0");+ h.get_by_label_contains("Đang kiểm tra mã kiểm…");+ h.get_by_label_contains("Đã kiểm tra 3 phút trước · qua ArtCraft API");+ h.get_by_label("Đã cập nhật LightCraft lên 0.5.0");++ let mut table = harness(ViewMode::Table);+ table.run_steps(8);+ snapshot(&mut table, "table-vi-dark");+ table.get_by_label("Phiên bản");+ assert!(table.query_all_by_label_contains("Thg 8, 2026").count() >= 1);+ check_settings_layout();+}++/// Checks the actual modal after resizing it while open, including Linux's extra controls.+fn check_settings_layout() {+ let mut h = harness_on(ViewMode::Grid, Os::Linux);+ h.run_steps(8);+ h.get_by_label("Tùy chọn").click();+ h.run_steps(8);+ for (width, height) in [(1180.0, 900.0), (720.0, 500.0), (480.0, 500.0), (1180.0, 900.0)] {+ h.set_size(egui::vec2(width, height));+ h.run_steps(8);+ let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, height));+ let done = h.get_by_label("Xong").rect();+ let quit = h.get_by_label("Thoát ArtCraft Launcher").rect();+ assert!(screen.contains_rect(done), "{width}x{height}: Done is clipped: {done:?}");+ assert!(screen.contains_rect(quit), "{width}x{height}: Quit is clipped: {quit:?}");+ assert!(!done.intersects(quit));+ let title = h.get_all_by_label("Tùy chọn").last().unwrap().rect();+ assert!(screen.contains_rect(title), "{width}x{height}: title is clipped: {title:?}");+ h.get_by_label("Theo hệ thống").scroll_to_me();+ h.run_steps(8);+ let theme = h.get_by_label("Theo hệ thống").rect();+ assert!(theme.bottom() < done.top(), "appearance controls must scroll above the fixed actions");+ h.get_by_label("Tham gia Discord").scroll_to_me();+ h.run_steps(8);+ let discord = h.get_by_label("Tham gia Discord").rect();+ assert!(screen.contains_rect(discord), "community links must be reachable by scrolling");+ let mut links = vec![discord];+ for label in ["Trang web", "GitHub", "X", "Instagram", "LinkedIn", "YouTube", "TikTok"] {+ let candidates: Vec<_> = h+ .query_all_by_label(label)+ .map(|n| n.rect())+ .filter(|r| r.top() >= discord.top() - 1.0 && r.bottom() < done.top() && r.height() > 20.0)+ .collect();+ assert_eq!(candidates.len(), 1, "{width}x{height}: settings link {label}: {candidates:?}");+ links.extend(candidates);+ }+ for (i, r) in links.iter().enumerate() {+ assert!(screen.contains_rect(*r), "{width}x{height}: link is clipped: {r:?}");+ for other in links.iter().skip(i + 1) {+ assert!(!r.intersects(*other), "{width}x{height}: links overlap: {r:?}, {other:?}");+ }+ }+ snapshot(&mut h, &format!("settings-vi-{width}x{height}-bottom"));+ h.get_by_label("Ngôn ngữ").scroll_to_me();+ h.run_steps(8);+ let language = h.get_by_label("Ngôn ngữ").rect();+ assert!(screen.contains_rect(language), "the top of settings must remain reachable");+ snapshot(&mut h, &format!("settings-vi-{width}x{height}-top"));+ }+ h.get_by_label("Xong").click();+ h.run_steps(4);+ assert!(h.query_by_label("Xong").is_none(), "Done must still close the resized dialog");+}