i18n: add Czech (cs) interface language (#639)

Squash of PR #639 by @Losminek.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Lošák 2026-10-12 02:11:51 +02:00 committed by GitHub
parent 3aeedf4856
commit 6d7c652b65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1408 additions and 51 deletions

View File

@ -60,3 +60,4 @@ Generated-in-code assets are original and have no file to list:
| `crates/ui-egui/src/i18n/de.tsv` | the CADCraft contributors | Original German translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/es.tsv` | @PedroGuille | Original Spanish translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/pt_br.tsv` | @WladmirJunior | Original Brazilian Portuguese translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/cs.tsv` | @Losminek (AI-assisted) | Original Czech translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |

View File

@ -111,7 +111,7 @@ numbers):
| Layouts & plotting | Paper-space layouts, viewports (scale, lock, per-viewport layer freeze), MSPACE/PSPACE through viewports, page setups, PLOT and EXPORTPDF |
| Parametric | Geometric and dimensional constraints, AUTOCONSTRAIN, PARAMETERS with expressions, conflict detection; constraints re-solve after every edit |
| Grips | Hot grips with Space to cycle Stretch, Move, Rotate, Scale and Mirror |
| Interface languages | English, Ukrainian (Українська) and German (Deutsch), live switching and a remembered Auto/system-language choice; [details](docs/localization.md) |
| Interface languages | English, Ukrainian (Українська), Brazilian Portuguese (Português (Brasil)), German (Deutsch), Spanish (Español) and Czech (Čeština), live switching and a remembered Auto/system-language choice; [details](docs/localization.md) |
| Automation | MCP server, JSON control channel, `cadcraft-cli` (info, convert, run, commands, mcp) |
## Quick start

View File

@ -57,7 +57,7 @@ impl NativeMenu {
items.insert("cc-settings".into(), ("options".into(), "Settings...".into(), settings));
let _ = menu.append(&app_menu);
for (title, entries) in menus::tree(app) {
let sub = Submenu::new(i18n::tr(language, &title), true);
let sub = Submenu::new(i18n::tr_menu(language, &title), true);
build(&sub, &entries, &mut items, &mut submenus, &mut counter, language);
let _ = menu.append(&sub);
submenus.push((title, sub));
@ -87,7 +87,7 @@ impl NativeMenu {
item.set_text(i18n::tr(language, label));
}
for (label, submenu) in &self.submenus {
submenu.set_text(i18n::tr(language, label));
submenu.set_text(i18n::tr_menu(language, label));
}
}
let now = cadcraft_ui_egui::now_ms();
@ -113,7 +113,7 @@ fn build(
for e in entries {
match e {
Entry::Sub { label, children } => {
let sub = Submenu::new(i18n::tr(language, label), true);
let sub = Submenu::new(i18n::tr_menu(language, label), true);
build(&sub, children, items, submenus, counter, language);
let _ = parent.append(&sub);
submenus.push((label.clone(), sub));

View File

@ -45,3 +45,6 @@ sync_github_name = true
[people.WladmirJunior]
real_name = "Wladmir Júnior"
sync_github_name = true
[people.Losminek]
sync_github_name = true

View File

@ -165,7 +165,7 @@ pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
});
response.on_hover_text("More toolbar commands");
response.on_hover_text(crate::tl!("More toolbar commands"));
}
if let Some(c) = clicked {
app.start(c);

File diff suppressed because it is too large Load Diff

View File

@ -23,10 +23,12 @@ pub enum Preference {
German,
#[serde(rename = "es")]
Spanish,
#[serde(rename = "cs")]
Czech,
}
impl Preference {
pub const ALL: [Self; 6] = [Self::Auto, Self::English, Self::Ukrainian, Self::BrazilianPortuguese, Self::German, Self::Spanish];
pub const ALL: [Self; 7] = [Self::Auto, Self::English, Self::Ukrainian, Self::BrazilianPortuguese, Self::German, Self::Spanish, Self::Czech];
/// The codes a preference can be given as, for messages ("auto, en or uk").
pub fn codes() -> String {
@ -45,6 +47,7 @@ impl Preference {
Self::BrazilianPortuguese => "pt-BR",
Self::German => "de",
Self::Spanish => "es",
Self::Czech => "cs",
}
}
@ -56,6 +59,7 @@ impl Preference {
Self::BrazilianPortuguese => "Português (Brasil)",
Self::German => "Deutsch",
Self::Spanish => "Español",
Self::Czech => "Čeština",
}
}
@ -75,6 +79,7 @@ impl Preference {
Self::BrazilianPortuguese => "pt-BR",
Self::German => "de",
Self::Spanish => "es",
Self::Czech => "cs",
Self::Auto => locales.iter().find_map(|s| locale_language(s)).unwrap_or("en"),
}
}
@ -91,7 +96,7 @@ pub struct Language {
catalog: OnceLock<Catalog<'static>>,
}
pub static LANGUAGES: [Language; 5] = [
pub static LANGUAGES: [Language; 6] = [
Language { code: "en", name: "English", source: "", plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
Language {
code: "uk", name: "Українська", source: include_str!("uk.tsv"), plural: plural_uk, plural_forms: 3, catalog: OnceLock::new()
@ -108,6 +113,7 @@ pub static LANGUAGES: [Language; 5] = [
Language { code: "de", name: "Deutsch", source: include_str!("de.tsv"), plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
// Spanish integer plurals follow the English rule too.
Language { code: "es", name: "Español", source: include_str!("es.tsv"), plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
Language { code: "cs", name: "Čeština", source: include_str!("cs.tsv"), plural: plural_cs, plural_forms: 3, catalog: OnceLock::new() },
];
/// BCP 47 and POSIX locale tags, including `uk-UA`, `uk_UA.UTF-8` and `pt_BR.UTF-8`.
@ -168,6 +174,16 @@ pub fn t(source: &str) -> &str {
CURRENT.with(|l| tr(l.get(), source))
}
/// Menu headings can differ from command or object labels (e.g. Czech Kótování / Kóta).
/// Catalogs without a `menu` entry retain their ordinary translation.
pub fn tr_menu<'a>(language: &str, source: &'a str) -> &'a str {
catalog(language).and_then(|c| c.get(&("menu", source))).copied().unwrap_or_else(|| tr(language, source))
}
pub fn menu(source: &str) -> &str {
CURRENT.with(|l| tr_menu(l.get(), source))
}
/// Named placeholders allow translations to reorder values. Inserted values are never looked up
/// or substituted again, so a user's name containing `{n}` is preserved verbatim.
pub fn format(source: &str, values: &[(&str, String)]) -> String {
@ -213,6 +229,15 @@ fn plural_en(n: u64) -> usize {
usize::from(n != 1)
}
/// Czech integer plurals: one (1), few (2–4), other (0 and 5 or more).
pub fn plural_cs(n: u64) -> usize {
match n {
1 => 0,
2..=4 => 1,
_ => 2,
}
}
pub fn count(n: u64, one: &str, other: &str) -> String {
let language = CURRENT.get();
let source = catalog(language).and_then(|c| c.get(&("@plural", one))).copied();

View File

@ -15,7 +15,7 @@ fn catalog_is_valid_and_preserves_placeholders_and_ellipses() {
let (catalog, errors) = parse(language.source);
assert!(errors.is_empty(), "{}: {errors:?}", language.code);
for ((context, key), value) in catalog {
assert!(["", "@plural"].contains(&context), "unknown context: {context}");
assert!(["", "menu", "@plural"].contains(&context), "unknown context: {context}");
let forms: Vec<_> = if context == "@plural" { value.split('|').collect() } else { vec![value] };
if context == "@plural" {
assert_eq!(forms.len(), language.plural_forms, "{}: {key}", language.code);
@ -39,7 +39,7 @@ fn catalog_is_valid_and_preserves_placeholders_and_ellipses() {
/// to English for what they don't cover yet; their gaps are reported, not enforced, so adding a
/// command doesn't require a row in every language.
const REQUIRED: &[&str] = &["uk"];
const REPORTED: &[&str] = &["pt-BR", "de", "es"];
const REPORTED: &[&str] = &["pt-BR", "de", "es", "cs"];
fn check(language: &str, what: &str, missing: Vec<String>) {
if REQUIRED.contains(&language) {
@ -101,6 +101,7 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
include_str!("../parametric.rs"),
include_str!("../quick.rs"),
include_str!("../vpscale.rs"),
include_str!("../options/dialog.rs"),
];
let mut missing = BTreeSet::new();
for source in sources {
@ -388,3 +389,118 @@ fn default_fonts_cover_every_ukrainian_letter_in_the_catalog() {
}
});
}
#[test]
fn czech_locales_preferences_and_plural_forms() {
for tag in ["cs", "cs-CZ", "cs_CZ.UTF-8", "CS-cz", " cs-CZ ", "cs_CZ@euro"] {
assert_eq!(locale_language(tag), Some("cs"), "{tag}");
}
for tag in ["cz", "csharp", "csomething"] {
assert_eq!(locale_language(tag), None, "{tag}");
}
assert_eq!(Preference::parse("cs"), Some(Preference::Czech));
assert_eq!(Preference::Czech.name(), "Čeština");
assert_eq!(Preference::Czech.resolve(&["en-US".into()]), "cs");
assert_eq!(Preference::Auto.resolve(&["pl-PL".into(), "cs-CZ".into(), "en-US".into()]), "cs");
let mut app = CadApp::new(Session::new(), Services::default());
app.ui.interface_language = Preference::Czech;
let mut restored = CadApp::new(Session::new(), Services::default());
restored.load_prefs(&app.prefs_json());
assert_eq!(restored.ui.interface_language, Preference::Czech);
with_language("cs", || {
for (n, form) in [(0, 2), (1, 0), (2, 1), (4, 1), (5, 2), (11, 2), (21, 2), (22, 2), (25, 2), (u64::MAX, 2)] {
assert_eq!(plural_cs(n), form, "{n}");
let contributor = ["přispěvatel", "přispěvatelé", "přispěvatelů"][form];
let commit = ["commit", "commity", "commitů"][form];
assert_eq!(count(n, "{n} contributor", "{n} contributors"), format!("{n} {contributor}"));
assert_eq!(count(n, "{n} commit", "{n} commits"), format!("{n} {commit}"));
}
assert_eq!(format("Current: {color}", &[("color", "{n} Open".into())]), "Aktuální: {n} Open");
});
}
#[test]
fn czech_menu_headings_keep_object_labels_and_other_catalogs() {
with_language("cs", || {
assert_eq!(menu("View"), "Zobrazení");
assert_eq!(t("View"), "Pohled");
assert_eq!(menu("Dimension"), "Kótování");
assert_eq!(t("Dimension"), "Kóta");
assert_eq!(menu("Draw"), "Kreslení");
assert_eq!(menu("Modify"), "Modifikace");
assert_eq!(menu("Edit"), "Úpravy");
assert_eq!(menu("File"), "Soubor");
});
for language in ["en", "uk", "de", "es", "pt-BR", "unknown"] {
for source in crate::menus::MENUS {
assert_eq!(tr_menu(language, source), tr(language, source));
}
}
}
#[test]
fn live_czech_switch_preserves_drawing_command_and_saved_choice() {
let mut app = CadApp::new(Session::new(), Services::default());
app.ui.in_window_menu = true;
app.session.state_mut().unwrap().title = "Open".into();
let ctx = egui::Context::default();
app.logic(&ctx);
assert!(render(&mut app, &ctx).contains("Properties"));
app.start("line");
let prompt = crate::control::cmdline_state(&app);
let commands = crate::control::all_commands(&app);
app.run("ui.language", json!({"lang":"cs"})).unwrap();
let text = render(&mut app, &ctx);
for label in ["Vlastnosti", "Soubor", "Zobrazení", "Kreslení", "Kótování", "Určete první bod"] {
assert!(text.contains(label), "missing {label}: {text}");
}
assert_eq!(app.language(), "cs");
assert_eq!(app.session.state().unwrap().title, "Open");
assert_eq!(crate::control::cmdline_state(&app), prompt);
assert_eq!(crate::control::all_commands(&app), commands);
assert_eq!(t("Properties"), "Properties");
app.run("ui.resetpalettes", json!({})).unwrap();
assert_eq!(app.language(), "cs");
app.run("ui.language", json!({"lang":"en"})).unwrap();
assert!(render(&mut app, &ctx).contains("Properties"));
}
#[test]
fn czech_dialogs_render_with_stable_ids() {
for (dialog, heading) in [
("dsettings", "Nastavení kreslení"),
("about", "O aplikaci CADCraft"),
("commands", "Přehled příkazů"),
("blocks", "Bloky"),
("layers", "Správce vlastností hladin"),
("qselect", "Rychlý výběr"),
("parameters", "Správce parametrů"),
("language", "Čeština"),
("options:preferences", "Uživatelské předvolby"),
] {
let mut app = CadApp::new(Session::new(), Services::default());
app.ui.interface_language = Preference::Czech;
app.ui.dialog = Some(dialog.into());
let ctx = egui::Context::default();
app.logic(&ctx);
let text = render(&mut app, &ctx);
assert!(text.contains(heading), "{dialog}: {text}");
assert_eq!(app.ui.dialog.as_deref(), Some(dialog));
}
}
#[test]
fn default_fonts_cover_czech_letters_and_typographic_quotes() {
let ctx = egui::Context::default();
let mut output = ctx.run_ui(Default::default(), |_| {});
output.textures_delta.clear();
let letters: BTreeSet<_> = "ÁČĎÉĚÍŇÓŘŠŤÚŮÝŽáčďéěíňóřšťúůýž„“".chars().collect();
ctx.fonts_mut(|view| {
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
let mut font = view.fonts.font(&family);
for c in &letters {
assert!(font.characters().contains_key(c), "missing {c} in {family:?}");
}
}
});
}

View File

@ -1203,6 +1203,7 @@
Multiline... Мультилінія...
Multiline Style... Стиль мультилінії...
Solid Editing Редагування тіл
More toolbar commands Інші команди панелі інструментів
Default save version Типова версія збереження
Automatic save every Автозбереження кожні
min хв

Can't render this file because it has a wrong number of fields in line 8.

View File

@ -456,7 +456,7 @@ pub(crate) fn entry_ui(ui: &mut egui::Ui, e: &Entry, clicked: &mut Option<String
}
}
Entry::Sub { label, children } => {
ui.menu_button(crate::i18n::t(label), |ui| {
ui.menu_button(crate::i18n::menu(label), |ui| {
for c in children {
entry_ui(ui, c, clicked);
}
@ -474,7 +474,7 @@ pub fn menu_bar(app: &mut CadApp, ui: &mut egui::Ui) {
|ui| {
egui::MenuBar::new().ui(ui, |ui| {
for (name, entries) in &tree {
ui.menu_button(crate::i18n::t(name), |ui| {
ui.menu_button(crate::i18n::menu(name), |ui| {
for e in entries {
entry_ui(ui, e, &mut clicked);
}

View File

@ -9,7 +9,7 @@
//! | name | value |
//! |---|---|
//! | `theme` | `"system"`, `"light"` or `"dark"` |
//! | `interfaceLanguage` | `"auto"`, `"en"` or `"uk"` |
//! | `interfaceLanguage` | `"auto"`, `"en"`, `"uk"`, `"pt-BR"`, `"de"`, `"es"` or `"cs"` |
//! | `saveFormat` | `"dxf"` or `"dwg"`: the format Save As suggests for new drawings |
//! | `saveVersion` | `"2000"` … `"2018"`: the version drawings with no file yet are saved in |
//! | `SAVETIME` | minutes between automatic saves, 0–600 (0: off) |

View File

@ -115,7 +115,7 @@ Select a language with `engine.execute` and the UI command `ui.language`:
{"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"uk"}}}
```
Supported values are `auto`, `en`, `uk` and `de`. `ui.set` also accepts
Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es` and `cs`. `ui.set` also accepts
`{"interfaceLanguage":"uk"}`; `ui.inspect` reports the saved choice in its UI state.
Invalid choices leave the current language unchanged. Menus, command labels and prompt text
returned by the control channel retain their English source text. See [localization.md](localization.md).

View File

@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (German catalog landed: second non-English language, per-language plural rules) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Czech catalog added: reference/literal coverage, Czech plurals and menu headings) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)
How far CADCraft's interface and drawing text are from AutoCAD's languages. Summarised in
[ROADMAP.md](../ROADMAP.md#languages); work items in [gaps.md](gaps.md#localization).
@ -64,8 +64,10 @@ plus native review by a human; extending the catalog to the remaining English-on
| Korean | ko | 0 (0%) | 0% | Hangul renders; macOS Korean IME fixes pending (#118, #119) | no | none | 4–6 h |
| Vietnamese | vi | 0 (0%) | 0% | Latin with stacked diacritics: renders per code point, untested | no | none | 3–5 h |
| Ukrainian | uk | 989 catalog rows (≈ 100% of the catalog; ≈ 80% of all UI strings, estimated: engine diagnostics and uncatalogued text stay English) | dialogs and tooltips yes; command reference searchable in Ukrainian | Cyrillic: yes (bundled egui fonts) | translated by a contributor; no second review | partial | 2–4 h |
| Czech | cs | 1,202 catalog rows; reference catalog and marked UI literals covered | dialogs and tooltips yes; contextual menu headings; command reference searchable in Czech | Latin: yes (bundled egui proportional and monospace fonts; Czech letters and quotes tested) | AI-assisted by @Losminek; independent review pass; not yet proofread by a native speaker | complete current catalog; English-only interface gaps remain | native review + shared English-only UI work |
Other languages shipped: Ukrainian, Brazilian Portuguese and German (all partial). AutoCAD for Mac ships 7
Other languages shipped: Ukrainian, Brazilian Portuguese, German and Czech. Czech covers the
reference catalog and marked UI literals; engine diagnostics and uncatalogued text remain English. AutoCAD for Mac ships 7
non-English languages (zh-Hans, es, fr, ja, de, ko, it); of those, CADCraft now ships German,
and additionally Ukrainian, which AutoCAD for Mac doesn't ship.
@ -77,6 +79,7 @@ each language, which needs humans. Translations parallelise fully once the catal
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Czech added: 1,202 rows, one/few/other plurals, locale/preferences/menu/font/render tests; independent review pass; native proofreading pending |
| 2026-10-11 | minor | German catalog landed: `de` preference and catalog, per-language plural rules; non-English languages 1 → 2; total hours 60–100 → 55–95 |
| 2026-10-10 | minor | Right-to-left drawing text: bidi and Arabic-script shaping in TEXT and MTEXT (#287) |
| 2026-10-10 | minor | Ukrainian catalog landed on main (#36): catalog infrastructure exists, Ukrainian partial; total hours 70–110 → 60–100 |

View File

@ -1,46 +1,42 @@
# Interface languages
Choose **Edit → Interface Language** and select **Auto (system language)**, **English**,
**Українська**, or **Deutsch**. The interface updates without restarting, including the native
macOS menus.
**Українська**, or **Português (Brasil)**. The interface updates without restarting, including the native macOS menus.
**Українська**, **Português (Brasil)**, **Deutsch**, **Español**, or **Čeština** (Czech).
The interface updates without restarting, including the native macOS menus.
The choice is saved with the other interface preferences (`cadcraft.prefs`, beside the theme):
eframe storage on the desktop, browser local storage on the web. Installations without a saved
choice use Auto.
Auto uses the first supported language in the operating system's preferred locales, or the
browser's `navigator.languages` on the web. Ukrainian tags such as `uk-UA` and `uk_UA.UTF-8`
resolve to `uk`, German tags such as `de-DE` and `de_AT.UTF-8` resolve to `de`; unsupported
languages fall back to English. Set `CADCRAFT_LOCALE=uk-UA` to
override the desktop's locale for a launch. An explicit saved English, Ukrainian or German
choice takes precedence over that override.
resolve to `uk`, and Portuguese tags such as `pt-BR` and `pt_BR.UTF-8` resolve to `pt-BR`;
unsupported languages fall back to English. Set `CADCRAFT_LOCALE=uk-UA` to
override the desktop's locale for a launch. An explicit saved English, Ukrainian or Brazilian Portuguese choice
takes precedence over that override.
browser's `navigator.languages` on the web. BCP 47 and POSIX tags select their primary language:
`uk-UA` → `uk`, `de_AT.UTF-8` → `de`, `es-MX` → `es`, `pt_BR.UTF-8` → `pt-BR`,
`cs-CZ` and `cs_CZ.UTF-8` → `cs`. Unsupported languages fall back to English.
Set `CADCRAFT_LOCALE=cs-CZ` to override the desktop's locale for a launch.
An explicit saved language choice takes precedence over that override.
Automation can select the language through the existing control channel:
```json
{"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"uk"}}}
{"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"cs"}}}
```
The `ui.set` method also accepts `{"interfaceLanguage":"uk"}`. Supported values are `auto`,
`en`, `uk`, and `de`; invalid values are rejected without changing the current selection.
Resetting palettes keeps the language choice.
`en`, `uk`, and `pt-BR`; invalid values are rejected without changing the current selection. Resetting
palettes keeps the language choice.
The `ui.set` method also accepts `{"interfaceLanguage":"cs"}`. Supported values are `auto`,
`en`, `uk`, `pt-BR`, `de`, `es`, and `cs`; invalid values are rejected without changing the current
selection. Resetting palettes keeps the language choice.
## Translation coverage
Ukrainian and German cover command and menu labels, tool groups and tooltips, the Start page,
The catalogs cover command and menu labels, tool groups and tooltips, the Start page,
file and model-space tabs, status controls, object snaps, drafting prompts, property labels,
dialogs, and the About window's controls. The command reference can be searched by English
command ID, English label, or translated label.
The Ukrainian and Brazilian Portuguese catalogs cover command and menu labels, tool groups and tooltips, the Start page, file and
model-space tabs, status controls, object snaps, drafting prompts, property labels, dialogs,
and the About window's controls. The command reference can be searched by English command
ID, English label, or translated label.
command ID, English label, or translated label. Ukrainian has enforced coverage;
missing entries in the other catalogs are reported by tests and fall back to English.
Czech has 1,202 catalog rows, covering the reference catalog and every marked UI
literal, including Options. Four `menu` entries distinguish menu headings from object/command
labels: **Zobrazení / Pohled**, **Kreslení / Kreslit**, **Kótování / Kóta**, and **Modifikace / Upravit**.
The translation is AI-assisted work by @Losminek, reviewed against Czech desktop and CAD UI
conventions and an independent review pass; it has not yet been proofread by a native speaker.
Drawing titles, layer/layout/block/style names, parameter names and expressions, text in a
drawing, and command-line input are user data and retain their original spelling. Clickable
@ -49,9 +45,9 @@ by the engine. CLI/MCP/control responses, engine diagnostics and command history
English. Uncatalogued text also falls back to English. Number formatting and the operating
system's own file-picker controls follow the existing behavior.
The bundled egui fonts cover Ukrainian, including `Ґґ Єє Іі Її`, and German, including
`ÄÖÜ äöü ß`; the translations add no font
assets and work on the web without system fonts.
The bundled egui fonts cover Ukrainian (`Ґґ Єє Іі Її`), German (`ÄÖÜ äöü ß`), and Czech
(`ÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ áčďéěíňóřšťúůýž`, including typographic quotes `„…“`).
The translations add no font assets and work on the web without system fonts.
## Adding translations
@ -60,14 +56,14 @@ The implementation follows the display-time catalog pattern used by
[PdfCraft #106](https://github.com/storytold/pdfcraft/pull/106), and
[FilmCraft #238](https://github.com/storytold/filmcraft/pull/238). Its code is local to CADCraft.
- `crates/ui-egui/src/i18n/uk.tsv` and `crates/ui-egui/src/i18n/de.tsv` are UTF-8 with three
tab-separated columns:
- `crates/ui-egui/src/i18n/uk.tsv` and `pt_br.tsv` are UTF-8 with three tab-separated columns:
- `crates/ui-egui/src/i18n/*.tsv` are UTF-8 with three tab-separated columns:
`context`, `English source`, and `translation`. Ordinary labels have an empty context.
`@plural` entries hold Ukrainian `one|few|many` forms for integer counts; German uses
`one|other|other` (singular/plural only, so the third form repeats the plural).
`@plural` entries hold the language's plural forms for integer counts: Ukrainian
`one|few|many`, Brazilian Portuguese `one|other` (0 and 1 are `one`).
Optional `menu` entries translate menu headings via `i18n::menu` / `i18n::tr_menu`;
without one, the ordinary translation is used.
- `@plural` entries hold the language's forms for integer counts: Ukrainian `one|few|many`,
German and Spanish `one|other`, Brazilian Portuguese `one|other` (0 and 1 are `one`),
Czech `one|few|other` (1; 2–4; everything else, including 0, 11, 21 and 22).
The API takes `u64`, so fractional counts are not accepted.
- Register a new catalog in `i18n::LANGUAGES`, add its preference variant and code/name,
and set its integer plural rule and form count on the `Language` row.
- Use `crate::tl!("English label")` for a literal, `i18n::t(label)` for a known dynamic UI
@ -75,11 +71,16 @@ The implementation follows the display-time catalog pattern used by
Translate only at the display boundary; never look up arbitrary user names or identifiers.
- Windows retain explicit IDs based on their English source, and tool groups retain their
source names as keys, so switching languages preserves their state.
- Add a translation attribution row in `ATTRIBUTION.md`, update the language lists, and add
locale, plural, font-glyph and live-switch tests. Add the catalog to `REPORTED` in
`i18n/tests.rs`: only Ukrainian, the reference translation, is in `REQUIRED`, so adding a
command needs a row in one language, not in all of them.
- Catalogs are embedded and parsed once. Missing or malformed rows cannot crash the app;
tests reject invalid rows, duplicate keys, changed placeholders, and mismatched ellipses.
- `cargo test -p cadcraft-ui-egui i18n` checks menu/command/tool/snap coverage, every marked
UI literal, locale resolution, preferences, integer plurals, bundled glyphs and an actual
English/Ukrainian/English egui render. `cargo xtask ci` runs the workspace quality gates.
UI literal, locale resolution, preferences, integer plurals, bundled glyphs and actual
egui renders with language switching. `cargo xtask assets` checks asset attribution;
`cargo xtask ci` runs the workspace quality gates.
Translations are original work under `MIT OR Apache-2.0`, written from the meaning of
CADCraft's English strings. Do not copy proprietary localization resources.