i18n: add Vietnamese (vi) interface localization

This commit is contained in:
Tống Huy Sơn
2026-10-22 11:33:34 +07:00
parent a86897dc91
commit e3bf9b7270
10 changed files with 2395 additions and 22 deletions
+1 -1
View File
@@ -230,7 +230,7 @@ The desktop app also offers a loopback-only control channel (`figcraft --control
- **Its own renderer:** tiny-skia on every core for the canvas and exports, with layer blending, effects and masks; booleans from linesweeper, geometry from kurbo, text from skrifa and HarfRust.
- **Runs in the browser:** the whole engine and UI compile to WebAssembly.
- **Accessible:** screen readers get names, roles, values and states for the tools, layer rows, fields, dropdowns, tabs and menus (AccessKit on desktop), F6 moves the keyboard between the toolbar, layers, canvas and properties (Tab within them, Escape back to the canvas), the system's reduce-motion setting is honoured, and panel text meets WCAG AA contrast in both themes.
- **Languages:** English, Russian and Turkish (reviewed by native speakers), and Spanish, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian and Georgian drafts, picked in Preferences or taken from the system or browser and switched live; adding a language is one catalog file ([`docs/translating.md`](docs/translating.md)).
- **Languages:** English, Russian and Turkish (reviewed by native speakers), and Spanish, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian, Georgian and Vietnamese drafts, picked in Preferences or taken from the system or browser and switched live; adding a language is one catalog file ([`docs/translating.md`](docs/translating.md)).
- **Plugins:** sandboxed WebAssembly plugins (a metered, pure-Rust interpreter with a memory cap and no file or network access, in the browser too) that act through the same commands, each run one undo step, within the permissions their manifest asks for. A Rust SDK and three example plugins are in [`plugins/`](plugins); see [`docs/plugins.md`](docs/plugins.md).
- **Live sessions (preview):** share the open file from the app (Share ▸ Start sharing: online through a server, or on your local network), give the others the short join code it shows (or the invite link), and edit it together: everyone's changes sync live, with cursors, selections, avatars and follow, and undo takes back only your own changes. Others join from the desktop app (invite links open it) or from the browser build. See [`docs/collab-protocol.md`](docs/collab-protocol.md).
- **A server of your own (preview):** `figcraft-server` keeps a team's files, publishes them from the app (File ▸ Publish to server…, or Share ▸ Online with your server in Preferences) and hands out share links and join codes that let people edit or only view; files survive restarts and apps reconnect where they were. No accounts yet: links are the access. Releases ship it for Linux, and the desktop app joins over `wss://`; [hosting it on a rented Linux server](docs/server.md#deploy-on-a-linux-server) takes a systemd unit and Caddy or nginx (or Docker). See [`docs/server.md`](docs/server.md).
+2 -2
View File
@@ -109,11 +109,11 @@ native speakers' reviews of the six language drafts need people.
| Japanese | ja | 99% | draft (needs native review) |
| German | de | 96% | draft (needs native review) |
| Korean | ko | 95% | draft (needs native review) |
| Vietnamese | vi | 0% | none |
| Vietnamese | vi | 100% | draft (needs native review) |
Percentages measured 2026-10-17 over 2,272 entries (strings added since a draft show English). Also shipped: Russian (`ru`, 99%, reviewed by VanTan), Swedish (`sv`, 99.6%, command labels since 2026-10-17; native review welcome), Turkish (`tr`, 99%) and Czech (`cs`, 99%, draft). Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese
(Brazil); FigCraft has the localization layer, English and Spanish (Spain and Latin America),
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese and Indonesian drafts (Preferences ▸
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Vietnamese drafts (Preferences ▸
Language; Traditional Chinese has no catalog; CJK UI
text uses an installed CJK font, loaded only when needed; adding a language:
[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping,
+1 -1
View File
@@ -427,7 +427,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"pref.language",
"Interface language",
"{language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|zh-Hans|…; else English}",
"{language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|vi|zh-Hans|…; else English}",
false,
|s, p| {
let lang = req_string("pref.language", p, "language")?;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -157,7 +157,7 @@ mod tests {
assert_eq!(language("zh-Hans"), HAN);
assert_eq!(language("bn"), BENGALI);
assert_eq!(language("fr"), 0);
assert!(["en", "es", "es-419", "fr", "de", "pt-BR", "tr", "id"].iter().all(|c| can_show(c)));
assert!(["en", "es", "es-419", "fr", "de", "pt-BR", "tr", "id", "vi"].iter().all(|c| can_show(c)));
}
#[test]
+2 -2
View File
@@ -176,7 +176,7 @@ fn plural(code: &str, n: usize) -> Plural {
"ru" if n % 10 == 1 && n % 100 != 11 => Plural::One,
"ru" | "pl" if (2..=4).contains(&(n % 10)) && !(12..=14).contains(&(n % 100)) => Plural::Few,
"ru" | "pl" => Plural::Many,
"ja" | "ko" | "zh-Hans" => Plural::Other,
"ja" | "ko" | "zh-Hans" | "vi" => Plural::Other,
"fr" | "pt-BR" if n <= 1 => Plural::One,
_ if n == 1 => Plural::One,
_ => Plural::Other,
@@ -187,7 +187,7 @@ fn plural(code: &str, n: usize) -> Plural {
// "other" wording. Czech supplies separate few forms; Russian and Polish supply few/many forms.
/// Whether count `n` takes the "one" form: 0 and 1 in French and Brazilian Portuguese, never in
/// Japanese, Korean and Chinese (no plural forms: always "other"), 1 elsewhere (CLDR's integer rules).
/// Japanese, Korean, Chinese and Vietnamese (no plural forms: always "other"), 1 elsewhere (CLDR's integer rules).
#[cfg(test)]
fn plural_one(lang: usize, n: usize) -> bool {
plural(LANGS.get(lang).map_or("en", |l| l.code), n) == Plural::One
+6 -2
View File
@@ -349,6 +349,7 @@ fn system_languages_pick_the_first_one_with_a_catalog() {
assert_eq!(code("system", Some("pt-BR")), "pt-BR");
assert_eq!(code("system", Some("tr_TR.UTF-8")), "tr");
assert_eq!(code("system", Some("id_ID.UTF-8")), "id");
assert_eq!(code("system", Some("vi_VN.UTF-8")), "vi");
// Portugal's Portuguese and a bare "pt" read the Brazilian catalog.
assert_eq!(code("system", Some("pt-PT")), "pt-BR");
assert_eq!(code("pt", None), "pt-BR");
@@ -417,12 +418,15 @@ fn french_and_portuguese_count_zero_as_singular() {
}
#[test]
fn japanese_korean_and_chinese_have_no_plural_forms() {
fn japanese_korean_chinese_and_vietnamese_have_no_plural_forms() {
let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code;
assert_eq!(code("system", Some("ja-JP")), "ja");
assert_eq!(code("system", Some("ko_KR.UTF-8")), "ko");
assert_eq!(code("system", Some("vi-VN")), "vi");
assert_eq!(code("system", Some("ja,en-US")), "ja");
for code in ["ja", "ko", "zh-Hans"] {
assert_eq!(LANGS[resolve("vi", None)].name, "Tiếng Việt");
assert!(LANGS[resolve("vi", None)].draft);
for code in ["ja", "ko", "zh-Hans", "vi"] {
let lang = LANGS.iter().position(|l| l.code == code).unwrap();
assert!(!plural_one(lang, 0) && !plural_one(lang, 1) && !plural_one(lang, 2), "{code}");
let ones: Vec<_> = LANGS[lang].ui.iter().filter(|(_, t)| matches!(t, Text::Plural { one: Some(_), .. })).map(|(k, _)| k).collect();
+6 -6
View File
@@ -4,7 +4,7 @@
FigCraft's interface has a localization layer with an English catalog, a Russian catalog reviewed
by a native speaker, and Spanish (with a Latin American overlay), French, German, Brazilian
Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts (machine-drafted,
Portuguese, Japanese, Korean, Simplified Chinese, Indonesian, Vietnamese and Czech drafts (machine-drafted,
waiting for native speakers' review), plus Swedish and Turkish catalogs. This page lists the twelve key
languages of the craftrules standard, then the other languages Figma ships. How to add a
language: [`translating.md`](translating.md).
@@ -32,7 +32,7 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
channel stay English. Accessibility names come from the same strings.
- **Language preference:** Preferences ▸ Language (System, then each catalog by its own name:
English, Čeština, Deutsch, Español, Español (Latinoamérica), Français, Bahasa Indonesia, 日本語,
한국어, Português (Brasil), Русский, Svenska, Türkçe, 简体中文), applied at once without a
한국어, Português (Brasil), Русский, Svenska, Türkçe, Tiếng Việt, 简体中文), applied at once without a
restart; System takes the first of the OS's preferred languages that has a catalog (on Windows
only the first preferred language counts, since that list also holds the languages added for a
keyboard; macOS AppleLanguages, `LANGUAGE` then the locale variables on Linux and BSD; a regional
@@ -53,7 +53,7 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
- **Not yet localized:** messages the engine writes (errors, a few toasts, Dev Mode property values
such as "inside"), number formatting (decimal points stay `.`), and decimal plural rules
(`trn` takes integers: Czech uses one/few/other and Russian one/few/many; French and Brazilian
Portuguese take the singular for 0 as well; Japanese, Korean and Chinese have one form;
Portuguese take the singular for 0 as well; Japanese, Korean, Chinese and Vietnamese have one form;
CLDR's "many" for millions in French, Spanish and Portuguese reads as the plural).
- **Script support in documents:** Unicode bidi with Arabic shaping and mirrored brackets in text
layers; right-to-left list markers and indents; right-to-left names in the layers panel, tabs
@@ -96,7 +96,7 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
| Japanese | ja | 2,254 / 2,272 (99.2%, draft) | yes | CJK text and IME in documents; UI text in an installed Japanese face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| German | de | 2,176 / 2,272 (95.8%, draft) | yes | Latin; long words elide in narrow fields | no | draft | 1–2 (after native review) |
| Korean | ko | 2,166 / 2,272 (95.3%, draft) | yes | Hangul text and IME in documents; UI text in an installed Korean face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| Vietnamese | vi | 0 (0%) | no | Latin with stacked diacritics | no | none | 3–5 |
| Vietnamese | vi | 2,352 / 2,352 (100%, draft) | yes | Latin with stacked diacritics | no | draft | 1–2 (after native review) |
Other languages (measured 2026-10-17): Russian (`ru`) 2,244 / 2,272 (98.8%), reviewed and approved
by VanTan, one/few/many integer plurals; Swedish (`sv`) 2,264 (99.6%), its command labels and menu
@@ -113,8 +113,8 @@ right-to-left mirroring of the panels is in Arabic's hours. Total: **29–51 h**
every string adds).
Priority: native reviews of the drafts and the es-419 overlay (every one of Figma's languages
and variants has one, plus Simplified Chinese, Indonesian and Czech, which Figma doesn't ship), then
Hindi, Vietnamese and Arabic. Traditional Chinese is not planned (no catalog; its users see English).
and variants has one, plus Simplified Chinese, Indonesian, Vietnamese and Czech, which Figma doesn't ship), then
Hindi and Arabic. Traditional Chinese is not planned (no catalog; its users see English).
## Revision history
+1 -1
View File
@@ -436,7 +436,7 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
- `pref.startOnHome {on?}`: the desktop app starts on the Home screen instead of in a new file
(off by default; files named on the command line still open, and `--control` starts in a new
file anyway, so control callers have a document).
- `pref.language {language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|zh-Hans|…}`: the app's interface language, applied live ("system":
- `pref.language {language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|vi|zh-Hans|…}`: the app's interface language, applied live ("system":
the OS or browser language; a language without a catalog shows English). Command ids, labels and
every result stay English.
+8 -6
View File
@@ -17,7 +17,7 @@ Simplified.
The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay
`es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese
(`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`), Indonesian (`id`), Finnish
(`fi`), Polish (`pl`) and Bengali (`bn`), machine-drafted and waiting for native speakers' review. Georgian
(`fi`), Polish (`pl`), Bengali (`bn`) and Vietnamese (`vi`), machine-drafted and waiting for native speakers' review. Georgian
(`ka`) was written with AI assistance and reviewed by a native speaker, and stays a draft until a
maintainer changes its status. Russian (`ru`, Русский) was
reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode and Smart animate as familiar
@@ -61,7 +61,9 @@ formal Anda (never kamu), keeps established design terms in English (frame, laye
instance, auto layout, plugin, file) and keeps a noun unchanged after a count, so its plural entries
give only `other`. Georgian has no letter case (never write Mtavruli for capitals), uses verbal nouns
for commands (გვერდის დამატება) and keeps a noun singular after a numeral, so counts read
"შრე: {n}" and its plural entries give the same text for `one` and `other`.
"შრე: {n}" and its plural entries give the same text for `one` and `other`. Vietnamese uses concise
imperatives for buttons and menu items (Lưu, Xóa) and keeps a noun unchanged after a count, so its
plural entries give only `other`.
CJK text needs a CJK font, which egui and Inter lack: the desktop app adds an installed one when
something on screen needs it (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts,
@@ -80,8 +82,8 @@ picker leaves Georgian out. A new CJK or other non-Latin language also needs its
set `[meta] name` and `status = "draft — needs native review"`.
2. Translate every value; keep the keys. Keep `{placeholders}` exactly, "…" where English has it,
brand names and keyboard glyphs. English labels of commands: `figcraft-cli commands`.
3. Plurals: give `one` only if the language has a singular form (Japanese, Korean and Chinese give
only `other`). `other` is required; `one`, `few` and `many` are optional. `trn` picks `one` for
3. Plurals: give `one` only if the language has a singular form (Japanese, Korean, Chinese and
Vietnamese give only `other`). `other` is required; `one`, `few` and `many` are optional. `trn` picks `one` for
1 (also 0 in French and Brazilian Portuguese). Russian uses `one` for 1, 21, 31… except 11;
`few` for 2–4, 22–24… except 12–14; and `many` for the other integers, including 0. Keep `{n}`
in all Russian forms. Add new rules to `plural` in
@@ -94,8 +96,8 @@ picker leaves Georgian out. A new CJK or other non-Latin language also needs its
4. To require full coverage, add the code to `COMPLETE` in
[`crates/ui-egui/src/i18n_tests.rs`](../crates/ui-egui/src/i18n_tests.rs). Every later PR that
adds a string then has to translate it into each complete language, so only Spanish is there:
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian and
Georgian are registered as drafts, Russian and Turkish are reviewed but not required, and a string added
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian,
Georgian and Vietnamese are registered as drafts, Russian and Turkish are reviewed but not required, and a string added
later shows English in them until it is translated. To list what a draft lacks,
add its code to `COMPLETE` locally and run the check.
5. Run the check and look at the panels in the app (longer words must fit the right panel):