mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 14:47:05 -06:00
Merge pull request #1223 from macrobi/codex/hungarian-localization
Add initial Hungarian (hu) interface localization
This commit is contained in:
@@ -405,6 +405,7 @@ Newest first.
|
||||
- **2026-10-20** · Edit › Keyboard Shortcuts is craft-keymap's shared dialog: commands grouped by menu with Show and Filter, recording that asks before taking a key from another command, undo and redo, named shortcut sets (edits of Default become Custom; an imported set becomes a set of its own), an on-screen US keyboard, InDesign Show Set files reviewed before they apply, JSON export and an HTML summary. ui.json keeps the `shortcuts` map earlier versions read, for the active set. No readiness percentage change.
|
||||
- **2026-10-19** · Import InDesign Set… runs on craft-keymap's importer: a set's Window menu panel items (Layers F7, Pages F12, Swatches F5) and Tools items set the panel and tool shortcuts, all of an item's Default keys apply, and the report adds numeric keypad keys read as main keys and likely commands for unmatched items (`window.importShortcuts`). On InDesign's Windows defaults it matches 366 of 1,629 items (was 233) and changes 23 commands. No readiness percentage change.
|
||||
- **2026-10-19** · Keyboard shortcuts run on the shared craft-keymap crate. Panels take shortcuts, with InDesign's defaults where the key is free (Swatches F5, Color F6, Layers F7, Info F8, Stroke F10, Paragraph Styles F11, Character Styles Shift+F11, Pages F12, Align Shift+F7); tool keys are editable in Edit › Keyboard Shortcuts and `window.setShortcut`, and tools sharing a key take turns (B). Saved shortcuts load unchanged. No readiness percentage change.
|
||||
- **2026-10-17** · HU1: initial Hungarian (`hu` / Magyar) interface localization (updated 2026-11-01): 1,906 catalog entries, four context captions and all 37 tool labels. AI-assisted; the initial catalog was personally reviewed in the app by Veress Róbert, a native Hungarian speaker with over 30 years of DTP experience. Uncatalogued strings keep their English fallback. Hungarian spelling and hyphenation are separate work in #1484. No readiness percentage change.
|
||||
- **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change.
|
||||
- **2026-10-20** · PDF/X-1a:2003 export (craft-libs#194): `file.exportPdf {standard: "x1a"}`, the Export PDF dialog's PDF/X-1a:2003 preset and standard. The PDF/X-4 print file is flattened in its inks by craft-prepress (no transparency; spot, process, overprint and live text kept; complex regions as DeviceN images at the Transparency Flattener preset's resolution), then written as PDF 1.4 with the X-1a identification and checked; `validate-pdf --standard x1a`. Soft drop shadows reach the flattener in their swatch's inks; other soft effects are still RGB images. No readiness percentage change.
|
||||
- **2026-10-20** · Print color (craft-libs#190): CMYK print PDFs blend in DeviceCMYK through craft-pdf's print settings instead of byte rewrites, with the CMYK working space as the output intent in PDF/X-4 only; overprint (Overprint Fill, Stroke and Gap, overprinting text, 100% [Black]) is written as `/op` `/OP` `/OPM 1`; Knockout Group writes `/K true` instead of becoming an image; File › Print prints the PDF Export PDF writes. No readiness percentage change.
|
||||
|
||||
@@ -63,6 +63,11 @@ real_name = "Marco Arnulfo"
|
||||
display_name = "Marco Arnulfo"
|
||||
sync_github_name = false
|
||||
|
||||
[people.macrobi]
|
||||
real_name = "Veress Róbert"
|
||||
display_name = "Veress Róbert"
|
||||
sync_github_name = false
|
||||
|
||||
[people.4waymedia]
|
||||
real_name = "Paul Gemignani"
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ mod ar;
|
||||
mod cs;
|
||||
mod de;
|
||||
mod fr;
|
||||
mod hu;
|
||||
mod it;
|
||||
mod ja;
|
||||
mod ka;
|
||||
@@ -36,6 +37,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[
|
||||
("cs", "Čeština"),
|
||||
("pl", "Polski"),
|
||||
("ka", "ქართული"),
|
||||
("hu", "Magyar"),
|
||||
];
|
||||
|
||||
/// The interface languages in the order the Preferences picker lists them: English first (the
|
||||
@@ -77,6 +79,8 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
if text.trim().is_empty() { s } else { text }
|
||||
}
|
||||
|
||||
/// Looks up a label in lazily cached language tables, using the shared table where supported.
|
||||
/// Unknown languages and missing keys keep the source text; `tr` also handles blank entries.
|
||||
fn lookup<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
static GERMAN: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
static JAPANESE: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
@@ -90,6 +94,10 @@ fn lookup<'a>(lang: &str, s: &'a str) -> &'a str {
|
||||
static CZECH: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
static FRENCH: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
static GEORGIAN: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
static HUNGARIAN: OnceLock<HashMap<&str, &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);
|
||||
}
|
||||
@@ -145,6 +153,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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ const LANGUAGE_TABLES: &[(&str, &[(&str, &str)])] = &[
|
||||
("cs", cs::TABLE),
|
||||
("de", de::TABLE),
|
||||
("fr", fr::TABLE),
|
||||
("hu", hu::TABLE),
|
||||
("it", it::TABLE),
|
||||
("ja", ja::TABLE),
|
||||
("ka", ka::TABLE),
|
||||
@@ -17,8 +18,15 @@ const LANGUAGE_TABLES: &[(&str, &[(&str, &str)])] = &[
|
||||
];
|
||||
|
||||
/// Every per-language table `tr_context` looks up.
|
||||
const CONTEXT_TABLES: &[(&str, &[(&str, &str, &str)])] =
|
||||
&[("ar", ar::CONTEXT), ("cs", cs::CONTEXT), ("ja", ja::CONTEXT), ("pl", pl::CONTEXT), ("ru", ru::CONTEXT), ("uk", uk::CONTEXT)];
|
||||
const CONTEXT_TABLES: &[(&str, &[(&str, &str, &str)])] = &[
|
||||
("ar", ar::CONTEXT),
|
||||
("cs", cs::CONTEXT),
|
||||
("hu", hu::CONTEXT),
|
||||
("ja", ja::CONTEXT),
|
||||
("pl", pl::CONTEXT),
|
||||
("ru", ru::CONTEXT),
|
||||
("uk", uk::CONTEXT),
|
||||
];
|
||||
|
||||
/// The languages of the shared tables' columns, in column order.
|
||||
const SHARED_COLUMNS: [&str; 5] = ["de", "fr", "es", "ja", "zh"];
|
||||
@@ -31,6 +39,7 @@ fn or_english<'a>(text: &'a str, english: &'a str) -> &'a str {
|
||||
if text.trim().is_empty() { english } else { text }
|
||||
}
|
||||
|
||||
/// Checks cached catalog lookup across language switches, including source-text fallback.
|
||||
#[test]
|
||||
fn cached_lookup_preserves_every_translation_across_language_switches() {
|
||||
let own = |lang: &str, key: &str| {
|
||||
@@ -61,6 +70,167 @@ fn cached_lookup_preserves_every_translation_across_language_switches() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks Hungarian catalog integrity, context captions and count substitutions, including
|
||||
/// English fallback and preservation of user-defined style and workspace names.
|
||||
#[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", "Arrangement"), "Elrendezés");
|
||||
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", "Columns:"), "Hasábok:");
|
||||
assert_eq!(tr_context("hu", "Columns:", "grid"), "Oszlopok:");
|
||||
assert_eq!(tr_context("hu", "Columns:", "unknown"), "Hasábok:");
|
||||
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");
|
||||
}
|
||||
|
||||
/// Checks every Latin catalog letter and all Hungarian accented vowels against the
|
||||
/// configured bundled UI faces, with system and optional craft-fonts fallbacks disabled.
|
||||
#[test]
|
||||
fn hungarian_catalog_glyphs_exist_in_the_bundled_ui_fonts() {
|
||||
let definitions = crate::theme::font_definitions(&[], "hu");
|
||||
let db = designcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
db.set_system_fallback(false);
|
||||
let letters: std::collections::BTreeSet<_> = hu::TABLE
|
||||
.iter()
|
||||
.flat_map(|(_, text)| text.chars())
|
||||
.chain(hu::CONTEXT.iter().flat_map(|(_, _, text)| text.chars()))
|
||||
.chain("ÁáÉéÍíÓóÖöŐőÚúÜüŰű".chars())
|
||||
.filter(|c| c.is_alphabetic() && *c <= '\u{024f}')
|
||||
.collect();
|
||||
for (name, family, style, ui_family) in [
|
||||
("ui", "Source Sans 3", "Regular", egui::FontFamily::Proportional),
|
||||
("ui-semibold", "Source Sans 3", "Semibold", egui::FontFamily::Name("semibold".into())),
|
||||
("mono", "JetBrains Mono", "Regular", egui::FontFamily::Monospace),
|
||||
] {
|
||||
assert_eq!(definitions.families[&ui_family].first().map(String::as_str), Some(name));
|
||||
let face = db.face(family, style);
|
||||
assert_eq!(face.data(), definitions.font_data[name].bytes(), "configured UI face {name}");
|
||||
for letter in &letters {
|
||||
assert_ne!(face.glyph_for(*letter), 0, "missing {letter} in {family} {style}");
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"Hungarian glyph coverage: {} Latin catalog letters, including ŐőŰű, in Source Sans 3 Regular/Semibold and JetBrains Mono Regular; no system/craft fallback",
|
||||
letters.len()
|
||||
);
|
||||
if let Some(dir) = std::env::var_os("DESIGNCRAFT_HU_REVIEW_SHOTS") {
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(820.0, 260.0)).build_ui_state(
|
||||
|ui, ready: &mut bool| {
|
||||
if !*ready {
|
||||
ui.ctx().set_fonts(crate::theme::font_definitions(&[], "hu"));
|
||||
*ready = true;
|
||||
return;
|
||||
}
|
||||
ui.style_mut().visuals = egui::Visuals::light();
|
||||
ui.painter().rect_filled(ui.ctx().content_rect(), 0.0, egui::Color32::WHITE);
|
||||
ui.heading("Magyar ékezetek · ő, ű / Ő, Ű");
|
||||
for (name, family) in [
|
||||
("Source Sans 3 Regular", egui::FontFamily::Proportional),
|
||||
("Source Sans 3 Semibold", egui::FontFamily::Name("semibold".into())),
|
||||
("JetBrains Mono Regular", egui::FontFamily::Monospace),
|
||||
] {
|
||||
ui.label(egui::RichText::new(name).color(egui::Color32::DARK_GRAY));
|
||||
ui.label(
|
||||
egui::RichText::new("Árvíztűrő tükörfúrógép · Őő Űű · ÁÉÍÓÖŐÚÜŰ")
|
||||
.font(egui::FontId::new(22.0, family))
|
||||
.color(egui::Color32::BLACK),
|
||||
);
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h.render().unwrap().save(std::path::Path::new(&dir).join("hungarian-bundled-ui-fonts.png")).unwrap();
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.ui.language = "hu".into();
|
||||
app.run("file.new", serde_json::json!({"pages": 1})).unwrap();
|
||||
app.run(
|
||||
"frame.create",
|
||||
serde_json::json!({"rect": [60, 60, 420, 180], "content": "text", "text": "Magyar kiadvány
|
||||
Árvíztűrő tükörfúrógép
|
||||
Őő Űű · ÁÉÍÓÖŐÚÜŰ"}),
|
||||
)
|
||||
.unwrap();
|
||||
app.run("type.char", serde_json::json!({"attrs": {"fontFamily": "Source Sans 3", "size": 28}})).unwrap();
|
||||
app.run("window.panel.activate", serde_json::json!({"panel": "character"})).unwrap();
|
||||
let mut interface = crate::test_window::open(app, egui::vec2(1400.0, 900.0));
|
||||
interface.render().unwrap().save(std::path::Path::new(&dir).join("designcraft-hungarian-interface.png")).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks translated labels and source-text fallback, plus key uniqueness and nonempty rows
|
||||
/// in the shared, Arabic, Brazilian Portuguese and Italian catalogs.
|
||||
#[test]
|
||||
fn translates_known_strings_and_keeps_the_rest() {
|
||||
assert_eq!(tr("de", "File"), "Datei");
|
||||
assert_eq!(tr("ja", "Swatches"), "スウォッチ");
|
||||
assert_eq!(tr("fr", "Something new"), "Something new");
|
||||
assert_eq!(tr("", "File"), "File");
|
||||
assert_eq!(tr("ar", "File"), "ملف");
|
||||
assert_eq!(tr("ar", "Unknown label"), "Unknown label");
|
||||
assert_eq!(tr("pt-br", "File"), "Arquivo");
|
||||
assert_eq!(tr("pt-br", "New Document…"), "Novo documento…");
|
||||
assert_eq!(tr("pt-br", "Unknown label"), "Unknown label");
|
||||
assert_eq!(tr("it", "Edit"), "Modifica");
|
||||
assert_eq!(tr("it", "New Document…"), "Nuovo documento…");
|
||||
assert_eq!(tr("it", "Unknown label"), "Unknown label");
|
||||
assert!(is_rtl("ar"));
|
||||
for (i, (en, translation)) in ar::TABLE.iter().enumerate() {
|
||||
assert!(ar::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}");
|
||||
assert!(!translation.is_empty(), "{en}");
|
||||
}
|
||||
for (i, (en, translation)) in pt_br::TABLE.iter().enumerate() {
|
||||
assert!(pt_br::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}");
|
||||
assert!(!translation.is_empty(), "{en}");
|
||||
}
|
||||
for (i, (en, translation)) in it::TABLE.iter().enumerate() {
|
||||
assert!(it::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}");
|
||||
assert!(!translation.is_empty(), "{en}");
|
||||
}
|
||||
// Italian entries are for keys the shared or Arabic table knows. A new English string
|
||||
// falls back to English until it is translated, so it doesn't fail here.
|
||||
for (en, _) in it::TABLE {
|
||||
assert!(TABLE.iter().any(|(key, _)| key == en) || ar::TABLE.iter().any(|(key, _)| key == en), "it: unknown key {en}");
|
||||
}
|
||||
// Every row is unique and complete.
|
||||
for (i, (en, t)) in TABLE.iter().enumerate() {
|
||||
assert!(TABLE[..i].iter().all(|(e, _)| e != en), "duplicate {en}");
|
||||
assert!(t.iter().all(|x| !x.is_empty()), "{en}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Data Merge arrangement combo takes its accessible name from `tr(lang, "Arrangement")`.
|
||||
/// A row in only the shared table leaves the languages whose lookup returns directly with the
|
||||
/// English name.
|
||||
@@ -77,6 +247,7 @@ fn arrangement_accessible_name_is_localized_in_every_interface_language() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks language-specific context precedence and fallback to the plain translation.
|
||||
#[test]
|
||||
fn context_captions_come_from_their_context_or_the_plain_translation() {
|
||||
for (lang, table) in CONTEXT_TABLES {
|
||||
@@ -110,6 +281,38 @@ fn context_captions_come_from_their_context_or_the_plain_translation() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps quantity labels distinct from numeric values and checks translated Pages-panel captions.
|
||||
#[test]
|
||||
fn a_column_count_and_a_list_number_read_differently() {
|
||||
for (lang, count, numeral) in [
|
||||
("hu", "Darabszám:", "Szám:"),
|
||||
("de", "Anzahl:", "Zahl:"),
|
||||
("ja", "数:", "番号:"),
|
||||
("uk", "Кількість:", "Номер:"),
|
||||
("ar", "العدد:", "الرقم:"),
|
||||
("fr", "Nombre :", "Numéro:"),
|
||||
("zh", "数量:", "编号:"),
|
||||
] {
|
||||
assert_eq!((tr_context(lang, "Number:", "count"), tr(lang, "Number:")), (count, numeral), "{lang}");
|
||||
}
|
||||
assert_eq!(tr_context("en", "Number:", "count"), "Number:");
|
||||
assert_eq!(tr("de", "Gutter:"), "Spaltenabstand:");
|
||||
// The Pages panel menu.
|
||||
for key in [
|
||||
"Insert Pages…",
|
||||
"Move Pages…",
|
||||
"New Parent…",
|
||||
"Override All Parent Page Items",
|
||||
"Remove All Local Overrides",
|
||||
"Detach All Objects from Parent",
|
||||
"Create Alternate Layout…",
|
||||
"Allow Selected Spread to Shuffle",
|
||||
] {
|
||||
assert_ne!(tr("pt-br", key), key);
|
||||
}
|
||||
assert_ne!(tr("ar", "Apply Parent to Pages"), "Apply Parent to Pages");
|
||||
}
|
||||
|
||||
/// The tables `tr` reads, as (language, key, translation), context captions included.
|
||||
fn entries() -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut entries: Vec<_> = LANGUAGE_TABLES.iter().flat_map(|(lang, table)| table.iter().map(|(key, text)| (*lang, *key, *text))).collect();
|
||||
@@ -263,6 +466,23 @@ fn interface_strings() -> HashSet<&'static str> {
|
||||
keys.extend(crate::panel_docking::DOCK_LABELS.iter().map(|(_, english)| *english));
|
||||
// UI labels passed to `tr` that no other catalog has yet.
|
||||
keys.extend([
|
||||
"180°",
|
||||
"Create Links When Placing Text Files",
|
||||
"Delete Character Style…",
|
||||
"Delete Paragraph Style…",
|
||||
"Delete the character style \"{name}\"?",
|
||||
"Delete the paragraph style \"{name}\"?",
|
||||
"IT",
|
||||
"Replace with:",
|
||||
"Show the Typography panel",
|
||||
"Stroke Behind Fill",
|
||||
"Stylistic Set",
|
||||
"The selected fonts have no OpenType features.",
|
||||
"This story was edited in the layout: you'll be asked before those edits are replaced",
|
||||
"This style is in use. Text using it will be reassigned to:",
|
||||
"VA",
|
||||
"Y:",
|
||||
"“{}” has been edited in this layout. Updating replaces those edits with the text from the file.",
|
||||
"(any)",
|
||||
"(unchanged)",
|
||||
"[No Language]",
|
||||
|
||||
@@ -154,6 +154,7 @@ fn define_lists_command_path_matches_the_type_menu() {
|
||||
assert!(menu_ids("Type").contains(&"list.define"));
|
||||
}
|
||||
|
||||
/// Checks that screen-mode menu checkmarks follow the current Normal or Preview mode.
|
||||
#[test]
|
||||
fn screen_mode_shows_the_current_mode() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
@@ -163,6 +164,24 @@ fn screen_mode_shows_the_current_mode() {
|
||||
assert_eq!((mode(&app, "normal"), mode(&app, "preview")), (Some(false), Some(true)));
|
||||
}
|
||||
|
||||
/// Checks that translated customization paths match the visible submenu captions, while
|
||||
/// allowing uncatalogued paths to keep their English fallback.
|
||||
#[test]
|
||||
fn hungarian_menu_paths_match_the_visible_submenu_labels() {
|
||||
// These paths are displayed by menu customization; they must use the same
|
||||
// captions as the actual menu tree, including nested special characters.
|
||||
for title in ["Edit", "Type", "View"] {
|
||||
for path in submenu_paths(title) {
|
||||
let translated = crate::i18n::tr("hu", &path);
|
||||
if translated == path {
|
||||
continue; // New, uncatalogued paths still fall back to English.
|
||||
}
|
||||
let visible = path.split('/').map(|part| crate::i18n::tr("hu", part)).collect::<Vec<_>>().join("/");
|
||||
assert_eq!(translated, visible, "{path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn walk(items: &[Item], out: &mut Vec<(String, String, Value)>) {
|
||||
for it in items {
|
||||
match it {
|
||||
@@ -648,6 +667,7 @@ fn split_window_contextual_bars_have_independent_widget_ids() {
|
||||
assert!(!bars[0].intersects(bars[1]), "each pane keeps its own bar: {bars:?}");
|
||||
}
|
||||
|
||||
/// Checks the document-grid menu label, shortcut, checkmark and repeated toggle behavior.
|
||||
#[test]
|
||||
fn show_document_grid_toggles_from_the_grids_and_guides_menu() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
@@ -666,6 +686,8 @@ fn show_document_grid_toggles_from_the_grids_and_guides_menu() {
|
||||
assert!(!app.ui.document_grid);
|
||||
}
|
||||
|
||||
/// Checks language selection and rejection of unknown language codes, Preferences dispatch,
|
||||
/// translated menu labels and preservation of document data when switching to Arabic.
|
||||
#[test]
|
||||
fn interface_language_switches_and_validates() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
@@ -683,6 +705,113 @@ fn interface_language_switches_and_validates() {
|
||||
run_ui(&mut app, "app.language", &json!({})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some("preferences"));
|
||||
assert_eq!(app.ui.language, "de");
|
||||
app.ui.dialog = None;
|
||||
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "zh"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.language, "zh");
|
||||
assert_eq!(crate::i18n::tr(&app.ui.language, "File"), "文件");
|
||||
|
||||
app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap();
|
||||
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "ar"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.language, "ar");
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "ar"})), 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("ar", title), title, "{title}");
|
||||
}
|
||||
// Every menu title has a translation (Japanese never matches the English).
|
||||
for (title, _) in menu_tree() {
|
||||
assert_ne!(crate::i18n::tr("ja", title), title, "{title}");
|
||||
}
|
||||
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "pt-br"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.language, "pt-br");
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "pt-br"})), Some(true));
|
||||
for (title, _) in menu_tree() {
|
||||
// "Layout" is the natural Brazilian Portuguese word, so it stays identical
|
||||
// to English; every other title must be translated.
|
||||
if title != "Layout" {
|
||||
assert_ne!(crate::i18n::tr("pt-br", title), title, "{title}");
|
||||
}
|
||||
}
|
||||
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "it"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.language, "it");
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "it"})), Some(true));
|
||||
for (title, _) in menu_tree() {
|
||||
// "File" and "Layout" are the usual Italian menu names, so they stay identical
|
||||
// to English; every other title must be translated.
|
||||
if title != "File" && title != "Layout" {
|
||||
assert_ne!(crate::i18n::tr("it", title), title, "{title}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks Hungarian selection, menu captions and saved UI preferences without changing
|
||||
/// document data or translating user-defined names.
|
||||
#[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");
|
||||
}
|
||||
|
||||
/// Checks Ukrainian selection, menu captions and saved UI preferences without changing
|
||||
/// document data or translating user-defined names.
|
||||
#[test]
|
||||
fn ukrainian_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 · Мої слова"})).unwrap();
|
||||
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "uk"})).unwrap().unwrap();
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "uk"})), 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("uk", 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, "uk");
|
||||
assert_eq!(crate::i18n::tr(&restored.language, "File"), "Файл");
|
||||
assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name");
|
||||
assert!(crate::i18n::LANGUAGES.contains(&("uk", "Українська")), "Preferences lists Ukrainian by its own name");
|
||||
}
|
||||
|
||||
/// Checks Russian selection, menu captions and saved UI preferences without changing
|
||||
/// document data or translating user-defined names.
|
||||
#[test]
|
||||
fn russian_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 · Мои слова"})).unwrap();
|
||||
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "ru"})).unwrap().unwrap();
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "ru"})), 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("ru", 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, "ru");
|
||||
assert_eq!(crate::i18n::tr(&restored.language, "File"), "Файл");
|
||||
assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name");
|
||||
assert!(crate::i18n::LANGUAGES.contains(&("ru", "Русский")), "Preferences lists Russian by its own name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -71,7 +71,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",
|
||||
|
||||
@@ -10,6 +10,8 @@ use crate::{DesignApp, icons};
|
||||
|
||||
const BTN: f32 = 24.0;
|
||||
|
||||
/// Draws the adaptive Tools panel with localized tool flyouts, fill/stroke controls and
|
||||
/// screen-mode buttons; flyout rows accommodate translated labels and their shortcuts.
|
||||
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let frame = egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.0, t.divider));
|
||||
@@ -173,43 +175,77 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.ctx(),
|
||||
|ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
for tl in g {
|
||||
ui.horizontal(|ui| {
|
||||
let (r, row) = ui.allocate_exact_size(vec2(210.0, 24.0), Sense::click());
|
||||
if row.hovered() {
|
||||
ui.painter().rect_filled(r, 2.0, t.hover);
|
||||
}
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
egui::Rect::from_min_size(r.min + vec2(4.0, 3.0), vec2(18.0, 18.0)),
|
||||
tl.icon,
|
||||
t.icon,
|
||||
);
|
||||
if tl.id == current {
|
||||
ui.painter().circle_filled(r.min + vec2(-2.0, 12.0), 2.0, t.accent);
|
||||
}
|
||||
ui.painter().text(
|
||||
r.min + vec2(28.0, 12.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
crate::i18n::tr(&app.ui.language, tl.label),
|
||||
egui::FontId::proportional(12.5),
|
||||
t.text,
|
||||
);
|
||||
if let Some(sc) = crate::menus::tool_shortcut_label(app, tl.id) {
|
||||
ui.painter().text(
|
||||
r.right_center() - vec2(6.0, 0.0),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
sc,
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
}
|
||||
row.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl.label));
|
||||
if row.clicked() {
|
||||
chosen = Some(tl.id);
|
||||
}
|
||||
});
|
||||
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(
|
||||
crate::menus::tool_shortcut_label(app, tl.id).unwrap_or_default(),
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
)
|
||||
.size()
|
||||
.x;
|
||||
width.max(label_width + shortcut_width + 46.0)
|
||||
});
|
||||
let max_width = (ui.ctx().content_rect().width()
|
||||
- egui::Frame::popup(ui.style()).total_margin().sum().x)
|
||||
.max(0.0);
|
||||
let mut scroll =
|
||||
egui::ScrollArea::horizontal().id_salt(("tool-flyout-scroll", gi)).max_width(max_width);
|
||||
if opened_now {
|
||||
scroll = scroll.horizontal_scroll_offset(0.0);
|
||||
}
|
||||
scroll.show(ui, |ui| {
|
||||
for tl in g {
|
||||
ui.horizontal(|ui| {
|
||||
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);
|
||||
}
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
egui::Rect::from_min_size(r.min + vec2(4.0, 3.0), vec2(18.0, 18.0)),
|
||||
tl.icon,
|
||||
t.icon,
|
||||
);
|
||||
if tl.id == current {
|
||||
ui.painter().circle_filled(r.min + vec2(-2.0, 12.0), 2.0, t.accent);
|
||||
}
|
||||
ui.painter().text(
|
||||
r.min + vec2(28.0, 12.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
crate::i18n::tr(&app.ui.language, tl.label),
|
||||
egui::FontId::proportional(12.5),
|
||||
t.text,
|
||||
);
|
||||
if let Some(sc) = crate::menus::tool_shortcut_label(app, tl.id) {
|
||||
ui.painter().text(
|
||||
r.right_center() - vec2(6.0, 0.0),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
sc,
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
}
|
||||
row.widget_info(|| {
|
||||
egui::WidgetInfo::labeled(
|
||||
egui::Role::Button,
|
||||
true,
|
||||
crate::i18n::tr(&app.ui.language, tl.label),
|
||||
)
|
||||
});
|
||||
if row.clicked() {
|
||||
chosen = Some(tl.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -19,10 +19,203 @@ fn column_width(cols: f32) -> f32 {
|
||||
8.0 + super::BTN * cols + 4.0 * (cols - 1.0) + 8.0
|
||||
}
|
||||
|
||||
/// Returns the bounds of the labeled test widget, failing if the label cannot be found.
|
||||
fn rect(h: &egui_kittest::Harness<'static, test_window::Window>, label: &str) -> egui::Rect {
|
||||
h.get_by_label(label).rect()
|
||||
}
|
||||
|
||||
/// Collects painted text bounds and their clip rectangles, including nested shapes.
|
||||
fn collect_text(shape: &egui::Shape, clip: egui::Rect, texts: &mut Vec<(String, egui::Rect, egui::Rect)>) {
|
||||
match shape {
|
||||
egui::Shape::Text(text) => texts.push((text.galley.text().to_owned(), text.visual_bounding_rect(), clip)),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().for_each(|shape| collect_text(shape, clip, texts)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks translated tooltips and right-click flyout rows in both Tools-panel layouts,
|
||||
/// including painted labels, unchanged shortcuts and unclipped, nonoverlapping text.
|
||||
#[test]
|
||||
fn hungarian_tool_buttons_translate_every_tool_and_keep_shortcuts() {
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
|
||||
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 shortcut = tool.shortcut.map(crate::menus::shortcut_text);
|
||||
assert_eq!(crate::menus::tool_shortcut_label(&h.state().app, tool.id), shortcut, "default binding for {}", tool.id);
|
||||
let tip = shortcut.map_or_else(|| label.to_owned(), |shortcut| format!("{label} ({shortcut})"));
|
||||
assert!(rect(&h, &tip).is_finite(), "{tip}");
|
||||
}
|
||||
// A user-assigned chord also has to fit beside a translated flyout label.
|
||||
let custom_shortcut = "Ctrl+Alt+Shift+F12";
|
||||
h.state_mut()
|
||||
.app
|
||||
.run(
|
||||
"window.setShortcut",
|
||||
serde_json::json!({"id": "tool.select", "params": {"tool": "ellipseFrame"}, "shortcut": custom_shortcut, "steal": true}),
|
||||
)
|
||||
.unwrap();
|
||||
for (gi, group) in designcraft_tools::TOOL_GROUPS.iter().enumerate() {
|
||||
// Single tools have no flyout; the collector group is split into separate
|
||||
// buttons in two-column mode and is covered by the tooltip checks above.
|
||||
if group.len() < 2 || (double && designcraft_tools::SPLIT_IN_TWO_COLUMNS.contains(&group[0].id)) {
|
||||
continue;
|
||||
}
|
||||
let first = group[0];
|
||||
h.state_mut().app.run("tool.select", serde_json::json!({"tool": first.id})).unwrap();
|
||||
h.run_steps(3);
|
||||
let label = crate::i18n::tr("hu", first.label);
|
||||
let tip = first.shortcut.map(crate::menus::shortcut_text).map_or_else(|| label.to_owned(), |shortcut| format!("{label} ({shortcut})"));
|
||||
let at = rect(&h, &tip).center();
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
}
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().app.ui.flyout, Some(gi), "right-click opens {} (double={double})", first.id);
|
||||
let mut texts = Vec::new();
|
||||
for shape in &h.output().shapes {
|
||||
collect_text(&shape.shape, shape.clip_rect, &mut texts);
|
||||
}
|
||||
for tool in *group {
|
||||
let label = crate::i18n::tr("hu", tool.label);
|
||||
// Flyout accessibility labels omit the shortcut; inspect the painted
|
||||
// text inside this row so another button cannot satisfy the assertion.
|
||||
let row = rect(&h, label);
|
||||
assert!(row.is_finite() && row.is_positive(), "{label}");
|
||||
let painted = |expected: &str| {
|
||||
let (_, bounds, clip) = texts
|
||||
.iter()
|
||||
.find(|(text, bounds, _)| text == expected && row.contains(bounds.center()))
|
||||
.unwrap_or_else(|| panic!("missing flyout text {expected:?} for {} (double={double})", tool.id));
|
||||
assert!(row.contains_rect(*bounds) && clip.contains_rect(*bounds), "clipped flyout text {expected:?}: {bounds:?}");
|
||||
*bounds
|
||||
};
|
||||
let label_bounds = painted(label);
|
||||
let binding = if tool.id == "ellipseFrame" { Some(custom_shortcut) } else { tool.shortcut };
|
||||
if let Some(shortcut) = binding.map(crate::menus::shortcut_text) {
|
||||
let shortcut_bounds = painted(&shortcut);
|
||||
assert!(label_bounds.right() < shortcut_bounds.left(), "label overlaps shortcut for {} (double={double})", tool.id);
|
||||
}
|
||||
}
|
||||
h.key_press(egui::Key::Escape);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().app.ui.flyout.is_none());
|
||||
}
|
||||
assert_eq!(crate::i18n::tr("hu", "Direct Selection Tool"), "Közvetlen kijelölő eszköz");
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps long translated flyout rows reachable by scrolling in a narrow viewport,
|
||||
/// resetting on reopening while preserving the open scroll position, shortcut and selection.
|
||||
#[test]
|
||||
fn hungarian_flyout_scrolls_horizontally_in_narrow_windows() {
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
|
||||
for double in [false, true] {
|
||||
for width in [180.0, 240.0] {
|
||||
let mut app = app(double);
|
||||
app.ui.language = "hu".into();
|
||||
let chord = "Ctrl+Alt+Shift+F12";
|
||||
app.run(
|
||||
"window.setShortcut",
|
||||
serde_json::json!({"id": "tool.select", "params": {"tool": "ellipseFrame"}, "shortcut": chord, "steal": true}),
|
||||
)
|
||||
.unwrap();
|
||||
app.select_tool("rectangleFrame");
|
||||
let mut h = test_window::open(app, vec2(width, 1000.0));
|
||||
let at = rect(&h, "Téglalapkeret eszköz (F)").center();
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
}
|
||||
h.run_steps(3);
|
||||
let gi = h.state().app.ui.flyout.unwrap();
|
||||
let area = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("flyout", gi)))).unwrap();
|
||||
let viewport = h.ctx.content_rect();
|
||||
assert!(viewport.expand(0.5).contains_rect(area), "flyout {area:?} exceeds viewport {viewport:?}");
|
||||
let label = "Ellipsziskeret eszköz";
|
||||
let shortcut = crate::menus::shortcut_text(chord);
|
||||
let painted = |h: &egui_kittest::Harness<'static, test_window::Window>, text: &str| {
|
||||
let row = rect(h, label);
|
||||
let mut texts = Vec::new();
|
||||
for shape in &h.output().shapes {
|
||||
collect_text(&shape.shape, shape.clip_rect, &mut texts);
|
||||
}
|
||||
let (_, bounds, clip) = texts.into_iter().find(|(t, b, _)| t == text && row.contains(b.center())).unwrap();
|
||||
(bounds, clip)
|
||||
};
|
||||
let (label_bounds, label_clip) = painted(&h, label);
|
||||
assert!(label_clip.contains_rect(label_bounds), "label must be readable at the start");
|
||||
let (before, clip) = painted(&h, &shortcut);
|
||||
let oversized = rect(&h, label).width() > clip.width();
|
||||
if width == 180.0 {
|
||||
assert!(oversized, "the tighter case must exercise horizontal overflow on every platform");
|
||||
}
|
||||
review_shot(&mut h, &format!("flyout-{width}-double-{double}-left.png"));
|
||||
if oversized {
|
||||
assert!(!clip.contains_rect(before), "the shortcut starts beyond the narrow viewport");
|
||||
wheel(&mut h, area.center(), vec2(-1000.0, 0.0));
|
||||
let (after, clip) = painted(&h, &shortcut);
|
||||
assert!(clip.contains_rect(after), "scrolling must make the complete shortcut readable");
|
||||
assert!(after.left() < before.left(), "horizontal input must move the row");
|
||||
h.run_steps(3);
|
||||
let (settled, clip) = painted(&h, &shortcut);
|
||||
assert!(clip.contains_rect(settled), "an open flyout must retain its horizontal scroll position");
|
||||
h.key_press(egui::Key::Escape);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().app.ui.flyout.is_none());
|
||||
// Reopen through the toolbar input path, without changing the stored scroll state.
|
||||
let at = rect(&h, "Téglalapkeret eszköz (F)").center();
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton {
|
||||
pos: at,
|
||||
button: PointerButton::Secondary,
|
||||
pressed,
|
||||
modifiers: Modifiers::NONE,
|
||||
});
|
||||
}
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().app.ui.flyout, Some(gi));
|
||||
let (reopened, clip) = painted(&h, label);
|
||||
assert!(clip.contains_rect(reopened), "a reopened flyout must show the complete translated label");
|
||||
assert!((reopened.left() - label_bounds.left()).abs() < 0.5, "reopening must return to the initial left edge");
|
||||
review_shot(&mut h, &format!("flyout-{width}-double-{double}-reopened.png"));
|
||||
wheel(&mut h, area.center(), vec2(-1000.0, 0.0));
|
||||
let (rescrolled, clip) = painted(&h, &shortcut);
|
||||
assert!(clip.contains_rect(rescrolled), "a reopened flyout must still allow scrolling to the shortcut");
|
||||
} else {
|
||||
assert!(clip.contains_rect(before));
|
||||
}
|
||||
review_shot(&mut h, &format!("flyout-{width}-double-{double}-right.png"));
|
||||
assert_eq!(crate::menus::tool_shortcut_label(&h.state().app, "ellipseFrame"), Some(shortcut));
|
||||
let at = rect(&h, label).intersect(area).center();
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
}
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().app.session.tool_id(), "ellipseFrame");
|
||||
assert!(h.state().app.ui.flyout.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves synthetic UI evidence only when the local review explicitly requests it.
|
||||
fn review_shot(h: &mut egui_kittest::Harness<'static, test_window::Window>, name: &str) {
|
||||
if let Some(dir) = std::env::var_os("DESIGNCRAFT_HU_REVIEW_SHOTS") {
|
||||
h.render().unwrap().save(std::path::Path::new(&dir).join(name)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
@@ -197,7 +390,8 @@ fn the_tools_panel_lays_out_like_indesigns() {
|
||||
for double in [false, true] {
|
||||
let h = test_window::open(app(double), vec2(1400.0, 900.0));
|
||||
let _ = rect(&h, "Gradient Swatch Tool (G)");
|
||||
let _ = rect(&h, "Gradient Feather Tool (Shift+G)");
|
||||
let feather = format!("Gradient Feather Tool ({})", crate::menus::shortcut_text("Shift+G"));
|
||||
let _ = rect(&h, &feather);
|
||||
let _ = rect(&h, "View Options");
|
||||
let split = ["Content Placer Tool (B)", "Apply Color (,)", "Apply Gradient (.)", "Normal"];
|
||||
for label in split {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (missing translations are reported, never a failure; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** minor (Hungarian interface catalog, bundled-font coverage and flyout regression checks) · **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/table.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
|
||||
@@ -19,9 +19,11 @@ in the UI crate appear in no catalog (some are test data or proper names), so th
|
||||
somewhat higher and every percentage below is a slight overestimate. Help: DesignCraft has no
|
||||
help content in any language (Help ▸ opens the project's pages).
|
||||
|
||||
A missing translation never fails a test, a build or a release. `cargo xtask i18n-report` (also
|
||||
a step of `cargo xtask ci`, so the list is in every gate log) prints how many interface strings
|
||||
each language lacks, then the strings.
|
||||
Missing translations normally fall back to English and are reported without blocking a build.
|
||||
Targeted regression tests can require specific captions, such as the Data Merge arrangement
|
||||
label, in every registered interface language. `cargo xtask i18n-report` (also a step of
|
||||
`cargo xtask ci`, so the list is in every gate log) prints how many interface strings each
|
||||
language lacks, then the strings.
|
||||
|
||||
InDesign 2026 ships 25 interface localizations (`.lproj` folders: cs, da, de, el, en, en_GB, es,
|
||||
fi, fr, hu, it, ja, ko, nb, nl, pl, pt, ro, ru, sq, sv, tr, uk, zh_CN, zh_TW); Arabic and Hebrew
|
||||
@@ -52,17 +54,39 @@ 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 interface (2026-11-01): `hu.rs` contains **1,906 entries and four context-specific
|
||||
captions**, including all 37 tool names. The initial catalog was prepared with AI assistance
|
||||
and personally reviewed in the app by **Veress Róbert**, a native Hungarian speaker with over
|
||||
30 years of DTP experience. Further terminology and in-context feedback is welcome. This is a
|
||||
partial catalog, not a new coverage measurement against current main: uncatalogued strings
|
||||
fall back to English. Preferences lists `Magyar`; interface selection does not enable the
|
||||
separate spelling and hyphenation work in #1484.
|
||||
|
||||
The interface uses bundled **Source Sans 3 Regular** and **Source Sans 3 Semibold**; monospace
|
||||
labels use **JetBrains Mono Regular** (`assets/fonts/`, registered by `theme::font_definitions`).
|
||||
`hungarian_catalog_glyphs_exist_in_the_bundled_ui_fonts` checks the actual configured font
|
||||
bytes, every Latin letter used by the catalog and all Hungarian accented vowels in both cases,
|
||||
including **ŐőŰű**, with system and optional craft-fonts fallbacks disabled. No new font asset
|
||||
or font dependency is needed.
|
||||
|
||||
Hungarian regressions cover placeholder/context integrity, formal address and count/column
|
||||
terminology, language persistence, matching menu paths, translated tool labels and real
|
||||
right-click flyouts in both Tools-panel layouts. Narrow-window coverage exercises horizontal
|
||||
scrolling, complete label/shortcut accessibility and selecting a scrolled row at 180 and
|
||||
240 points; the existing wide-window checks keep their assertions.
|
||||
|
||||
## Other shipped languages
|
||||
|
||||
| Language | Code | UI strings translated | Status |
|
||||
|---|---|---|---|
|
||||
| Hungarian | hu | 1,906 entries + 4 context captions (2026-11-01); initial catalog native-reviewed | 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 |
|
||||
| Polish | pl | 1,717 (99%), count captions category-first; AI-drafted, awaiting native review | partial |
|
||||
| Georgian | ka | 1,724 (~100%: every catalog key plus the command labels the others lack), count captions as `noun: n` | partial (reviewed by a native speaker) |
|
||||
|
||||
DesignCraft ships **5** interface languages beyond the twelve; InDesign ships 17 beyond its
|
||||
DesignCraft ships **6** interface languages beyond the twelve; InDesign ships 17 beyond its
|
||||
eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, tr, uk, zh_TW).
|
||||
|
||||
## Document languages
|
||||
@@ -92,6 +116,7 @@ German variants have no automatic hyphenation; their German shaping and quotes a
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | minor | Initial Hungarian interface: 1,906 entries, four context captions, native review, bundled-font glyph coverage and wide/narrow flyout regressions |
|
||||
| 2026-10-11 | minor | Localized Japanese print, import and table controls; checked Japanese and English renders |
|
||||
| 2026-10-14 | minor | Georgian (`ka`) interface added |
|
||||
| 2026-10-10 | major | Created: catalogs measured (1,724 strings), the twelve key languages plus Italian and Ukrainian |
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string>de</string>
|
||||
<string>es</string>
|
||||
<string>fr</string>
|
||||
<string>hu</string>
|
||||
<string>it</string>
|
||||
<string>ja</string>
|
||||
<string>ka</string>
|
||||
|
||||
Reference in New Issue
Block a user