Merge pull request #2833 from storytold/land/2016

Land #1971 and #2016: Georgian font and Georgian interface language
This commit is contained in:
Tanish Manku
2026-11-05 16:31:28 -07:00
committed by GitHub
12 changed files with 1600 additions and 27 deletions
+1
View File
@@ -78,3 +78,4 @@ Generated-in-code assets are original and have no file to list:
| `crates/ui-egui/src/i18n/tr.tsv` | @ogzhnklvz (AI-assisted) | Original Turkish translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; written from the English strings, no other catalog used as a source; the running app reviewed by a native speaker |
| `crates/ui-egui/src/i18n/el.tsv` | @Camthalion77 with ChatGPT (AI-assisted) | Original Greek translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; independent native proofreading pending |
| `crates/ui-egui/src/i18n/vi.tsv` | @2ez4gcx with Claude Code (AI-assisted) | Original Vietnamese translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; terminology chosen with a native speaker, full proofreading pending |
| `crates/ui-egui/src/i18n/ka.tsv` | Davit Matchakhelidze (@devius), native speaker, with Claude Code | Original Georgian translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied; written from the English strings, no other catalog used as a source; terminology and doubtful strings decided by a native Georgian speaker; no font assets added (the Georgian face comes from craft-fonts) |
+1 -1
View File
@@ -118,7 +118,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 (Українська), Brazilian Portuguese (Português (Brasil)), German (Deutsch), Spanish (Español), Czech (Čeština), Russian (Русский), French (Français), Simplified Chinese (简体中文, with an installed CJK font), Italian (Italiano), Korean (한국어, with an installed Hangul font), Swedish (Svenska), Arabic (العربية, with an installed Arabic font), Hungarian (Magyar) and Turkish (Türkçe), 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), Czech (Čeština), Russian (Русский), French (Français), Simplified Chinese (简体中文, with an installed CJK font), Italian (Italiano), Korean (한국어, with an installed Hangul font), Swedish (Svenska), Arabic (العربية, with an installed Arabic font), Hungarian (Magyar), Turkish (Türkçe), Greek (Ελληνικά), Bosnian (Bosanski) and Georgian (ქართული), 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
+5 -5
View File
@@ -3,10 +3,10 @@
//! empty, `CRAFT_FONTS` is empty and the build is unchanged. It only reads the local checkout
//! (no network). A bad checkout is a warning, or an error with `CRAFT_FONTS_REQUIRED=1`.
//!
//! On wasm32 it embeds only BIZ UDPGothic Regular, the `Arab` faces and the `Thai` Regular faces
//! (about 5.9 MB), to keep the web build within hosting limits (Cloudflare Pages: 25 MiB per
//! file). The web build has no system fonts, so these are its only fallback for Japanese,
//! Arabic-script and Thai text.
//! On wasm32 it embeds only BIZ UDPGothic Regular, the `Arab` and `Geor` faces and the `Thai`
//! Regular faces (about 6.1 MB), to keep the web build within hosting limits (Cloudflare Pages:
//! 25 MiB per file). The web build has no system fonts, so these are its only fallback for
//! Japanese, Arabic-script, Georgian and Thai text.
use std::fmt::Write as _;
use std::path::PathBuf;
@@ -56,7 +56,7 @@ fn craft_fonts(dir: &std::path::Path) -> Result<String, String> {
continue;
}
let has = |script: &str| scripts.split(',').any(|s| s.trim() == script);
let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || has("Arab") || (has("Thai") && *style == "Regular");
let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || has("Arab") || has("Geor") || (has("Thai") && *style == "Regular");
if wasm && !web_face {
continue;
}
+3 -2
View File
@@ -1,8 +1,8 @@
//! Fonts from the optional craft-fonts build input (<https://github.com/storytold/craft-fonts>).
//!
//! Built with `CRAFT_FONTS_DIR=<craft-fonts checkout>`, `build.rs` embeds the fonts listed in its
//! `fonts/manifest.txt` (web builds: only BIZ UDPGothic Regular, the Arabic faces and the Thai
//! Regular faces). Without it [`CRAFT_FONTS`] is empty and nothing changes. The font table ([`crate::ttf`])
//! `fonts/manifest.txt` (web builds: only BIZ UDPGothic Regular, the Arabic and Georgian faces and
//! the Thai Regular faces). Without it [`CRAFT_FONTS`] is empty and nothing changes. The font table ([`crate::ttf`])
//! lists the embedded fonts with the installed ones, under [`CraftFont::stem`]. A text style
//! can name one (`BIZ UDPGothic`), and they are fallbacks for characters a text's font lacks.
//! They are copied out of the binary only when first used.
@@ -85,6 +85,7 @@ mod tests {
Some("Arab") => '\u{0628}',
Some("Thai") => '\u{0E01}',
Some("Hang" | "Kore") => '가',
Some("Geor") => 'ა',
_ => 'A',
};
assert!(face.is_some_and(|x| x.has_glyph(sample)), "{} lacks {sample}", f.family);
+21 -2
View File
@@ -45,6 +45,8 @@ pub enum Preference {
Hungarian,
#[serde(rename = "tr")]
Turkish,
#[serde(rename = "ka")]
Georgian,
#[serde(rename = "bs")]
Bosnian,
#[serde(rename = "vi")]
@@ -52,7 +54,7 @@ pub enum Preference {
}
impl Preference {
pub const ALL: [Self; 19] = [
pub const ALL: [Self; 20] = [
Self::Auto,
Self::English,
Self::Greek,
@@ -70,6 +72,7 @@ impl Preference {
Self::Arabic,
Self::Hungarian,
Self::Turkish,
Self::Georgian,
Self::Bosnian,
Self::Vietnamese,
];
@@ -119,6 +122,7 @@ impl Preference {
Self::Arabic => "ar",
Self::Hungarian => "hu",
Self::Turkish => "tr",
Self::Georgian => "ka",
Self::Bosnian => "bs",
Self::Vietnamese => "vi",
}
@@ -149,6 +153,15 @@ impl Preference {
Self::Arabic => "العربية",
Self::Hungarian => "Magyar",
Self::Turkish => "Türkçe",
// Georgian letters come from the craft-fonts Georgian face; a build without it shows
// the English name (as Korean does) and keeps the interface in English.
Self::Georgian => {
if crate::theme::can_show("ka") {
"ქართული"
} else {
"Georgian"
}
}
Self::Bosnian => "Bosanski",
// Its letters need a font egui doesn't bundle ([`crate::theme::vietnamese_font`]).
Self::Vietnamese => {
@@ -188,6 +201,7 @@ impl Preference {
Self::Arabic => "ar",
Self::Hungarian => "hu",
Self::Turkish => "tr",
Self::Georgian => "ka",
Self::Bosnian => "bs",
Self::Vietnamese => "vi",
Self::Auto => locales.iter().find_map(|s| locale_language(s)).unwrap_or("en"),
@@ -206,7 +220,7 @@ pub struct Language {
catalog: OnceLock<Catalog<'static>>,
}
pub static LANGUAGES: [Language; 18] = [
pub static LANGUAGES: [Language; 19] = [
Language { code: "en", name: "English", source: "", plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
// Greek: one for exactly 1; other for zero and all other integer counts.
Language {
@@ -253,6 +267,11 @@ pub static LANGUAGES: [Language; 18] = [
Language { code: "hu", name: "Magyar", source: include_str!("i18n/hu.tsv"), plural: plural_ko, plural_forms: 1, catalog: OnceLock::new() },
// Turkish keeps a noun singular after a number ("3 commit"), so like Korean it has one form.
Language { code: "tr", name: "Türkçe", source: include_str!("i18n/tr.tsv"), plural: plural_ko, plural_forms: 1, catalog: OnceLock::new() },
// Georgian keeps a noun singular after a numeral ("5 კომიტი"), so one form, the Korean rule.
// Its letters come from the craft-fonts `Geor` face (`theme::can_show`).
Language {
code: "ka", name: "ქართული", source: include_str!("i18n/ka.tsv"), plural: plural_ko, plural_forms: 1, catalog: OnceLock::new()
},
// Bosnian (Latin script, Ijekavian): one (1, 21), few (2–4, 22–24, not 12–14), other — the
// Ukrainian rule for integer counts.
Language { code: "bs", name: "Bosanski", source: include_str!("i18n/bs.tsv"), plural: plural_uk, plural_forms: 3, catalog: OnceLock::new() },
File diff suppressed because it is too large Load Diff
+19 -2
View File
@@ -203,6 +203,7 @@ fn assert_additional_locale_aliases() {
("bs", &["bs", "bs-BA", "bs_BA.UTF-8", "BS-ba", " bs-BA ", "bs-Latn-BA"]),
("vi", &["vi", "vi-VN", "vi_VN.UTF-8", "VI-vn", " vi-VN "]),
("el", &["el", "el-GR", "el_GR.UTF-8", "EL-cy"]),
("ka", &["ka", "ka-GE", "ka_GE.UTF-8", "KA-ge", " ka-GE "]),
("zh", &["zh", "zh-CN"]),
] {
for tag in tags {
@@ -214,7 +215,7 @@ fn assert_additional_locale_aliases() {
fn assert_unrecognized_locale_tags() {
// Look-alike codes, three-letter codes and endonyms pick nothing: `se` is Northern Sami.
let look_alikes = ["ua", "ukulele", "pl-PL", "", "garbage", "💙", "ptolemy", "cz", "csharp", "csomething", "italic"];
let others = ["kr", "korean", "kotlin", "se", "swe", "svenska", "hun", "magyar", "tur", "türkçe", "elvish"];
let others = ["kr", "korean", "kotlin", "se", "swe", "svenska", "hun", "magyar", "tur", "türkçe", "elvish", "kat", "geo", "kab", "kartuli"];
for tag in look_alikes.into_iter().chain(others) {
assert_eq!(locale_language(tag), None, "{tag}");
}
@@ -482,7 +483,7 @@ fn dialogs_render_in_every_language_and_keep_their_source_ids() {
#[test]
fn interface_fonts_cover_every_letter_in_each_catalog() {
for language in translated() {
let extra_font = ["ko", "zh", "ar", "vi"].contains(&language.code);
let extra_font = ["ko", "zh", "ar", "vi", "ka"].contains(&language.code);
if extra_font && !crate::theme::can_show(language.code) {
continue;
}
@@ -578,6 +579,22 @@ fn korean_ui_fonts_cover_the_language_picker_in_both_families() {
assert!(missing.is_empty(), "missing Korean picker glyphs: {}", missing.join(", "));
}
/// A Georgian noun stays singular after a numeral, so one count form; the Dimension menu and the
/// dimension object take different words. Mtavruli (U+1C90–U+1CBF) is a display style, not upper
/// case: never in the catalog.
#[test]
fn georgian_counts_menu_headings_and_no_mtavruli() {
with_language("ka", || {
for n in [0, 1, 2, 5, 21, u64::MAX] {
assert_eq!(count(n, "{n} commit", "{n} commits"), format!("{n} კომიტი"));
}
assert_eq!(menu("Dimension"), "ზომების დატანა");
assert_eq!(t("Dimension"), "ზომა");
});
let mtavruli: Vec<char> = include_str!("ka.tsv").chars().filter(|c| ('\u{1c90}'..='\u{1cbf}').contains(c)).collect();
assert!(mtavruli.is_empty(), "{mtavruli:?}");
}
/// The production font chain covers alphanumeric ASCII in proportional and monospace, plus ⌘ ⇧ ↩ … − in proportional and ⇧ in monospace.
#[test]
fn production_font_chain_covers_latin_and_shortcut_symbols() {
+1
View File
@@ -276,6 +276,7 @@ fn run_language_command(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
"No installed interface font has the Vietnamese letters, so the interface stays in English. Install one that does (Segoe UI, Arial, DejaVu Sans or Noto Sans) to use Vietnamese."
}
"ar" => "No Arabic font is installed, so the interface stays in English. Install one (Noto Sans Arabic, for example) to use Arabic.",
"ka" => "This build has no Georgian font, so the interface stays in English. Build with craft-fonts (its Geor face) to use Georgian.",
_ => "No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese.",
});
}
+1 -1
View File
@@ -10,7 +10,7 @@
//! |---|---|
//! | `theme` | `"system"`, `"light"` or `"dark"` |
//! | `animations` | `"system"`, `"on"` or `"off"`: dialog and menu fades and smoothed scrolling |
//! | `interfaceLanguage` | `"auto"`, `"en"`, `"uk"`, `"pt-BR"`, `"de"`, `"es"`, `"cs"`, `"ru"`, `"fr"`, `"zh"`, `"it"`, `"ko"`, `"sv"`, `"ar"`, `"hu"`, `"tr"`, `"bs"`, `"vi"` or `"el"` |
//! | `interfaceLanguage` | `"auto"`, `"en"`, `"uk"`, `"pt-BR"`, `"de"`, `"es"`, `"cs"`, `"ru"`, `"fr"`, `"zh"`, `"it"`, `"ko"`, `"sv"`, `"ar"`, `"hu"`, `"tr"`, `"bs"`, `"vi"`, `"el"` or `"ka"` |
//! | `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) |
+42 -9
View File
@@ -314,7 +314,8 @@ pub fn mono() -> FontId {
/// language: file, layout and layer names can be Japanese or Chinese whatever the interface
/// shows (#513). In a Chinese interface (`language` "zh…") the Chinese font comes first, else a
/// Japanese one, so names take their own glyph forms; in an Arabic interface an installed
/// Arabic font follows. A font embedded from craft-fonts comes
/// Arabic font follows. The embedded Georgian face loads in every language, so the language
/// picker can show ქართული. A font embedded from craft-fonts comes
/// last, for systems without any (and the web build, which has no system fonts). A Korean
/// interface adds a Hangul font only when the CJK font found doesn't cover Hangul
/// ([`korean_font`]).
@@ -482,7 +483,8 @@ pub fn shows_vietnamese(language: &str) -> bool {
/// shows (#513), with the interface's own face leading. A sans-serif face reads best, and a
/// Chinese interface takes the face with its own glyphs ahead of a Japanese one — a Japanese face
/// shares most ideographs but lacks the simplified-only ones (图 园 测), which would be tofu.
/// العربية adds its Arabic face after those, egui's bundled faces carrying no Arabic glyphs.
/// The Georgian face follows in every language (egui's bundled faces carry no Georgian glyphs, and
/// the picker names the language ქართული); العربية adds its Arabic face after those.
fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str) -> Vec<&'a cadcraft_fonts::CraftFont> {
let cjk = |f: &&cadcraft_fonts::CraftFont| f.style == "Regular" && f.scripts.iter().any(|s| matches!(*s, "Jpan" | "Hans" | "Hant"));
let mut out: Vec<_> = faces.iter().copied().filter(cjk).collect();
@@ -490,6 +492,7 @@ fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str
if language.starts_with("zh") {
out.sort_by_key(|f| !f.scripts.contains(&"Hans"));
}
out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Geor")));
if language == "ar" {
out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Arab")));
}
@@ -728,12 +731,14 @@ pub fn shows_hangul(language: &str) -> bool {
/// Whether the interface can show `language` without missing-glyph boxes: Chinese needs a font
/// with its glyphs (installed, or the CJK face this build embedded from craft-fonts), Korean a
/// font with Hangul, العربية an installed Arabic font or the embedded Arabic face, Tiếng Việt a
/// system interface font with its tone-marked letters ([`vietnamese_font`]).
/// system interface font with its tone-marked letters ([`vietnamese_font`]), ქართული the
/// embedded Georgian face.
pub fn can_show(language: &str) -> bool {
match language {
"ko" => shows_hangul("ko"),
"vi" => vietnamese_font().is_some(),
"ar" => arabic_font().is_some() || embedded_script("Arab"),
"ka" => embedded_script("Geor"),
l if l.starts_with("zh") => cjk_font().is_some() || embedded_script("Hans") || embedded_script("Hant"),
_ => true,
}
@@ -843,13 +848,16 @@ mod tests {
let japanese = face("BIZ UDPGothic", &["Jpan", "Latn"]);
let chinese = face("Han Test Face", &["Hans", "Latn"]);
let arabic = face("Noto Sans Arabic", &["Arab"]);
let faces = [&japanese, &chinese, &arabic];
let georgian = face("Noto Sans Georgian", &["Geor"]);
let faces = [&japanese, &chinese, &arabic, &georgian];
// Every interface reads CJK names, so both CJK faces load, the sans-serif face first.
assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face"]);
// The Georgian face loads in every interface too, after them: the picker shows ქართული.
assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian"]);
// A Chinese interface takes the Han face first: a Japanese face lacks 图 园 测.
assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic"]);
assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic", "Noto Sans Georgian"]);
// العربية adds the Arabic face, after the CJK ones egui's bundled faces have no glyphs for.
assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Arabic"]);
assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian", "Noto Sans Arabic"]);
assert_eq!(families(&super::embedded_ui_faces(&faces, "ka")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian"]);
// Another weight is a style of a face, not an interface face of its own.
let bold = CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: &[] };
assert!(super::embedded_ui_faces(&[&bold], "en").is_empty());
@@ -860,13 +868,38 @@ mod tests {
/// leaving the installed-font answer the gate had before.
#[test]
fn embedded_faces_answer_for_their_scripts() {
let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"])];
assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab"));
let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"]), face("Noto Sans Georgian", &["Geor"])];
assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab") && super::any_script(&faces, "Geor"));
assert!(!super::any_script(&faces, "Hans") && !super::any_script(&faces, "Hant"));
// A `Bold`-only face is not the interface's, and a script code is matched whole.
let bold = CraftFont { family: "Some Han Bold", style: "Bold", scripts: &["Hans"], bytes: &[] };
assert!(!super::any_script(&[bold], "Hans") && !super::any_script(&faces, "Ara"));
assert!(!super::embedded_script("Hans") || super::can_show("zh"));
// ქართული rides on the embedded Georgian face alone.
assert_eq!(super::can_show("ka"), super::embedded_script("Geor"));
assert!(super::can_show("en") && super::can_show("uk"));
}
/// With the craft-fonts Georgian face (release and web builds), every Mkhedruli letter and the
/// picker's ქართული render in both families, in a Georgian and in an English interface.
#[test]
fn embedded_georgian_face_covers_the_alphabet_in_every_interface() {
if !super::embedded_script("Geor") {
return;
}
let letters: Vec<char> = ('ა'..='ჰ').chain("ქართული".chars()).collect();
for language in ["ka", "en"] {
let ctx = egui::Context::default();
super::install_fonts(&ctx, language);
let mut output = ctx.run_ui(Default::default(), |_| {});
output.textures_delta.clear();
ctx.fonts_mut(|view| {
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
for c in &letters {
assert!(view.has_glyph(&egui::FontId::new(12.0, family.clone()), *c), "{language}: missing Georgian glyph {c} in {family:?}");
}
}
});
}
}
}
+3 -2
View File
@@ -188,9 +188,10 @@ 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`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar` and `hu`.
Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr`, `bs`, `el` and `ka`.
Chinese needs an installed CJK font, Arabic an installed Arabic font; Korean needs a font covering the Korean interface from the
optional craft-fonts input or the operating system. Without a suitable font, the saved choice is
optional craft-fonts input or the operating system; Georgian needs the `Geor` face from craft-fonts (an installed font
does not count). Without a suitable font, the saved choice is
retained and the result's `shown` is `en`. `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
+29 -3
View File
@@ -1,7 +1,7 @@
# Interface languages
Choose **Edit → Interface Language** and select **Auto (system language)**, **English**,
**Українська**, **Português (Brasil)**, **Deutsch**, **Español**, **Čeština** (Czech), **Русский**, **Français**, **简体中文**, **Italiano**, **한국어** (Korean), **Svenska** (Swedish), **العربية** (Arabic), **Magyar** (Hungarian), **Türkçe** (Turkish), or **Ελληνικά** (Greek).
**Українська**, **Português (Brasil)**, **Deutsch**, **Español**, **Čeština** (Czech), **Русский**, **Français**, **简体中文**, **Italiano**, **한국어** (Korean), **Svenska** (Swedish), **العربية** (Arabic), **Magyar** (Hungarian), **Türkçe** (Turkish), **Ελληνικά** (Greek), **Bosanski** (Bosnian), or **ქართული** (Georgian).
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
@@ -11,7 +11,7 @@ Auto uses the first supported language in the operating system's preferred local
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`, `it-IT` and `it_CH.UTF-8` → `it`, `ko-KR` and `ko_KR.UTF-8` → `ko`,
`sv-SE`, `sv-FI` and `sv_SE.UTF-8` → `sv`, `hu-HU` and `hu_HU.UTF-8` → `hu`, `tr-TR` and `tr_TR.UTF-8` → `tr`. Unsupported languages fall back to English.
`sv-SE`, `sv-FI` and `sv_SE.UTF-8` → `sv`, `hu-HU` and `hu_HU.UTF-8` → `hu`, `tr-TR` and `tr_TR.UTF-8` → `tr`, `ka-GE` and `ka_GE.UTF-8` → `ka`. 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.
@@ -22,7 +22,7 @@ Automation can select the language through the existing control channel:
```
The `ui.set` method also accepts `{"interfaceLanguage":"cs"}`. Supported values are `auto`,
`en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr`, and `el`; invalid values are rejected without changing the current
`en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr`, `bs`, `el`, and `ka`; invalid values are rejected without changing the current
selection. Resetting palettes keeps the language choice.
## Translation coverage
@@ -101,6 +101,12 @@ catalog, the saved `ko` preference is kept but the interface falls back to Engli
command reports that a font is needed, and the picker uses the readable label **Korean** wherever
Hangul can't be shown. This change does not fix platform IME issues or localize engine output.
The bundled egui fonts have no Georgian letters. Georgian is drawn with the Noto Sans Georgian face
(`Geor`) of the craft-fonts build input, which release builds embed (`craft-fonts-ref` in the
workflows, and `web-craft-fonts` for the web build, whose embedded faces include it). The face loads
in every interface language, so Georgian text (file, layout and layer names, labels) draws. A build
without craft-fonts has no Georgian face.
## Greek (`el`) interface
The Greek translation covers all **1,330** distinct source keys collected from the existing
@@ -118,6 +124,26 @@ original AI-assisted work contributed by @Camthalion77 with language review; it
from Autodesk localization files. Independent native proofreading and visual checks remain
follow-up work.
## Georgian (`ka`) interface
Georgian has **1,458 rows**: 1,452 ordinary labels, four contextual menu headings (**ზომების
დატანა** for the Dimension menu, **ზომა** for the object; **ხატვა** for Draw) and two single-form
count templates, since a Georgian noun stays singular after a numeral ("5 კომიტი"). It covers the
union of the existing catalogs, and no marked UI literal falls back to English. Commands are verbal
nouns (**შენახვა**, **შემობრუნება**); prompts read "მიუთითეთ …" for Specify, "შეიყვანეთ …" for
Enter and "მონიშნეთ …" for Select, and clickable keywords keep the spelling the engine accepts.
Georgian has no letter case, so nothing is capitalised, and Mtavruli (a display style, not upper
case) never appears; a test checks that. The translation is original: written with Claude Code from
the meaning of each English string, with Georgian technical-drawing vocabulary (შრე, დაშტრიხვა,
მომრგვალება, ფასკა, წანაცვლება, ობიექტზე მიბმა, ხედვის არე); terminology and doubtful strings
were decided by a native Georgian speaker (@devius). No other product's localization was consulted.
egui's bundled faces have no Georgian letters, so Georgian is drawn with the craft-fonts Noto Sans
Georgian (`Geor`) face, which loads in every interface language when a build embeds it; the picker
then shows **ქართული**. A build without that face keeps the saved `ka` choice but shows the interface
in English, names the language **Georgian** in the picker, and the language command reports that a
Georgian font is needed.
## Adding translations
The implementation follows the display-time catalog pattern used by