i18n: add Vietnamese (vi) interface translation (#1458)

* i18n: add Vietnamese (vi) interface translation (#1434)

* vi: translate the strings main gained since the catalog was drafted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Thân Minh Nghĩa
2026-10-14 13:19:39 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 533615620e
commit 1059b196c1
7 changed files with 2621 additions and 10 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes
| **References** | Table of contents, footnotes and endnotes, citations and bibliography (APA, MLA, Chicago, IEEE), captions, table of figures, cross-references, index, table of authorities |
| **Review** | Spelling and grammar with suggestions (English and Polish, by each run's language), thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector |
| **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français or Български (follows the system language by default) |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български or Tiếng Việt (follows the system language by default) |
| **Files** | .docx read/write (opens in Word), Word 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images |
The honest picture, area by area, is in [ROADMAP.md](ROADMAP.md), the
+3 -2
View File
@@ -110,11 +110,12 @@ Weights and evidence per area: [docs/target-app-parity.md](docs/target-app-parit
| Japanese | ja | partial | ~95% |
| German | de | partial (#574) | ~95% |
| Korean | ko | none | 0% |
| Vietnamese | vi | none | 0% |
| Vietnamese | vi | partial (#1434) | ~95% |
Also shipped: Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian (`sr`, `sr-latn`, #250), Russian (`ru`) and Polish (`pl`), all partial (~95%). Bulgarian (`bg`) covers everything the interface audit finds (2,419 entries). Estonian (`et`), Italian (`it`, #594) and Czech (`cs`) now have complete interface catalogs, and Turkish (`tr`) a complete 2,325-entry one (every key of the Russian catalog), all with
system-locale selection and a saved language choice (Italian also has 51 strings none of the others had yet); Estonian, Turkish, Italian and Czech proofing are not included. Hungarian (`hu`) has a
complete 2,419-entry catalog, also without proofing.
complete 2,419-entry catalog, also without proofing. Vietnamese (`vi`, #1434) has a complete
2,448-entry catalog, also without proofing.
Proofing: English, Polish and Portuguese (spelling, grammar, hyphenation), chosen per run from its language. Word ships
30 interface languages and proofing for ~50. Detail: [docs/localization-parity.md](docs/localization-parity.md).
+2
View File
@@ -79,3 +79,5 @@ display_name = "manucarcor"
[people.webdevtodayjason]
display_name = "Jason Brashear"
sync_github_name = false
[people.Nghiathan13]
+3 -1
View File
@@ -52,7 +52,7 @@ pub struct LangInfo {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 19] = [
pub static LANGUAGES: [LangInfo; 20] = [
LangInfo { code: "en", name: "English", source: "", prefer_hans: false, editing_lang: None, catalog: OnceLock::new() },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` resolve here (see `candidates`).
LangInfo {
@@ -137,6 +137,8 @@ pub static LANGUAGES: [LangInfo; 19] = [
LangInfo {
code: "bg", name: "Български", source: include_str!("bg.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new()
},
// Vietnamese; `vi`, `vi-VN` and `vi_VN.UTF-8` resolve here.
LangInfo { code: "vi", name: "Tiếng Việt", source: include_str!("vi.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new() },
];
impl LangInfo {
+88 -1
View File
@@ -926,7 +926,7 @@ fn polish_covers_every_key_of_every_other_catalog() {
// Hungarian and Bulgarian also name the citation styles, and Bulgarian has a few one-line
// pieces of gallery labels. Polish matches every other catalog, including the style names and
// equation gallery Russian translates.
for other in Lang::all().filter(|l| *l != Lang::EN && !matches!(l.code(), "pl" | "it" | "hu" | "bg")) {
for other in Lang::all().filter(|l| *l != Lang::EN && !matches!(l.code(), "pl" | "it" | "hu" | "bg" | "vi")) {
let mut missing: Vec<String> = keys(other.0.source).difference(&polish).cloned().collect();
missing.sort();
assert!(missing.is_empty(), "pl lacks {}'s {missing:?}", other.code());
@@ -1211,3 +1211,90 @@ fn bundled_interface_fonts_cover_bulgarian_without_system_fallbacks() {
}
assert!(!lang("bg").uses_cjk());
}
#[test]
fn vietnamese_locales_and_saved_preference_work_without_changing_the_document() {
let vi = lang("vi");
for tag in ["vi", "vi-VN", "vi_VN.UTF-8", "VI-vi", "vi-Latn-VN", "vi-US"] {
assert_eq!(lang_from_tag(tag), Some(vi), "{tag}");
}
assert_eq!(first_supported([UNSUPPORTED, "vi-VN", "en-US"]), Some(vi));
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_engine::sample::sample_document()), Default::default());
let before = serde_json::to_value(&app.session.doc).unwrap();
let result = app.run("ui.language", serde_json::json!({"value": "VI"})).unwrap();
assert_eq!(result["effective"], "vi");
assert_eq!(app.ui.language, "vi");
assert!(result["available"].as_array().unwrap().iter().any(|l| l["code"] == "vi" && l["name"] == "Tiếng Việt"));
assert_eq!(serde_json::to_value(&app.session.doc).unwrap(), before);
let saved = serde_json::to_string(&app.ui).unwrap();
let restored: crate::UiState = serde_json::from_str(&saved).unwrap();
assert_eq!(restored.language, "vi");
assert_eq!(Lang::from_pref(&restored.language), vi);
assert_eq!(vi.name(), "Tiếng Việt");
}
#[test]
fn vietnamese_covers_existing_catalog_keys_with_an_explicit_preview_fallback() {
use std::collections::HashSet;
let vi = lang("vi");
let (entries, errors) = parse_entries(vi.0.source);
assert!(errors.is_empty(), "{errors:?}");
let vi_keys: HashSet<_> = entries.into_iter().map(|e| (e.context, e.source)).collect();
// The short plain title fits the preview too; this is the only deliberate context fallback.
let preview_title = ("preview".to_string(), "Title".to_string());
assert!(vi_keys.contains(&(String::new(), "Title".to_string())));
assert_eq!(trc(vi, "preview", "Title"), "Tiêu đề");
assert_eq!(trc(vi, "preview", "Title"), tr(vi, "Title"));
// Compare complete (context, source) keys so a contextual entry cannot hide a missing plain
// translation, or silently satisfy another context's key.
for other in Lang::all().filter(|l| *l != Lang::EN && l.code() != "vi") {
let (entries, errors) = parse_entries(other.0.source);
assert!(errors.is_empty(), "{}: {errors:?}", other.code());
let keys: HashSet<_> = entries.into_iter().map(|e| (e.context, e.source)).collect();
let mut missing: Vec<_> = keys.difference(&vi_keys).filter(|key| **key != preview_title).cloned().collect();
missing.sort();
assert!(missing.is_empty(), "vi lacks {}'s {missing:?}", other.code());
}
}
#[test]
fn vietnamese_translations_preserve_templates_and_count_labels() {
let vi = lang("vi");
assert_eq!(tr(vi, "Home"), "Trang chủ");
assert_eq!(tr(vi, "Save"), "Lưu");
assert_eq!(tr(vi, "Spelling & Grammar"), "Chính tả & Ngữ pháp");
assert_eq!(tr(vi, "unknown future label"), "unknown future label");
assert_eq!(fmt(tr(vi, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Đã xuất draft-{words}.docx");
// Count-neutral wording (`Từ: 21`): a Vietnamese noun stays unchanged after a number.
for count in [0, 1, 2, 11, 21, 101] {
let words = count.to_string();
assert_eq!(fmt(tr(vi, "{words} words"), &[("words", &words)]), format!("Từ: {count}"));
assert_eq!(fmt(tr(vi, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Từ: 1 trên {count}"));
}
}
#[test]
fn bundled_interface_fonts_cover_vietnamese_letters() {
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
let (entries, errors) = parse_entries(lang("vi").0.source);
assert!(errors.is_empty(), "{errors:?}");
// Cover the catalog's Latin letters and combining marks, and all 134 non-ASCII Vietnamese
// letters even when a letter is not used by the current translations. U+1EA0..U+1EF9 contains
// the Vietnamese vowel/tone combinations; the other forms are in earlier Latin blocks.
let extra = "ÀàÁáÃãÈèÉéÌìÍíÒòÓóÕõÙùÚúÝýÂâĂăĐđÊêÔôƠơƯưĨĩŨũ";
let chars: std::collections::HashSet<char> = entries
.iter()
.flat_map(|e| e.translation.chars())
.filter(|c| ('\u{00C0}'..='\u{024F}').contains(c) || ('\u{0300}'..='\u{036F}').contains(c) || ('\u{1E00}'..='\u{1EFF}').contains(c))
.chain(extra.chars())
.chain('\u{1EA0}'..='\u{1EF9}')
.collect();
for (family, style) in [("Inter", "Regular"), ("Inter", "Medium"), ("Inter", "SemiBold"), ("JetBrains Mono", "Regular")] {
let face = db.face(family, style);
assert_eq!(face.family, family, "must use the bundled interface face");
for ch in &chars {
assert_ne!(face.glyph_for(*ch), 0, "{family} {style} lacks {ch:?}");
}
}
assert!(!lang("vi").uses_cjk());
}
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -19,8 +19,8 @@ native-speaker review (human).
## What WordCraft ships (measured)
- **19 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, `uk`, `es`, `pt-br`,
Serbian `sr`/`sr-latn` (#250), Estonian `et`, Turkish `tr`, Russian `ru`, Italian `it` (#594), German `de` (#574), Polish `pl`, Czech `cs`, Hungarian `hu`, French `fr` and Bulgarian `bg`. Catalogs have 2,057–2,454 entries
- **20 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, `uk`, `es`, `pt-br`,
Serbian `sr`/`sr-latn` (#250), Estonian `et`, Turkish `tr`, Russian `ru`, Italian `it` (#594), German `de` (#574), Polish `pl`, Czech `cs`, Hungarian `hu`, French `fr`, Bulgarian `bg` and Vietnamese `vi` (#1434). Catalogs have 2,057–2,454 entries
each; Polish has every key of every other catalog, including the built-in style names and the equation gallery, except the citation style names only Hungarian and Bulgarian translate so far (a test checks it). A test (`every_tab_and_command_is_translated`) enforces that every ribbon tab, command
label and ribbon location is translated in every language.
`pl`, `pl-PL` and `pl_PL.UTF-8` system locales pick Polish automatically.
@@ -57,10 +57,10 @@ shared items below.
| Japanese | ja | ~930; 98% / ~95% | most | CJK ✅, IME ✅; no vertical text, ruby or document grid | no | partial | 3–5 |
| German | de | 2,021 entries; same key set as zh-hans (#574); 98% of extracted, ~95% of visible | most | Latin ✅ | no | partial | 1–2 |
| Korean | ko | 0 | ❌ | Hangul ✅; macOS Korean IME fixes pending (#163, #164) | no | none | 3–5 |
| Vietnamese | vi | 0 | ❌ | Latin with stacked diacritics, untested | no | none | 3–4 |
| Vietnamese | vi | 2,448 entries; every key of the reference catalogs (#1434); 98% of extracted, ~95% of visible | most | Latin ✅ | no | partial | 0–1 (review) |
Other shipped languages: **13** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian
Cyrillic and Latin (`sr`, `sr-latn`, #250), Estonian (`et`), Turkish (`tr`), Russian (`ru`), Italian (`it`, #594), Polish (`pl`), Czech (`cs`), Hungarian (`hu`), French (`fr`) and Bulgarian (`bg`), all partial.
Other shipped languages: **14** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian
Cyrillic and Latin (`sr`, `sr-latn`, #250), Estonian (`et`), Turkish (`tr`), Russian (`ru`), Italian (`it`, #594), Polish (`pl`), Czech (`cs`), Hungarian (`hu`), French (`fr`), Bulgarian (`bg`) and Vietnamese (`vi`, #1434), all partial.
Estonian uses the existing fonts, follows `et-EE` system locales, and is available in Options or
through `ui.language` with `{"value":"et"}`. Its 1,307 entries cover the existing interface catalog.
Estonian spelling, grammar, dates and document templates are not added by the interface catalog.