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:
parent
3aeedf4856
commit
6d7c652b65
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue