i18n: add Bulgarian (bg) interface translation (#825)

* i18n: add Bulgarian (bg) interface translation

A 2,386-entry Bulgarian catalog: every key of the reference catalog, the equation gallery's
labels and everything the interface audit finds. Written from the meaning of the English
labels in ordinary Bulgarian computing vocabulary; no other product's localisation and no other
catalog in this repository was used. Counts use count-neutral wording ("Думи: 21").

- bg, bg-BG, bg_BG.UTF-8 select it automatically; "Български" in Options and
  ui.language with {"value":"bg"}.
- The interface audit checks Bulgarian coverage the way it checks Russian.
- Tests: locales and saved preference, catalog coverage and key translations, count wording,
  and the bundled Inter/JetBrains Mono faces covering the Bulgarian letters.

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

* Review fixes for #825: Bulgarian built-in style names

Adds the built-in style names Russian translates (Normal Table, Default
Paragraph Font, Annotation Text/Reference/Subject, Envelope Address/Return,
Index Heading, Index {n}, Line Number, Normal Indent, Table of Figures, TOA
Heading, TOC {n}) and Good afternoon; updates the entry count in the docs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 07:56:30 -04:00
1 parent f7ce977e4a
commit e619b12ceb
8 files changed
+2541 -13

No files matched your search

+1
View File
@@ -71,5 +71,6 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
| `crates/ui-egui/src/i18n/de.tsv` | @arekown with Kimi | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | German interface catalog |
| `crates/ui-egui/src/i18n/ru.tsv` | Dmitrii Khlapov with Claude | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Russian interface catalog; reviewed by a native speaker |
| `crates/ui-egui/src/i18n/tr.tsv` | @PbyLoyth with Codex and Claude Code | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Turkish interface catalog |
| `crates/ui-egui/src/i18n/bg.tsv` | Dimitar Klaturov (@bulgariamitko) with Claude Code | Original translation from the English interface labels in this repository; no other product localisation or other catalog used | MIT OR Apache-2.0 | Bulgarian interface catalog |
| `crates/ui-egui/src/i18n/hu.tsv` | @Skaywalker | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | Hungarian interface catalog |
| `crates/ui-egui/src/i18n/fr.tsv` | @pechnikov2026-star | Original translation from the English interface labels in this repository; no other product localisation copied | MIT OR Apache-2.0 | French interface catalog |
+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 or Français (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 Български (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 -1
View File
@@ -110,7 +110,7 @@ Weights and evidence per area: [docs/target-app-parity.md](docs/target-app-parit
| Korean | ko | none | 0% |
| Vietnamese | vi | none | 0% |
Also shipped: Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian (`sr`, `sr-latn`, #250), Russian (`ru`) and Polish (`pl`), all partial (~95%). 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
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.
Proofing: English, Polish and Portuguese (spelling, grammar, hyphenation), chosen per run from its language. Word ships
@@ -198,6 +198,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-12 | Native saves use exclusively created sibling temporary files, synchronize before replacement and clean up on failure; existing temporary-name files/links and original files on failed saves are protected. Synthetic CLI and focused tests on macOS; no new Word parity or readiness claim. |
| 2026-10-11 | Field codes like Word's field keys (#339): show codes (Alt+F9, Shift+F9), edit, unlink, lock and step through fields |
| 2026-10-11 | Word's common keyboard shortcuts (#330): 101 → 125 of ~250 |
| 2026-10-12 | Bulgarian (`bg`) interface: 2,419 catalog entries (every key of the reference catalog, the equation gallery and everything the interface audit finds), `bg-BG` system locales and a saved language choice; locale, catalog and bundled-font tests; no Bulgarian proofing resources |
| 2026-10-11 | Hungarian (`hu`) interface: all 2,419 catalog entries, built-in style names translated, `hu-HU` system locales and a saved language choice; locale, catalog and bundled-font tests; no Hungarian proofing resources |
| 2026-10-11 | Turkish (`tr`) interface: all 1,307 catalog entries, `tr-TR`/`tr-CY` system locales and a saved language choice; command search keeps Turkish dotted and dotless I apart (`italik` finds İtalik); locale, catalog, search and bundled-font tests; no Turkish proofing resources |
| 2026-10-11 | Italian (`it`) interface (#594): 2,399 entries, feature names as Italian Word users know them, locale and saved-preference tests, bundled-font checks; no Italian proofing resources |
@@ -263,6 +264,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-12 | trivial | Native saves protect existing temporary-name entries and originals on pre-replacement failure; synthetic macOS evidence, explicit durability/platform limits; readiness unchanged |
| 2026-10-11 | trivial | Word's common keyboard shortcuts (#330): 125 of ~250 |
| 2026-10-11 | trivial | Collapsed duplicate rows left by parallel merges |
| 2026-10-12 | trivial | Bulgarian interface (`bg`): progress log and shipped languages |
| 2026-10-11 | trivial | Hungarian interface (`hu`): progress log and shipped languages |
| 2026-10-11 | trivial | Turkish interface (`tr`): progress log and shipped languages |
| 2026-10-12 | minor | Native drawing-level ink and its picture fallback survive DOCX edits and repeated saves; adapted to the current graphic writer; rendering and editing InkML remain missing |
+2 -2
View File
@@ -314,7 +314,7 @@ fn untranslated_interface_text() {
complete_catalogs_have_every_string_the_interface_looked_up(pseudo::looked_up());
}
/// Russian, Hungarian and Turkish cover everything the audit saw translated, plus the equation
/// Russian, Hungarian, Turkish and Bulgarian cover everything the audit saw translated, plus the equation
/// gallery's labels (shown in menus the audit doesn't open one by one). Numbered style names go
/// through a template.
fn complete_catalogs_have_every_string_the_interface_looked_up(mut keys: std::collections::BTreeSet<String>) {
@@ -353,7 +353,7 @@ fn complete_catalogs_have_every_string_the_interface_looked_up(mut keys: std::co
)
};
let mut report = String::new();
for code in ["ru", "hu", "tr"] {
for code in ["ru", "hu", "tr", "bg"] {
let lang = super::Lang::from_code(code).unwrap_or(super::Lang::EN);
let missing: Vec<&String> = keys.iter().filter(|s| english_words(s) && !super::has(lang, s) && !numbered(s) && !same(s)).collect();
if !missing.is_empty() {
File diff suppressed because it is too large. Load diff
+5 -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; 18] = [
pub static LANGUAGES: [LangInfo; 19] = [
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 {
@@ -125,6 +125,10 @@ pub static LANGUAGES: [LangInfo; 18] = [
LangInfo { code: "hu", name: "Magyar", source: include_str!("hu.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new() },
// French; `fr`, `fr-FR`, `fr_CA` and other regions resolve here.
LangInfo { code: "fr", name: "Français", source: include_str!("fr.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new() },
// Bulgarian; `bg`, `bg-BG` and `bg_BG.UTF-8` resolve here.
LangInfo {
code: "bg", name: "Български", source: include_str!("bg.tsv"), prefer_hans: false, editing_lang: None, catalog: OnceLock::new()
},
];
impl LangInfo {
+80 -3
View File
@@ -919,9 +919,9 @@ fn polish_covers_every_key_of_every_other_catalog() {
use std::collections::HashSet;
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
let polish = keys(lang("pl").0.source);
// Russian, Italian, Hungarian and Turkish also translate the built-in style names and the equation gallery,
// which the others don't yet; Polish matches every other catalog.
for other in Lang::all().filter(|l| *l != Lang::EN && !matches!(l.code(), "pl" | "ru" | "it" | "hu" | "tr")) {
// Russian, Italian, Hungarian, Turkish and Bulgarian also translate the built-in style names
// and the equation gallery, which the others don't yet; Polish matches every other catalog.
for other in Lang::all().filter(|l| *l != Lang::EN && !matches!(l.code(), "pl" | "ru" | "it" | "hu" | "tr" | "bg")) {
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());
@@ -1109,3 +1109,80 @@ fn bundled_interface_fonts_cover_italian_letters() {
}
}
}
#[test]
fn bulgarian_locales_and_saved_preference_work_without_changing_the_document() {
let bg = lang("bg");
for tag in ["bg", "bg-BG", "bg_BG.UTF-8", "BG-bg", "bg-Cyrl-BG"] {
assert_eq!(lang_from_tag(tag), Some(bg), "{tag}");
}
assert_eq!(first_supported(["ko-KR", "bg-BG", "en-US"]), Some(bg));
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
let before = serde_json::to_value(&app.session.doc).unwrap();
let result = app.run("ui.language", serde_json::json!({"value": "BG"})).unwrap();
assert_eq!(result["effective"], "bg");
assert_eq!(app.ui.language, "bg");
assert!(result["available"].as_array().unwrap().iter().any(|l| l["code"] == "bg" && l["name"] == "Български"));
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!(Lang::from_pref(&restored.language), bg);
assert_eq!(bg.name(), "Български");
}
#[test]
fn bulgarian_covers_the_entire_existing_interface_catalog() {
use std::collections::HashSet;
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
// Every key the interface looks up (Spanish carries them all), and no catalog errors.
assert!(keys(lang("bg").0.source).is_superset(&keys(lang("es").0.source)));
assert!(parse_entries(lang("bg").0.source).1.is_empty());
let bg = lang("bg");
for (source, expected) in [
("Home", "Начало"),
("Insert", "Вмъкване"),
("Font", "Шрифт"),
("Paragraph", "Абзац"),
("Review", "Преглед"),
("Save", "Запазване"),
("Heading 1", "Заглавие 1"),
("Hyphenation", "Сричкопренасяне"),
("Track Changes", "Проследяване на промените"),
("WordCraft", "WordCraft"),
("unknown future label", "unknown future label"),
] {
assert_eq!(tr(bg, source), expected, "{source}");
}
// The search hint's example must find the command it names.
assert_eq!(tr(bg, "Insert Table"), "Вмъкване на таблица");
assert!(tr(bg, "Type a command, e.g. \"insert table\"").contains("вмъкване на таблица"));
assert_eq!(fmt(tr(bg, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Страница 3 от 9");
// Count-neutral wording is grammatical for every count: `Думи: 21`, `Думи: 1 от 21`.
for count in [0, 1, 2, 5, 21, 100, 1000] {
let words = count.to_string();
assert_eq!(fmt(tr(bg, "{words} words"), &[("words", &words)]), format!("Думи: {count}"));
assert_eq!(fmt(tr(bg, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Думи: 1 от {count}"));
}
}
#[test]
fn bundled_interface_fonts_cover_bulgarian_without_system_fallbacks() {
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
let (entries, errors) = parse_entries(lang("bg").0.source);
assert!(errors.is_empty());
let chars: std::collections::HashSet<char> = entries
.iter()
.flat_map(|e| e.translation.chars())
// Cyrillic letters plus the Bulgarian quotes and dash; symbols come from the English sources.
.filter(|c| ('\u{0400}'..='\u{04FF}').contains(c) || matches!(c, '„' | '“' | '—' | '–'))
.chain("АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя".chars())
.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("bg").uses_cjk());
}
+10 -5
View File
@@ -19,9 +19,9 @@ native-speaker review (human).
## What WordCraft ships (measured)
- **18 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` and French `fr`. Catalogs have 2,021–2,419 entries
each; Polish has every key of every other catalog except the style names and equation gallery labels only Russian, Italian, Hungarian and Turkish translate so far (a test checks it). A test (`every_tab_and_command_is_translated`) enforces that every ribbon tab, command
- **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,021–2,419 entries
each; Polish has every key of every other catalog except the style names and equation gallery labels only Russian, Italian, Hungarian, Turkish 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.
- Of the 675 English strings a scan extracts from `tl!(…)` calls and command labels/locations, each
@@ -57,8 +57,8 @@ shared items below.
| 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 |
Other shipped languages: **12** — 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`) and French (`fr`), all partial.
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.
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.
@@ -66,6 +66,10 @@ Turkish works the same way (`tr-TR` and `tr-CY` system locales, `{"value":"tr"}`
entries, the same keys as the Russian catalog. Command search folds the Turkish dotted and dotless I (`italik` finds İtalik, `YAZI TİPİ`
finds Yazı Tipi) while English labels and ASCII command ids keep their usual matching. Turkish
proofing is not included; terminology and checks: [`localization-tr.md`](localization-tr.md).
Bulgarian (`bg-BG` system locales, `{"value":"bg"}`) has 2,419 entries: every key of the reference
catalog, the equation gallery's labels and everything the interface audit finds (the audit checks
Bulgarian as it does Russian). Counts use count-neutral wording (`Думи: 21`). Bulgarian text is
hyphenated by the Bulgarian syllable rules (#826); Bulgarian spelling and grammar are not included.
Hungarian follows `hu-HU` system locales and `{"value":"hu"}`, with a complete catalog of 2,419 entries: every key of
the Russian catalog, built-in style names shown translated, plus the citation style names. Hungarian proofing is not included; terminology and conventions:
[`localization-hu.md`](localization-hu.md).
@@ -117,6 +121,7 @@ allows); all other text is written from the meaning of the English. No Italian p
| 2026-10-11 | minor | Font list shows installed fonts by their names in the interface language (宋体 for SimSun in Chinese, Japanese and Korean names likewise) and finds them by either name; a Chinese, Japanese or Korean font is stored as `w:eastAsia` too (#505) |
| 2026-10-11 | minor | Chinese/Japanese interface fonts: a test checks every catalog character has a glyph; an installed CJK font follows the embedded faces whenever they miss one; macOS font asset folders searched for PingFang (#486) |
| 2026-10-10 | minor | Russian interface (#300; 1,524 entries, built-in style names shown translated; covers everything the interface audit finds) |
| 2026-10-12 | minor | Bulgarian interface catalog (2,419 entries), locale selection, saved preference, audit coverage and font checks |
| 2026-10-11 | minor | Hungarian interface catalog (2,419 entries, built-in style names shown translated; covers everything the interface audit finds), locale selection, saved preference and font coverage checks |
| 2026-10-11 | minor | Turkish interface catalog (1,307 entries), locale selection, saved preference, Turkish casing in command search and font coverage checks; catalog sizes re-counted |
| 2026-10-11 | trivial | English spelling (#520): our supplement `assets/spelling/en-extra.txt` adds ~220 common modern words the Moby list lacks ("desktop", "download", "email"…) |