HU1: add Hungarian interface localization

Add 1,917 Hungarian translations and two context captions, register Magyar in Preferences, and cover language persistence and tool labels with regression tests. Let tool flyouts grow to fit translated labels.

AI-assisted translation reviewed by a native Hungarian contributor with over 30 years of DTP experience. Further in-context feedback is welcome. Add the contributor's requested public GitHub profile-name preference.
This commit is contained in:
macadminHU
2026-10-17 15:56:21 +02:00
committed by GitHub
parent fc36957fe0
commit e8908b0fc2
7 changed files with 2107 additions and 6 deletions
+2
View File
@@ -348,6 +348,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-17** · HU1: Hungarian (`hu` / Magyar) interface with 1,917 entries and two context captions, including tool names; AI-assisted and initially reviewed by a native DTP practitioner. Unknown/new strings remain English. Hungarian spelling and hyphenation remain follow-up work. No readiness percentage change.
- **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change.
- **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change.
- **2026-10-16** · A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign: IDML's `<Image FillColor>` is read and written, greyscale pixels become a tint of the swatch on opaque white and a bitmap's white lets the frame show through, on screen and in PDF (spot swatches print on their plate). Colour images keep their colours. Applying the swatch from the Swatches panel comes next. No readiness percentage change.
+3
View File
@@ -65,3 +65,6 @@ sync_github_name = false
real_name = "Marco Arnulfo"
display_name = "Marco Arnulfo"
sync_github_name = false
[people.macrobi]
sync_github_name = true
+50 -1
View File
@@ -8,6 +8,7 @@ mod ar;
mod cs;
mod de;
mod fr;
mod hu;
mod it;
mod ja;
mod ka;
@@ -32,6 +33,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[
("cs", "Čeština"),
("pl", "Polski"),
("ka", "ქართული"),
("hu", "Magyar"),
];
/// English → [German, French, Spanish, Japanese, Simplified Chinese].
@@ -4446,6 +4448,10 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
static CZECH: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static FRENCH: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static GEORGIAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static HUNGARIAN: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
if lang == "hu" {
return HUNGARIAN.get_or_init(|| hu::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
if lang == "ar" {
return ARABIC.get_or_init(|| ar::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
@@ -4501,6 +4507,7 @@ const CONTEXT: &[(&str, &str, [&str; 5])] = &[
/// Context-sensitive UI captions; unsupported contexts keep the shared translation.
pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str {
let contexts = match lang {
"hu" => hu::CONTEXT,
"uk" => uk::CONTEXT,
"ru" => ru::CONTEXT,
"cs" => cs::CONTEXT,
@@ -4625,12 +4632,54 @@ mod tests {
for (key, expected) in ka::TABLE {
assert_eq!(tr("ka", key), *expected, "ka: {key}");
}
for (key, expected) in hu::TABLE {
assert_eq!(tr("hu", key), *expected, "hu: {key}");
}
let unknown = String::from("A user-defined untranslated label");
for lang in ["pl", "cs", "uk", "ru", "ar", "pt-br", "it", "ka", "zh", "ja", "de", "fr", "es", "", "unknown"] {
for lang in ["hu", "pl", "cs", "uk", "ru", "ar", "pt-br", "it", "ka", "zh", "ja", "de", "fr", "es", "", "unknown"] {
assert!(std::ptr::eq(tr(lang, &unknown), unknown.as_str()));
}
}
#[test]
fn hungarian_catalog_preserves_placeholders_context_and_counts() {
let catalog: HashMap<_, _> = hu::TABLE.iter().copied().collect();
assert_eq!(catalog.len(), hu::TABLE.len(), "duplicate Hungarian keys");
let placeholders = |s: &str| {
let mut names: Vec<_> = s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name.to_owned())).collect();
names.sort();
names
};
for (key, translation) in hu::TABLE {
assert!(!translation.trim().is_empty(), "empty {key}");
assert_eq!(placeholders(key), placeholders(translation), "placeholders: {key}");
assert_eq!(key.ends_with('…'), translation.ends_with('…'), "ellipsis: {key}");
}
assert!(!is_rtl("hu"));
assert_eq!(tr("hu", "File"), "Fájl");
assert_eq!(tr("hu", "Bleed"), "Kifutó");
assert_eq!(tr("hu", "Unknown label"), "Unknown label");
assert_eq!(tr("hu", "Columns"), "Hasábok");
assert_eq!(tr_context("hu", "Columns", "table"), "Oszlopok");
assert_eq!(tr("hu", "Group"), "Csoportosítás");
assert_eq!(tr_context("hu", "Group", "selection"), "Csoport");
assert_eq!(tr_context("hu", "Columns", "unknown"), "Hasábok");
assert_eq!(tr_context("", "Columns", "table"), "Columns");
for n in [0, 1, 2, 5, 21] {
assert_eq!(count_label("hu", "pages", n), format!("{n} oldal"));
assert_eq!(count_label("hu", "objects", n), format!("{n} objektum"));
assert_eq!(count_label("hu", "errors", n), format!("{n} hiba"));
assert_eq!(
tr("hu", "{pages} Pages in {spreads} Spreads").replace("{pages}", &n.to_string()).replace("{spreads}", &n.to_string()),
format!("{n} oldal, {n} oldalpáron")
);
}
assert_eq!(style_name("hu", "[Basic Paragraph]"), "[Alapbekezdés]");
assert_eq!(style_name("hu", "Saját stílus"), "Saját stílus");
assert_eq!(workspace_name("hu", "Essentials"), "Alapok");
assert_eq!(workspace_name("hu", "Saját munkaterület"), "Saját munkaterület");
}
#[test]
fn translates_known_strings_and_keeps_the_rest() {
assert_eq!(tr("de", "File"), "Datei");
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -65,7 +65,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
"app.language",
"Interface Language",
None,
"{lang?: \"\"|de|fr|es|ja|zh|ar|pt-br|it|uk|ru|cs|pl|ka} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting",
"{lang?: \"\"|de|fr|es|ja|zh|ar|pt-br|it|uk|ru|cs|pl|ka|hu} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting",
),
(
"app.flattener",
@@ -3807,6 +3807,26 @@ mod tests {
assert!(untranslated.is_empty(), "untranslated Georgian menu labels: {untranslated:?}");
}
#[test]
fn hungarian_language_is_selectable_persisted_and_keeps_document_data() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap();
app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "text", "text": "My words · Árvíztűrő tükörfúrógép"})).unwrap();
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
run_ui(&mut app, "app.language", &json!({"lang": "hu"})).unwrap().unwrap();
assert_eq!(checked(&app, "app.language", &json!({"lang": "hu"})), Some(true));
assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap());
for (title, _) in menu_tree() {
assert_ne!(crate::i18n::tr("hu", title), title, "{title}");
}
let bytes = serde_json::to_vec(&app.ui).unwrap();
let restored: crate::UiState = serde_json::from_slice(&bytes).unwrap();
assert_eq!(restored.language, "hu");
assert_eq!(crate::i18n::tr(&restored.language, "File"), "Fájl");
assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name");
assert!(crate::i18n::LANGUAGES.contains(&("hu", "Magyar")), "Preferences lists Hungarian by its own name");
}
#[test]
fn ukrainian_language_is_selectable_persisted_and_keeps_document_data() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+47 -2
View File
@@ -135,9 +135,27 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
ui.ctx(),
|ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
let row_width = g.iter().fold(210.0_f32, |width, tl| {
let label = crate::i18n::tr(&app.ui.language, tl.label);
let label_width = ui
.painter()
.layout_no_wrap(label.to_owned(), egui::FontId::proportional(12.5), t.text)
.size()
.x;
let shortcut_width = ui
.painter()
.layout_no_wrap(
tl.shortcut.unwrap_or("").to_owned(),
egui::FontId::proportional(11.5),
t.text_dim,
)
.size()
.x;
width.max(label_width + shortcut_width + 46.0)
});
for tl in g {
ui.horizontal(|ui| {
let (r, row) = ui.allocate_exact_size(vec2(210.0, 24.0), Sense::click());
let (r, row) = ui.allocate_exact_size(vec2(row_width, 24.0), Sense::click());
if row.hovered() {
ui.painter().rect_filled(r, 2.0, t.hover);
}
@@ -166,7 +184,13 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
t.text_dim,
);
}
row.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl.label));
row.widget_info(|| {
egui::WidgetInfo::labeled(
egui::WidgetType::Button,
true,
crate::i18n::tr(&app.ui.language, tl.label),
)
});
if row.clicked() {
chosen = Some(tl.id);
}
@@ -642,6 +666,27 @@ mod tests {
h.get_by_label(label).rect()
}
#[test]
fn hungarian_tool_buttons_translate_every_tool_and_keep_shortcuts() {
for double in [false, true] {
let mut app = app(double);
app.ui.language = "hu".into();
let mut h = test_window::open(app, vec2(1400.0, 1000.0));
for tool in designcraft_tools::TOOL_GROUPS.iter().flat_map(|group| group.iter()) {
h.state_mut().app.run("tool.select", serde_json::json!({"tool": tool.id})).unwrap();
h.run_steps(3);
let label = crate::i18n::tr("hu", tool.label);
assert_ne!(label, tool.label, "untranslated tool: {}", tool.id);
let tip = match tool.shortcut {
Some(shortcut) => format!("{label} ({shortcut})"),
None => label.to_owned(),
};
assert!(rect(&h, &tip).is_finite(), "{tip}");
}
assert_eq!(crate::i18n::tr("hu", "Direct Selection Tool"), "Közvetlen kijelölő eszköz");
}
}
/// As InDesign's: a tool group's flyout closes on Esc and on a click outside it (the fill
/// swatch, a panel), not only on the canvas.
#[test]
+11 -2
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** minor (Polish and Georgian interfaces; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-17 · **Change:** minor (Hungarian interface; older catalog counts retain their dated measurement) · **Target:** Adobe InDesign 2026 (21.6)
Per-language status of the DesignCraft interface and of document typesetting in each script.
Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-typography.md),
@@ -11,7 +11,7 @@ Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-ty
## How it was measured
Interface strings live in `crates/ui-egui/src/i18n.rs` (a shared table with German, French,
Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,it,ja,ka,pt_br,uk}.rs`.
Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,de,fr,hu,it,ja,ka,pl,pt_br,ru,uk}.rs`.
Untranslated strings fall back to English. A script (2026-10-10) built the set of English
interface strings as the union of every catalog's keys, every engine command label and every
literal passed to `tr`/`t` in the UI crate: **1,724 strings**. A further ~145 capitalized literals
@@ -48,10 +48,19 @@ caption, table row/column movement and row-height distribution now use the exist
catalog. Print, import options and an active table were rendered in Japanese and English with
craft-fonts. The language totals above retain their dated measurement; this is a targeted fix.
Hungarian follow-up (2026-10-17): `hu.rs` adds **1,917 entries and two context-specific
captions**, including the 37 tool names. This is a partial interface catalog based on an earlier
source snapshot, not a new coverage measurement against current main. It was prepared with AI
assistance and initially reviewed by a native Hungarian contributor with over 30 years of DTP
experience; further terminology and in-context review is welcome. Preferences lists `Magyar`;
untranslated/new strings fall back to English. Hungarian spelling and hyphenation are separate
follow-up work and are not enabled by selecting this interface language.
## Other shipped languages
| Language | Code | UI strings translated | Status |
|---|---|---|---|
| Hungarian | hu | 1,917 entries + 2 context captions (2026-10-17); initial native review | partial |
| Czech | cs | 1,722 (99%), plural categories handled | partial |
| Italian | it | 1,464 (85%) | partial |
| Ukrainian | uk | 1,432 (83%), plural categories handled | partial |