From e8908b0fc26b7aabca55cdb3eb40d3fbfab08fe8 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:56:21 +0200 Subject: [PATCH 01/75] 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. --- ROADMAP.md | 2 + contributors/people.toml | 3 + crates/ui-egui/src/i18n.rs | 51 +- crates/ui-egui/src/i18n/hu.rs | 1973 +++++++++++++++++++++++++++++++++ crates/ui-egui/src/menus.rs | 22 +- crates/ui-egui/src/toolbar.rs | 49 +- docs/localization-parity.md | 13 +- 7 files changed, 2107 insertions(+), 6 deletions(-) create mode 100644 crates/ui-egui/src/i18n/hu.rs diff --git a/ROADMAP.md b/ROADMAP.md index 9bd0274ef..cb633c50f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 `` 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. diff --git a/contributors/people.toml b/contributors/people.toml index 7a33de655..21034681c 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -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 diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..71861bcf9 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -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> = OnceLock::new(); static FRENCH: OnceLock> = OnceLock::new(); static GEORGIAN: OnceLock> = OnceLock::new(); + static HUNGARIAN: OnceLock> = 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"); diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs new file mode 100644 index 000000000..332f3c038 --- /dev/null +++ b/crates/ui-egui/src/i18n/hu.rs @@ -0,0 +1,1973 @@ +//! Hungarian interface translation, prepared with AI assistance and initially reviewed +//! by a native Hungarian contributor with 30+ years of DTP experience. +//! Further terminology and in-context UI review is welcome. +//! Uncatalogued keys fall back to English. +pub(super) const TABLE: &[(&str, &str)] = &[ + ("Redefine Style", "Stílus újradefiniálása"), + ("Alt-click clears overrides", "Alt-kattintás: felülírások törlése"), + ("Apply \"{name}\", Clear Overrides", "„{name}” alkalmazása, felülírások törlése"), + (" Close ", " Bezárás "), + (" New file ", " Új fájl "), + (" OK ", " OK "), + (" Open ", " Megnyitás "), + (" Open sample magazine ", " Mintaújság megnyitása "), + (" (auto)", " (automatikus)"), + ("(Text Color)", "(Szövegszín)"), + ("(Top Level)", "(Legfelső szint)"), + ("(import)", "(importálás)"), + ("180°", "180°"), + ("90° CCW", "90° balra"), + ("90° CW", "90° jobbra"), + ("A, B, C, D...", "A, B, C, D..."), + ("About DesignCraft", "A DesignCraft névjegye"), + ("Absolute Colorimetric", "Abszolút kolorimetrikus"), + ("Absolute Numbering (instead of Section Numbering)", "Abszolút oldalszámozás (szakasz szerinti helyett)"), + ("Accept", "Elfogadás"), + ("Accept All", "Összes elfogadása"), + ("Accept All Changes", "Összes módosítás elfogadása"), + ("Accept Change", "Módosítás elfogadása"), + ("Activate Document", "Dokumentum aktiválása"), + ("Activate Panel", "Panel aktiválása"), + ("Actual Size", "Tényleges méret"), + ("Add", "Hozzáadás"), + ("Add Anchor", "Csomópont hozzáadása"), + ("Add Anchor Point", "Csomópont hozzáadása"), + ("Add Document", "Dokumentum hozzáadása"), + ("Add Document…", "Dokumentum hozzáadása…"), + ("Add Hyphenation Exception", "Elválasztási kivétel hozzáadása"), + ("Add Item", "Elem hozzáadása"), + ("Add Page to Spread", "Oldal hozzáadása az oldalpárhoz"), + ("Add Selection", "Kijelölt elemek hozzáadása"), + ("Add Selection to Article", "Kijelölt elemek hozzáadása a cikkhez"), + ("Add Unnamed Colors", "Névtelen színek hozzáadása"), + ( + "Add and remove pages as the primary text frame's story grows and shrinks", + "Oldalak hozzáadása és eltávolítása az elsődleges szövegkeret szövegfolyamának hosszához igazodva", + ), + ("Add the selection", "Kijelölt elemek hozzáadása"), + ("Add to Dictionary", "Hozzáadás a szótárhoz"), + ("Add to Flex Layout", "Hozzáadás a rugalmas elrendezéshez"), + ("Add to Previous Spread", "Hozzáadás az előző oldalpárhoz"), + ("Add to Swatches", "Hozzáadás a színtárhoz"), + ("Added", "Hozzáadva"), + ("Added Text (highlighted)", "Beszúrt szöveg (kiemelve)"), + ("Adjust Layout", "Elrendezés igazítása"), + ("Adjust Layout (objects follow the new page size)", "Elrendezés igazítása (az objektumok követik az új oldalméretet)"), + ("Advanced", "Haladó"), + ("Advanced Character Formats", "Speciális karakterformázás"), + ("Advanced Type", "Speciális szövegbeállítások"), + ("After", "Utána"), + ("After First:", "Az első után:"), + ("Align", "Igazítás"), + ("Align Center", "Középre igazítás"), + ("Align From:", "Igazítás alapja:"), + ("Align Left", "Balra igazítás"), + ("Align Left Edge", "Bal szél igazítása"), + ("Align Right", "Jobbra igazítás"), + ("Align Stroke:", "Körvonal igazítása:"), + ("Align away from spine", "Igazítás a gerinctől kifelé"), + ("Align bottom edges", "Alsó szélek igazítása"), + ("Align center", "Középre igazítás"), + ("Align horizontal centers", "Vízszintes középpontok igazítása"), + ("Align left", "Balra igazítás"), + ("Align left edges", "Bal szélek igazítása"), + ("Align right", "Jobbra igazítás"), + ("Align right edges", "Jobb szélek igazítása"), + ("Align to Key Object", "Igazítás a kulcsobjektumhoz"), + ("Align to Margins", "Igazítás a margókhoz"), + ("Align to Object Centers", "Igazítás az objektumok középpontjához"), + ("Align to Object Edges", "Igazítás az objektumok széléhez"), + ("Align to Page", "Igazítás az oldalhoz"), + ("Align to Selection", "Igazítás a kijelöléshez"), + ("Align to Spread", "Igazítás az oldalpárhoz"), + ("Align to baseline grid", "Igazítás az alapvonalrácshoz"), + ("Align top edges", "Felső szélek igazítása"), + ("Align towards spine", "Igazítás a gerinc felé"), + ("Align vertical centers", "Függőleges középpontok igazítása"), + ("Alignment", "Igazítás"), + ("Alignment:", "Igazítás:"), + ("All Caps", "Nagybetűk"), + ("All Documents", "Összes dokumentum"), + ("All Spots to Process", "Összes direktszín átalakítása nyomdai alapszínekké"), + ("All records", "Összes rekord"), + ("Allow Selected Spread to Shuffle", "Kijelölt oldalpár oldalainak átrendezése engedélyezett"), + ("Allow Spread to Shuffle", "Oldalpár oldalainak átrendezése engedélyezett"), + ("Alpha Channel", "Alfa-csatorna"), + ("Alternating Row Fills", "Váltakozó sorkitöltések"), + ("Anchored Object", "Rögzített objektum"), + ("Anchored Object Options", "Rögzített objektum beállításai"), + ("Anchored Object Options…", "Rögzített objektum beállításai…"), + ("Anchored Objects", "Rögzített objektumok"), + ("Angle", "Szög"), + ("Angle:", "Szög:"), + ("Appearance", "Megjelenés"), + ("Appearance of Black", "Fekete megjelenítése"), + ("Applies to fill", "A kitöltésre vonatkozik"), + ("Applies to text", "A szövegre vonatkozik"), + ("Apply", "Alkalmazás"), + ("Apply Attributes (Eyedropper", "Tulajdonságok alkalmazása (pipetta"), + ("Apply Attributes (Eyedropper)", "Tulajdonságok alkalmazása (pipetta)"), + ("Apply Cell Style", "Cellastílus alkalmazása"), + ("Apply Character Style", "Karakterstílus alkalmazása"), + ("Apply Color", "Szín alkalmazása"), + ("Apply Color (,)", "Szín alkalmazása (,)"), + ("Apply Condition", "Feltétel alkalmazása"), + ("Apply Gradient", "Színátmenet alkalmazása"), + ("Apply Gradient (.)", "Színátmenet alkalmazása (.)"), + ("Apply None", "Szín eltávolítása"), + ("Apply None (/)", "Szín eltávolítása (/)"), + ("Apply Normal", "Normál karakterhelyzet"), + ("Apply Object Style", "Objektumstílus alkalmazása"), + ("Apply Paragraph Style", "Bekezdésstílus alkalmazása"), + ("Apply Parent", "Mesteroldal alkalmazása"), + ("Apply Parent to Pages", "Mesteroldal alkalmazása az oldalakra"), + ("Apply Parent to Pages…", "Mesteroldal alkalmazása az oldalakra…"), + ("Apply Style:", "Alkalmazandó stílus:"), + ("Apply Subscript", "Alsó index alkalmazása"), + ("Apply Superscript", "Felső index alkalmazása"), + ("Apply Table Style", "Táblázatstílus alkalmazása"), + ("Apply to All Spreads", "Alkalmazás az összes oldalpárra"), + ("Apply to Stroke", "Alkalmazás a körvonalra"), + ("Arabic", "Arab"), + ("Arrange", "Sorrend"), + ("Art", "Grafika"), + ("ArtCraft App Page", "ArtCraft alkalmazásoldal"), + ("ArtCraft App Page…", "ArtCraft alkalmazásoldal…"), + ("ArtCraft Website", "ArtCraft weboldala"), + ("ArtCraft Website…", "ArtCraft weboldala…"), + ("Article Options", "Cikk beállításai"), + ("Articles", "Cikkek"), + ("Ascent", "Felső betűszármagasság"), + ("At Least", "Legalább"), + ("Attributes", "Tulajdonságok"), + ("Auto", "Automatikus"), + ("Auto Size", "Automatikus méretezés"), + ("Auto-Fit", "Automatikus illesztés"), + ("Autocorrect", "Automatikus javítás"), + ("Bar", "Vonás"), + ("Barbed", "Szakállas nyíl"), + ("Based On:", "Alapstílus:"), + ("Baseline", "Alapvonal"), + ("Baseline Grid", "Alapvonalrács"), + ("Baseline Shift:", "Alapvonal-eltolás:"), + ("Basic Character Formats", "Alapvető karakterformázás"), + ("Basic Feather", "Alapvető lágyítás"), + ("Before", "Előtte"), + ("Before Last:", "Az utolsó előtt:"), + ("Between syllables (not only at spaces)", "Szótagok között (nem csak szóközöknél)"), + ("Bevel", "Levágott sarok"), + ("Bevel and Emboss", "Domborítás"), + ("Beveled Rectangle", "Levágott sarkú téglalap"), + ("Bind to frame", "Hozzárendelés a kerethez"), + ("Binding", "Kötésirány"), + ("Binding:", "Kötésirány:"), + ("Black", "Fekete"), + ("Bleed", "Kifutó"), + ("Bleed Marks", "Kifutójelek"), + ("Bleed and Slug", "Kifutó és infóterület"), + ("Bleed:", "Kifutó:"), + ("Blinds", "Redőny"), + ("Body Rows", "Törzssorok"), + ("Bold", "Félkövér"), + ("Book", "Könyv"), + ("Bookmarks", "Könyvjelzők"), + ("Border", "Szegély"), + ("Both", "Mindkettő"), + ("Bottom", "Alul"), + ("Bottom Inset", "Alsó belső margó"), + ("Bottom:", "Lent:"), + ("Box", "Doboz"), + ("Break Link to Style", "Stíluskapcsolat megszüntetése"), + ("Break Thread", "Szövegkeret-lánc megszakítása"), + ("Bring Forward", "Egy szinttel előrébb"), + ("Bring to Front", "Előtérbe hozás"), + ("Bullet Character", "Felsorolásjel"), + ("Bulleted & Numbered Lists", "Felsorolások és számozott listák"), + ("Bulleted List", "Felsorolás"), + ("Bulleted and Numbered Lists", "Felsorolások és számozott listák"), + ("Bullets and Numbering", "Felsorolás és számozás"), + ("Business Card", "Névjegykártya"), + ("Butt", "Tompa vonalvég"), + ("Buttons", "Gombok"), + ("Buttons act in interactive PDF export.", "A gombok interaktív PDF-exportban működnek."), + ("Buttons and Forms", "Gombok és űrlapok"), + ("CMYK", "CMYK"), + ("CMYK:", "CMYK:"), + ("Can't Open the File", "A fájl nem nyitható meg"), + ("Can't Place the File", "A fájl nem helyezhető el"), + ("Cancel", "Mégse"), + ("Cap Height", "Nagybetűmagasság"), + ("Cap:", "Vonalvég:"), + ("Captions", "Képaláírások"), + ("Case sensitive", "Kis- és nagybetű megkülönböztetése"), + ("Case:", "Betűváltozat:"), + ("Cell Options", "Cellabeállítások"), + ("Cell Style", "Cellastílus"), + ("Cell Style Options…", "Cellastílus beállításai…"), + ("Cell Text", "Cellaszöveg"), + ("Cell Type", "Cellatípus"), + ("Center", "Középre"), + ("Center Content", "Tartalom középre igazítása"), + ("Center in frame", "Középre igazítás a keretben"), + ("Change", "Csere"), + ("Change All", "Összes cseréje"), + ("Change All (Object", "Összes cseréje (objektum"), + ("Change All (Object)", "Összes cseréje (objektum)"), + ("Change Case", "Kis- és nagybetűk váltása"), + ("Change Panel Layout", "Panelelrendezés módosítása"), + ("Change To", "Csere erre"), + ("Change to:", "Csere erre:"), + ("Changes", "Módosítások"), + ("Character", "Karakter"), + ("Character Color", "Karakter színe"), + ("Character Formatting", "Karakterformázás"), + ("Character Settings", "Karakterbeállítások"), + ("Character Style Options", "Karakterstílus beállításai"), + ("Character Style Options…", "Karakterstílus beállításai…"), + ("Character Style:", "Karakterstílus:"), + ("Character Styles", "Karakterstílusok"), + ("Character Styles panel", "Karakterstílusok panel"), + ("Characters", "Karakterek"), + ("Characters:", "Karakterek:"), + ("Check Box", "Jelölőnégyzet"), + ("Check Spelling", "Helyesírás-ellenőrzés"), + ("Check Spelling…", "Helyesírás-ellenőrzés…"), + ("Checked by default", "Alapértelmezés szerint bejelölve"), + ("Chinese", "Kínai"), + ("Choices", "Választási lehetőségek"), + ("Choose Image…", "Kép kiválasztása…"), + ("Choose a link.", "Válasszon egy csatolást."), + ("Circle", "Kör"), + ("Circle - Solid", "Kitöltött kör"), + ("Class:", "Osztály:"), + ("Clear", "Törlés"), + ("Clear Conveyor", "Tartalomgyűjtő kiürítése"), + ("Clear Rotation", "Elforgatás törlése"), + ("Clear Transformations", "Átalakítások törlése"), + ("Clear overrides", "Helyi felülírások törlése"), + ("Click to pin the object relative to the page edge.", "Kattintson az objektum oldalszélhez rögzítéséhez."), + ("Clipping Path", "Vágógörbe"), + ("Clipping Path…", "Vágógörbe…"), + ("Close", "Bezárás"), + ("Close Book", "Könyv bezárása"), + ("Close Library", "Könyvtár bezárása"), + ("Close Panel", "Panel bezárása"), + ("Close Path", "Görbe bezárása"), + ("Closed Path", "Zárt görbe"), + ("Col ←", "Oszlop ←"), + ("Col →", "Oszlop →"), + ("Collect", "Gyűjtés"), + ( + "Collect objects with the Content Collector (B); place them with the Content Placer.", + "Gyűjtse össze az objektumokat a Tartalomgyűjtővel (B), majd helyezze el őket a Tartalomelhelyezővel.", + ), + ("Color", "Szín"), + ("Color Blindness – Deuteranopia", "Színtévesztés – deuteranópia"), + ("Color Blindness – Protanopia", "Színtévesztés – protanópia"), + ("Color Group Options", "Színcsoport beállításai"), + ("Color Picker", "Színválasztó"), + ("Color Settings", "Színbeállítások"), + ("Color Settings…", "Színbeállítások…"), + ("Color:", "Szín:"), + ("Colour", "Szín"), + ("Colour Picker", "Színválasztó"), + ("Column", "Hasáb"), + ("Column Break", "Hasábtörés"), + ("Column Rules", "Hasábelválasztó vonalak"), + ("Column Width", "Oszlopszélesség"), + ("Column spacing", "Oszlopköz"), + ("Columns", "Hasábok"), + ("Columns first", "Először az oszlopok"), + ("Columns:", "Hasábok:"), + ("Comb", "Fésű"), + ("Combo Box", "Kombinált lista"), + ("Commercial Printing", "Nyomdai előállítás"), + ("Community", "Közösség"), + ("Community & Project Links", "Közösségi és projekthivatkozások"), + ("Composer", "Tördelő"), + ("Composer:", "Tördelő:"), + ("Composition", "Tördelés"), + ("Compress images", "Képek tömörítése"), + ("Compression", "Tömörítés"), + ("Condition Options…", "Feltétel beállításai…"), + ("Conditional Text", "Feltételes szöveg"), + ("Conditions", "Feltételek"), + ("Constrain proportions", "Arányok megtartása"), + ("Constrain proportions for width and height", "Szélesség és magasság arányának megtartása"), + ("Container", "Tároló"), + ("Content", "Tartalom"), + ("Content Fitting", "Tartalom illesztése"), + ("Content:", "Tartalom:"), + ("Contextual Alternates", "Környezetfüggő változatok"), + ("Contextual Task Bar", "Környezetfüggő feladatsáv"), + ("Control", "Vezérlő"), + ("Controlled by Parent", "A mesteroldal határozza meg"), + ("Convert Cell Type", "Cellatípus átalakítása"), + ("Convert Cell to Graphic Cell", "Cella átalakítása grafikus cellává"), + ("Convert Cell to Graphic Cell…", "Cella átalakítása grafikus cellává…"), + ("Convert Cell to Text Cell", "Cella átalakítása szöveges cellává"), + ("Convert Color", "Szín átalakítása"), + ("Convert Direction Point", "Irányítópont átalakítása"), + ("Convert Note to Text", "Jegyzet átalakítása szöveggé"), + ("Convert Point", "Pont átalakítása"), + ("Convert Point:", "Pont átalakítása:"), + ("Convert Selection to Multi-State Object", "Kijelölés átalakítása többállapotú objektummá"), + ("Convert Shape", "Alakzat átalakítása"), + ("Convert Shape:", "Alakzat átalakítása:"), + ("Convert Table to Text", "Táblázat átalakítása szöveggé"), + ("Convert Text to Table", "Szöveg átalakítása táblázattá"), + ("Convert to Button", "Átalakítás gombbá"), + ("Convert to Flex Layout", "Átalakítás rugalmas elrendezéssé"), + ("Convert to Form Field", "Átalakítás űrlapmezővé"), + ("Convert to Liquid Guide", "Átalakítás alkalmazkodó segédvonallá"), + ("Convert to Object", "Átalakítás objektummá"), + ("Convert to Text", "Átalakítás szöveggé"), + ("Conveyor", "Tartalomgyűjtő tálca"), + ("Copies:", "Példányszám:"), + ("Copy", "Másolás"), + ("Copy Link(s", "Csatolások másolása"), + ("Copy Link(s) To…", "Csatolások másolása ide…"), + ("Copyright Symbol", "Szerzői jog jele"), + ("Corner", "Sarok"), + ("Corner Options", "Sarokbeállítások"), + ("Corner Options…", "Sarokbeállítások…"), + ("Count", "Darabszám"), + ("Count:", "Darabszám:"), + ("Cover", "Borítás"), + ("Create", "Létrehozás"), + ("Create Alternate Layout", "Alternatív elrendezés létrehozása"), + ("Create Alternate Layout…", "Alternatív elrendezés létrehozása…"), + ("Create Flex Layout", "Rugalmas elrendezés létrehozása"), + ("Create Frame", "Keret létrehozása"), + ("Create Guides", "Segédvonalak létrehozása"), + ("Create Guides…", "Segédvonalak létrehozása…"), + ("Create Line", "Vonal létrehozása"), + ("Create Links When Placing Text Files", "Csatolások létrehozása szövegfájlok elhelyezésekor"), + ("Create Merged Document…", "Egyesített dokumentum létrehozása…"), + ("Create New Layer", "Új réteg létrehozása"), + ("Create New Style from Selection", "Új stílus létrehozása a kijelölésből"), + ("Create Outlines", "Görbévé alakítás"), + ("Create Path", "Görbe létrehozása"), + ("Create Table", "Táblázat létrehozása"), + ("Create Table…", "Táblázat létrehozása…"), + ("Create as a grid", "Létrehozás rácsként"), + ("Create new page", "Új oldal létrehozása"), + ("Create new style", "Új stílus létrehozása"), + ("Crop", "Vágás"), + ("Crop Amount", "Vágás mértéke"), + ("Crop Marks", "Vágójelek"), + ("Crop to:", "Vágás határa:"), + ("Cross-Reference Format", "Kereszthivatkozás formátuma"), + ("Cross-Reference Formats", "Kereszthivatkozás-formátumok"), + ("Cross-References", "Kereszthivatkozások"), + ("Current Page Number", "Aktuális oldalszám"), + ("Cursor Key:", "Kurzorgomb lépésköze:"), + ("Curved", "Ívelt"), + ("Custom", "Egyéni"), + ("Custom Page Size…", "Egyéni oldalméret…"), + ("Custom Tracking/Kerning", "Egyéni betűköz/alávágás"), + ("Cut", "Kivágás"), + ("Cyan", "Cián"), + ("Czech", "Cseh"), + ("Danish", "Dán"), + ("Dark", "Sötét"), + ("Dashed", "Szaggatott"), + ("Data Fields", "Adatmezők"), + ("Data Merge", "Adategyesítés"), + ("Data Merge Fields", "Adategyesítési mezők"), + ("Data Merge Options", "Adategyesítés beállításai"), + ("Decrease Point Size", "Betűméret csökkentése"), + ("Default", "Alapértelmezett"), + ("Default Figure Style", "Alapértelmezett számjegystílus"), + ("Default Fill and Stroke", "Alapértelmezett kitöltés és körvonal"), + ("Default Fill and Stroke (D)", "Alapértelmezett kitöltés és körvonal (D)"), + ("Default View:", "Alapértelmezett nézet:"), + ("Define Cross-Reference Format", "Kereszthivatkozás-formátum meghatározása"), + ("Define Cross-Reference Format…", "Kereszthivatkozás-formátum meghatározása…"), + ("Define Lists", "Listák meghatározása"), + ("Define Lists…", "Listák meghatározása…"), + ("Define Text Variable", "Szövegváltozó meghatározása"), + ("Define Text Variable…", "Szövegváltozó meghatározása…"), + ("Delete", "Törlés"), + ("Delete All Guides on Spread", "Oldalpár összes segédvonalának törlése"), + ("Delete Anchor Point", "Csomópont törlése"), + ("Delete Article", "Cikk törlése"), + ("Delete Bookmark", "Könyvjelző törlése"), + ("Delete Character Style", "Karakterstílus törlése"), + ("Delete Character Style…", "Karakterstílus törlése…"), + ("Delete Column", "Oszlop törlése"), + ("Delete Condition", "Feltétel törlése"), + ("Delete DTD", "DTD törlése"), + ("Delete Endnote", "Végjegyzet törlése"), + ("Delete Footnote", "Lábjegyzet törlése"), + ("Delete Guide", "Segédvonal törlése"), + ("Delete Hyperlink", "Hiperhivatkozás törlése"), + ("Delete Item", "Elem törlése"), + ("Delete Layer", "Réteg törlése"), + ("Delete List", "Lista törlése"), + ("Delete Note", "Jegyzet törlése"), + ("Delete Page", "Oldal törlése"), + ("Delete Pages", "Oldalak törlése"), + ("Delete Paragraph Style", "Bekezdésstílus törlése"), + ("Delete Paragraph Style…", "Bekezdésstílus törlése…"), + ("Delete Row", "Sor törlése"), + ("Delete Spread", "Oldalpár törlése"), + ("Delete Stroke Style", "Körvonalstílus törlése"), + ("Delete Swatch", "Színminta törlése"), + ("Delete Table", "Táblázat törlése"), + ("Delete Tag", "Címke törlése"), + ("Delete Text", "Szöveg törlése"), + ("Delete Text Variable", "Szövegváltozó törlése"), + ("Delete Type from Path", "Görbére írt szöveg törlése"), + ("Delete Unused Layers", "Nem használt rétegek törlése"), + ("Delete Workspace", "Munkaterület törlése"), + ("Delete Workspace…", "Munkaterület törlése…"), + ("Delete selected pages", "Kijelölt oldalak törlése"), + ("Delete the character style \"{name}\"?", "Törli a(z) „{name}” karakterstílust?"), + ("Delete the paragraph style \"{name}\"?", "Törli a(z) „{name}” bekezdésstílust?"), + ("Deleted", "Törölve"), + ("Deselect All", "Kijelölés megszüntetése"), + ("DesignCraft", "DesignCraft"), + ("DesignCraft Website", "DesignCraft weboldala"), + ("DesignCraft Website…", "DesignCraft weboldala…"), + ("DesignCraft on GitHub", "DesignCraft a GitHubon"), + ("DesignCraft on GitHub…", "DesignCraft a GitHubon…"), + ("Desired", "Kívánt"), + ("Desktop Printing", "Asztali nyomtatás"), + ("Detach All Objects from Parent", "Összes objektum leválasztása a mesteroldalról"), + ("Detect Edges", "Szélek felismerése"), + ("Deutsch", "Deutsch"), + ("Dialog", "Párbeszédablak"), + ("Dictionary", "Szótár"), + ("Digital Publishing", "Digitális kiadványkészítés"), + ("Digits", "Számjegyek"), + ("Dimensions Include Stroke Weight", "A méretek tartalmazzák a körvonal vastagságát"), + ("Direction", "Irány"), + ("Directional Feather", "Irányított lágyítás"), + ("Discretionary Hyphen", "Feltételes elválasztójel"), + ("Discretionary Ligatures", "Tetszőleges ligatúrák"), + ("Display All Blacks Accurately", "Minden fekete pontos megjelenítése"), + ("Display All Blacks as Rich Black", "Minden fekete megjelenítése dús feketeként"), + ("Display Performance", "Megjelenítési teljesítmény"), + ("Dissolve", "Áttűnés"), + ("Distance", "Távolság"), + ("Distribute", "Elosztás"), + ("Distribute Columns Evenly", "Oszlopok egyenletes elosztása"), + ("Distribute Rows Evenly", "Sorok egyenletes elosztása"), + ("Distribute ↔", "Elosztás ↔"), + ("Distribute ↕", "Elosztás ↕"), + ("Dock Panel", "Panel dokkolása"), + ("Dock panel", "Panel dokkolása"), + ("Document", "Dokumentum"), + ("Document CMYK", "Dokumentum CMYK-színtere"), + ("Document Endnote Options", "Dokumentum végjegyzet-beállításai"), + ("Document Endnote Options…", "Dokumentum végjegyzet-beállításai…"), + ("Document Footnote Options", "Dokumentum lábjegyzet-beállításai"), + ("Document Footnote Options...", "Dokumentum lábjegyzet-beállításai..."), + ("Document Footnote Options…", "Dokumentum lábjegyzet-beállításai…"), + ("Document Grid", "Dokumentumrács"), + ("Document Preferences", "Dokumentumbeállítások"), + ("Document Presets", "Dokumentumkészletek"), + ("Document RGB", "Dokumentum RGB-színtere"), + ("Document Recovery Data", "Dokumentum helyreállítási adatai"), + ("Document Setup", "Dokumentum beállításai"), + ("Document Setup…", "Dokumentum beállításai…"), + ("Document…", "Dokumentum…"), + ("Don't Save", "Ne mentse"), + ("Done", "Kész"), + ("Dotted", "Pontozott"), + ("Double Left Quotation Marks", "Bal oldali dupla idézőjel"), + ("Double Right Quotation Marks", "Jobb oldali dupla idézőjel"), + ("Drag Rows or Columns", "Sorok vagy oszlopok húzása"), + ("Draw Missing Glyphs from Fallback Fonts", "Hiányzó karakterjelek megjelenítése helyettesítő betűtípusból"), + ("Drop Caps", "Iniciálék"), + ("Drop Caps and Nested Styles", "Iniciálék és beágyazott stílusok"), + ("Drop Chars", "Iniciálé karaktereinek száma"), + ("Drop Lines", "Iniciálé sorainak száma"), + ("Drop Shadow", "Vetett árnyék"), + ("Duplicate", "Másolat készítése"), + ("Duplicate Spread", "Oldalpár másolása"), + ("Duplicate Spread at End of Document", "Oldalpár másolása a dokumentum végére"), + ("Dutch: 2005 Reform", "Holland: 2005-ös reform"), + ("Dynamic Spelling", "Folyamatos helyesírás-ellenőrzés"), + ("EPUB and HTML", "EPUB és HTML"), + ("Edit", "Szerkesztés"), + ("Edit \"{name}\"…", "„{name}” szerkesztése…"), + ("Edit Endnote", "Végjegyzet szerkesztése"), + ("Edit Note", "Jegyzet szerkesztése"), + ("Edit Page", "Oldal szerkesztése"), + ("Edit Parents", "Mesteroldalak szerkesztése"), + ("Edit Story", "Szövegfolyam szerkesztése"), + ("Edit in Story Editor", "Szerkesztés a szövegszerkesztőben"), + ("Edit page size", "Oldalméret szerkesztése"), + ("Edit/Interface Language", "Szerkesztés/Felület nyelve"), + ("Edit/Spelling", "Szerkesztés/Helyesírás"), + ("Edit/Transparency Blend Space", "Szerkesztés/Átlátszóság keverési színtere"), + ("Edit/Transparency Flattener Presets", "Szerkesztés/Lapítási készletek"), + ("Editorial", "Szerkesztőség"), + ("Effects", "Hatások"), + ("Effects: Drop Shadow", "Hatások: vetett árnyék"), + ("Ellipse", "Ellipszis"), + ("Ellipsis", "Három pont"), + ("Em Box Height", "Négyzet magassága"), + ("Em Dash", "Hosszú gondolatjel"), + ("Em Space", "Négyzetnyi szóköz"), + ("Email", "E-mail"), + ("Embed", "Beágyazás"), + ("Embed Link", "Csatolás beágyazása"), + ("Embedded", "Beágyazva"), + ("En Dash", "Félkvirtmínusz"), + ("En Space", "Félnégyzetnyi szóköz"), + ("Enable Autocorrect", "Automatikus javítás engedélyezése"), + ("Enable Dynamic Spelling (underline misspelled words)", "Folyamatos helyesírás-ellenőrzés (hibás szavak aláhúzása)"), + ("Endnotes", "Végjegyzetek"), + ("English", "English"), + ("English: Canadian", "Angol: kanadai"), + ("English: UK", "Angol: brit"), + ("English: USA", "Angol: amerikai"), + ("Entire Pasteboard", "Teljes munkaterület"), + ("Erase", "Radírozás"), + ("Español", "Español"), + ("Essentials", "Alapok"), + ("Essentials Classic", "Klasszikus alapok"), + ("Even Page Break", "Páros oldaltörés"), + ("Exactly", "Pontosan"), + ("Exclude Overlap", "Átfedés kizárása"), + ("Export", "Exportálás"), + ("Export Book to PDF…", "Könyv exportálása PDF-be…"), + ("Export EPUB", "EPUB exportálása"), + ("Export EPUB (Fixed Layout", "Rögzített elrendezésű EPUB exportálása"), + ("Export EPUB (Fixed Layout)", "Rögzített elrendezésű EPUB exportálása"), + ("Export EPUB (Fixed Layout)…", "Rögzített elrendezésű EPUB exportálása…"), + ("Export EPUB (Reflowable", "Újratördelhető EPUB exportálása"), + ("Export EPUB (Reflowable)", "Újratördelhető EPUB exportálása"), + ("Export EPUB (Reflowable)…", "Újratördelhető EPUB exportálása…"), + ("Export EPUB…", "EPUB exportálása…"), + ("Export HTML", "HTML exportálása"), + ("Export HTML…", "HTML exportálása…"), + ("Export IDML", "IDML exportálása"), + ("Export IDML…", "IDML exportálása…"), + ("Export PDF", "PDF exportálása"), + ("Export PDF (Interactive)", "Interaktív PDF exportálása"), + ("Export PDF (Interactive)…", "Interaktív PDF exportálása…"), + ("Export PDF…", "PDF exportálása…"), + ("Export Page as PNG", "Oldal exportálása PNG-ként"), + ("Export Page as PNG…", "Oldal exportálása PNG-ként…"), + ("Export PowerPoint…", "PowerPoint exportálása…"), + ("Export Selection as Snippet", "Kijelölés exportálása részletként"), + ("Export Tagging", "Exportcímkézés"), + ("Export Text", "Szöveg exportálása"), + ("Export Text…", "Szöveg exportálása…"), + ("Export XML", "XML exportálása"), + ("Export XML…", "XML exportálása…"), + ("Extend Text Selection", "Szövegkijelölés kiterjesztése"), + ("Extras", "Kiegészítők"), + ("Facing Pages", "Szemközti oldalak"), + ("Fade", "Elhalványulás"), + ("Farsi", "Perzsa"), + ("Fast", "Gyors"), + ("Fast Display", "Gyors megjelenítés"), + ("Favorites", "Kedvencek"), + ("Figure Space", "Számjegynyi szóköz"), + ("Figure Style:", "Számjegystílus:"), + ("File", "Fájl"), + ("File Handling", "Fájlkezelés"), + ("File/Export", "Fájl/Exportálás"), + ("Fill", "Kitöltés"), + ("Fill (X)", "Kitöltés (X)"), + ("Fill Frame Proportionally", "Keret arányos kitöltése"), + ("Fill proportionally", "Arányos kitöltés"), + ("Fill with Placeholder Text", "Kitöltés helykitöltő szöveggel"), + ("Find", "Keresés"), + ("Find All", "Összes keresése"), + ("Find Next", "Következő keresése"), + ("Find Object", "Objektum keresése"), + ("Find What", "Keresett szöveg"), + ("Find by Script Label", "Keresés parancsfájlcímke alapján"), + ("Find what:", "Keresett szöveg:"), + ("Find/Change", "Keresés és csere"), + ("Find/Change…", "Keresés és csere…"), + ("Find/Replace Font", "Betűtípus keresése és cseréje"), + ("Find/Replace Font…", "Betűtípus keresése és cseréje…"), + ("Finnish", "Finn"), + ("First", "Első"), + ("First Baseline", "Első alapvonal"), + ("First Line Indent:", "Első sor behúzása:"), + ("First Object Above", "Legfelső objektum"), + ("First Page", "Első oldal"), + ("First Spread", "Első oldalpár"), + ("Fit Content Proportionally", "Tartalom arányos illesztése"), + ("Fit Content to Frame", "Tartalom igazítása a kerethez"), + ("Fit Frame to Content", "Keret igazítása a tartalomhoz"), + ("Fit Guides to:", "Segédvonalak igazítása ehhez:"), + ("Fit Page in Window", "Oldal igazítása az ablakhoz"), + ("Fit Selection in Window", "Kijelölés igazítása az ablakhoz"), + ("Fit Spread in Window", "Oldalpár igazítása az ablakhoz"), + ("Fit Text", "Szöveg illesztése"), + ("Fit content to frame", "Tartalom igazítása a kerethez"), + ("Fit frame height to text", "Keretmagasság igazítása a szöveghez"), + ("Fit proportionally", "Arányos illesztés"), + ("Fitting", "Illesztés"), + ("Fitting:", "Illesztés:"), + ("Fixed", "Rögzített"), + ("Flattener Preview", "Lapítási előnézet"), + ("Flattener preset", "Lapítási készlet"), + ("Flex End", "Rugalmas elrendezés vége"), + ("Flex Layout", "Rugalmas elrendezés"), + ("Flex Layout Options", "Rugalmas elrendezés beállításai"), + ("Flex Start", "Rugalmas elrendezés eleje"), + ("Flip", "Tükrözés"), + ("Flip Horizontal", "Vízszintes tükrözés"), + ("Flip Vertical", "Függőleges tükrözés"), + ("Float Panel", "Panel lebegtetése"), + ("Float panel", "Panel lebegtetése"), + ("Flush Space", "Kitöltő szóköz"), + ("Font", "Betűtípus"), + ("Font Family:", "Betűcsalád:"), + ("Font Size:", "Betűméret:"), + ("Font Style:", "Betűváltozat:"), + ("Fonts in Document", "Betűtípusok a dokumentumban"), + ("Fonts in Document: {count} Missing: {missing}", "Betűtípusok a dokumentumban: {count} Hiányzik: {missing}"), + ("Footer Rows", "Láblécsorok"), + ("Footnote Formatting", "Lábjegyzet formázása"), + ("Footnote Options", "Lábjegyzet beállításai"), + ("Footnote Reference", "Lábjegyzethivatkozás"), + ("Footnote Reference Number in Text", "Lábjegyzethivatkozás száma a szövegben"), + ("Footnote Text", "Lábjegyzet szövege"), + ("Footnotes", "Lábjegyzetek"), + ("Forced Line Break", "Kényszerített sortörés"), + ("Form Field", "Űrlapmező"), + ("Form Fields", "Űrlapmezők"), + ("Format:", "Formátum:"), + ("Formatting Affects Text", "A formázás a szövegre vonatkozik"), + ("Formatting affects container (J)", "A formázás a tárolóra vonatkozik (J)"), + ("Formatting affects text (J)", "A formázás a szövegre vonatkozik (J)"), + ("Fractions", "Törtek"), + ("Frame Break", "Kerettörés"), + ("Frame Edges", "Keretek szélei"), + ("Frame Fitting", "Keretillesztés"), + ("Frame Fitting Options", "Keretillesztés beállításai"), + ("Frame Fitting Options…", "Keretillesztés beállításai…"), + ("Français", "Français"), + ("French", "Francia"), + ("French: Canadian", "Francia: kanadai"), + ("GREP", "GREP"), + ("GREP Style", "GREP-stílus"), + ("GREP Styles", "GREP-stílusok"), + ("Gap Color:", "Hézag színe:"), + ("General", "Általános"), + ("Generate Index", "Tárgymutató létrehozása"), + ("Generate Index…", "Tárgymutató létrehozása…"), + ("Generate Live Caption", "Élő képaláírás létrehozása"), + ("Generate QR Code", "QR-kód létrehozása"), + ("Generate QR Code…", "QR-kód létrehozása…"), + ("Generate Static Caption", "Statikus képaláírás létrehozása"), + ("Georgian", "Grúz"), + ("German: 2006 Reform", "Német: 2006-os reform"), + ("German: Swiss 2006 Reform", "Német: svájci, 2006-os reform"), + ("Get Story", "Szövegfolyam lekérése"), + ("Get Table", "Táblázat lekérése"), + ( + "Get help, share your layouts and shape what we build next.", + "Kérjen segítséget, mutassa meg kiadványait, és alakítsa velünk a következő fejlesztéseket.", + ), + ("Global Light", "Globális megvilágítás"), + ("Global Light…", "Globális megvilágítás…"), + ("Glyph Scaling:", "Karakterjel méretezése:"), + ("Glyphs", "Karakterjelek"), + ("Go To", "Ugrás"), + ("Go To First Page", "Ugrás az első oldalra"), + ("Go To Last Page", "Ugrás az utolsó oldalra"), + ("Go To Link", "Ugrás a csatoláshoz"), + ("Go To Next Page", "Ugrás a következő oldalra"), + ("Go To Page", "Ugrás oldalra"), + ("Go To Previous Page", "Ugrás az előző oldalra"), + ("Go To Source", "Ugrás a forráshoz"), + ("Go To URL", "Ugrás az URL-re"), + ("Go to Footnote Reference", "Ugrás a lábjegyzethivatkozáshoz"), + ("Go to Page", "Ugrás oldalra"), + ("Go to Page…", "Ugrás oldalra…"), + ("Gradient", "Színátmenet"), + ("Gradient Feather", "Színátmenetes lágyítás"), + ("Graphic", "Grafika"), + ("Graphic…", "Grafika…"), + ("Gray", "Szürke"), + ("Greek", "Görög"), + ("Gridify", "Rácsba rendezés"), + ("Grids", "Rácsok"), + ("Grids & Guides", "Rácsok és segédvonalak"), + ("Grids in Back", "Rácsok a háttérben"), + ("Group", "Csoportosítás"), + ("Group Transparency", "Csoport átlátszósága"), + ("Group with", "Csoportosítás ezzel"), + ("Group:", "Csoport:"), + ("Guide-Based", "Segédvonal-alapú"), + ("Guide-based", "Segédvonal-alapú"), + ("Guides", "Segédvonalak"), + ("Guides & Pasteboard", "Segédvonalak és munkaterület"), + ("Gutter", "Hasábköz"), + ("Gutter:", "Hasábköz:"), + ("H&J Violations", "Elválasztási és sorkizárási eltérések"), + ("H:", "M.:"), + ("Hair Space", "Hajszálköz"), + ("Header Rows", "Fejlécsorok"), + ("Hebrew", "Héber"), + ("Height", "Magasság"), + ("Height Only", "Csak magasság"), + ("Height and Width", "Magasság és szélesség"), + ("Height and width", "Magasság és szélesség"), + ("Height only", "Csak magasság"), + ("Height:", "Magasság:"), + ("Help", "Súgó"), + ("Hidden Characters", "Rejtett karakterek"), + ("Hide", "Elrejtés"), + ("Hide Bar", "Sáv elrejtése"), + ("Hide Guides", "Segédvonalak elrejtése"), + ("Hide Menu Item", "Menüpont elrejtése"), + ("Hide Others", "Többi elrejtése"), + ("Hide Rulers", "Vonalzók elrejtése"), + ("Hide/Lock Others", "Többi elrejtése/zárolása"), + ("High Contrast", "Erős kontraszt"), + ("High Quality", "Kiváló minőség"), + ("High Quality Display", "Kiváló minőségű megjelenítés"), + ("High Resolution", "Nagy felbontás"), + ("Highlight", "Kiemelés"), + ("Hindi", "Hindi"), + ("History", "Előzmények"), + ("Home", "Kezdőlap"), + ("Horizontal", "Vízszintes"), + ("Horizontal Gridline Every:", "Vízszintes rácsvonalak távolsága:"), + ("Horizontal Margins:", "Vízszintes margók:"), + ("Horizontal Offset", "Vízszintes eltolás"), + ("Horizontal Scale:", "Vízszintes méretezés:"), + ("Horizontal Spacing", "Vízszintes térköz"), + ("Horizontal:", "Vízszintes:"), + ("Hungarian", "Magyar"), + ("Hyperlink Options…", "Hiperhivatkozás beállításai…"), + ("Hyperlink from the selected text or frames", "Hiperhivatkozás a kijelölt szövegből vagy keretekből"), + ("Hyperlinks", "Hiperhivatkozások"), + ("Hyperlinks & Cross-References", "Hiperhivatkozások és kereszthivatkozások"), + ("Hyperlinks Cross-References", "Hiperhivatkozások és kereszthivatkozások"), + ("Hyphen Limit:", "Egymást követő elválasztások korlátja:"), + ("Hyphenate", "Elválasztás engedélyezése"), + ("Hyphenation", "Elválasztás"), + ("Hyphenation Exceptions", "Elválasztási kivételek"), + ("Hyphens and Dashes", "Kötőjelek és gondolatjelek"), + ("I, II, III, IV...", "I, II, III, IV..."), + ("IT", "IT"), + ("Image", "Kép"), + ("Import File", "Fájl importálása"), + ("Import Options", "Importálási beállítások"), + ("Import Options: Styles", "Importálási beállítások: stílusok"), + ("Import Warnings", "Importálási figyelmeztetések"), + ("Import XML", "XML importálása"), + ("Import XML…", "XML importálása…"), + ("In justified Arabic text", "Sorkizárt arab szövegben"), + ("Include Bleed", "Kifutóval együtt"), + ("Include Stroke Weight", "Körvonalvastagsággal együtt"), + ("Include document bleed", "Dokumentum kifutójának felvétele"), + ("Include when exporting", "Felvétel exportáláskor"), + ("Increase Point Size", "Betűméret növelése"), + ("Increment Every:", "Lépésköz:"), + ("Indent", "Behúzás"), + ("Indent to Here", "Behúzás eddig"), + ("Indents and Spacing", "Behúzások és térközök"), + ("Index", "Tárgymutató"), + ("Index Page References", "Tárgymutató-oldalhivatkozások"), + ("Info", "Információ"), + ("Ink Limit 300%", "300%-os festékterhelési korlát"), + ("Ink Manager", "Festékkezelő"), + ("Inner Glow", "Belső ragyogás"), + ("Inner Shadow", "Belső árnyék"), + ("Insert", "Beszúrás"), + ("Insert Anchored Object", "Rögzített objektum beszúrása"), + ("Insert Anchored Object…", "Rögzített objektum beszúrása…"), + ("Insert Break Character", "Töréskarakter beszúrása"), + ("Insert Column", "Oszlop beszúrása"), + ("Insert Column Left", "Oszlop beszúrása balra"), + ("Insert Column Right", "Oszlop beszúrása jobbra"), + ("Insert Column Rule", "Hasábelválasztó vonal beszúrása"), + ("Insert Cross-Reference", "Kereszthivatkozás beszúrása"), + ("Insert Cross-Reference…", "Kereszthivatkozás beszúrása…"), + ("Insert Endnote", "Végjegyzet beszúrása"), + ("Insert Field", "Mező beszúrása"), + ("Insert Footnote", "Lábjegyzet beszúrása"), + ("Insert Kashidas:", "Kasídák beszúrása:"), + ("Insert Math Expression", "Matematikai kifejezés beszúrása"), + ("Insert Math Expression…", "Matematikai kifejezés beszúrása…"), + ("Insert Page", "Oldal beszúrása"), + ("Insert Page After", "Oldal beszúrása utána"), + ("Insert Pages", "Oldalak beszúrása"), + ("Insert Pages…", "Oldalak beszúrása…"), + ("Insert Row", "Sor beszúrása"), + ("Insert Row Above", "Sor beszúrása fölé"), + ("Insert Row Below", "Sor beszúrása alá"), + ("Insert Special Character", "Speciális karakter beszúrása"), + ("Insert Table…", "Táblázat beszúrása…"), + ("Insert Variable", "Változó beszúrása"), + ("Insert White Space", "Szóközkarakter beszúrása"), + ("Insert field", "Mező beszúrása"), + ("Inset spacing", "Belső margók"), + ("Inside", "Belül"), + ("Inside:", "Belül:"), + ("Inspect Document", "Dokumentum vizsgálata"), + ("Intent:", "Leképezési mód:"), + ("Interactive", "Interaktív"), + ("Interactive for PDF", "Interaktív PDF"), + ("Interface", "Felület"), + ("Interface Color Theme", "Felület színtémája"), + ("Interface Language", "Felület nyelve"), + ("Internet Standard RGB (sRGB)", "Internetes szabványos RGB (sRGB)"), + ("Intersect", "Metszet"), + ("Invalid value", "Érvénytelen érték"), + ("Inverse Rounded Rectangle", "Befelé lekerekített téglalap"), + ("Invert", "Megfordítás"), + ("Inward", "Befelé"), + ("Isolate Blending", "Keverés elkülönítése"), + ("Italian", "Olasz"), + ("Italiano", "Italiano"), + ("Italic", "Dőlt"), + ("Japanese", "Japán"), + ("Join", "Összekapcsolás"), + ("Join Path", "Görbék összekapcsolása"), + ("Join our Discord", "Csatlakozás a Discordhoz"), + ("Join the ArtCraft Discord", "Csatlakozás az ArtCraft Discordhoz"), + ("Join the ArtCraft Discord…", "Csatlakozás az ArtCraft Discordhoz…"), + ("Join the ArtCraft community", "Csatlakozás az ArtCraft közösségéhez"), + ("Join the community", "Csatlakozás a közösséghez"), + ("Join:", "Vonalcsatlakozás:"), + ("Jump Object", "Objektum átugrása"), + ("Jump to Next Column", "Ugrás a következő hasábba"), + ("Justification", "Sorkizárás"), + ("Justify", "Sorkizárt"), + ("Justify Left", "Sorkizárt, utolsó sor balra"), + ("Justify all lines", "Minden sor sorkizárt"), + ("Justify with last line aligned center", "Sorkizárt, utolsó sor középre"), + ("Justify with last line aligned left", "Sorkizárt, utolsó sor balra"), + ("Justify with last line aligned right", "Sorkizárt, utolsó sor jobbra"), + ("Keep Violations", "Együtt tartási eltérések"), + ("Kenten", "Kenten"), + ("Kerning", "Alávágás"), + ("Kerning:", "Alávágás:"), + ("Keyboard Increments", "Billentyűzeti lépésközök"), + ("Keyboard Shortcuts", "Billentyűparancsok"), + ("Keyboard Shortcuts…", "Billentyűparancsok…"), + ("Kind:", "Fajta:"), + ("Knockout Group", "Kiütési csoport"), + ("Korean", "Koreai"), + ("Korean Line Breaks:", "Koreai sortörések:"), + ("Lab", "Lab"), + ("Landscape", "Fekvő"), + ("Language", "Nyelv"), + ("Language:", "Nyelv:"), + ("Last", "Utolsó"), + ("Last Object Below", "Legalsó objektum"), + ("Last Page", "Utolsó oldal"), + ("Last Spread", "Utolsó oldalpár"), + ("Layer Options…", "Réteg beállításai…"), + ("Layers", "Rétegek"), + ("Layout", "Elrendezés"), + ("Layout/Index", "Elrendezés/Tárgymutató"), + ("Layout/Pages", "Elrendezés/Oldalak"), + ("Leading", "Sortávolság"), + ("Leading:", "Sortávolság:"), + ("Left", "Balra"), + ("Left Indent:", "Bal oldali behúzás:"), + ("Left to Right", "Balról jobbra"), + ("Left:", "Balra:"), + ("Legacy Macintosh RGB", "Régi Macintosh RGB"), + ("Letter Spacing:", "Betűköz:"), + ("Library", "Könyvtár"), + ("Library Contents", "Könyvtár tartalma"), + ("Library Items", "Könyvtári elemek"), + ("Ligatures", "Ligatúrák"), + ("Light", "Világos"), + ("Limit", "Korlát"), + ("Line", "Vonal"), + ("Linear", "Lineáris"), + ("Lines:", "Sorok:"), + ("Link To:", "Hivatkozás célja:"), + ("Link images", "Képek csatolása"), + ("Linked Stories", "Csatolt szövegfolyamok"), + ("Links", "Csatolások"), + ("Liquid Layout", "Alkalmazkodó elrendezés"), + ("Liquid Page Rule", "Alkalmazkodó oldalszabály"), + ("Liquid Page Rule:", "Alkalmazkodó oldalszabály:"), + ("Liquid guides", "Alkalmazkodó segédvonalak"), + ("List Box", "Listamező"), + ("List Composite Fonts", "Összetett betűtípusok listázása"), + ("List Documents", "Dokumentumok listázása"), + ("List Styles", "Stílusok listázása"), + ("List Tools", "Eszközök listázása"), + ("Load DTD", "DTD betöltése"), + ("Load DTD…", "DTD betöltése…"), + ("Load Place Cursor", "Elhelyezőkurzor feltöltése"), + ("Load Profile…", "Profil betöltése…"), + ("Load Swatches", "Színtár betöltése"), + ("Load Swatches…", "Színtár betöltése…"), + ("Load…", "Betöltés…"), + ("Location:", "Hely:"), + ("Lock", "Zárolás"), + ("Lock Guides", "Segédvonalak zárolása"), + ("Lock Others", "Többi zárolása"), + ("Loop", "Ismétlés"), + ("Low Resolution", "Kis felbontás"), + ("Magenta", "Bíbor"), + ("Make Compound", "Összetett alakzat készítése"), + ("Make Compound Path", "Összetett görbe készítése"), + ("Make all settings the same", "Összes beállítás azonossá tétele"), + ("Map Styles to Tags", "Stílusok hozzárendelése címkékhez"), + ("Margins", "Margók"), + ("Margins and Columns", "Margók és hasábok"), + ("Margins and Columns…", "Margók és hasábok…"), + ("Margins:", "Margók:"), + ("Markers", "Jelölők"), + ("Marks and Bleeds", "Nyomdai jelek és kifutó"), + ("Math Expression SVG", "Matematikai kifejezés SVG-je"), + ("Maximum", "Maximum"), + ("Media", "Média"), + ("Media Options", "Médiabeállítások"), + ("Medium Dark", "Középsötét"), + ("Medium Light", "Középvilágos"), + ("Medium Resolution", "Közepes felbontás"), + ("Menu Customization", "Menük testreszabása"), + ("Menus", "Menük"), + ("Menus…", "Menük…"), + ("Merge Cells", "Cellák egyesítése"), + ("Merge Layers", "Rétegek egyesítése"), + ("Merge into Active Layer", "Egyesítés az aktív réteggel"), + ("Merge options", "Egyesítési beállítások"), + ("Metrics", "Metrikus"), + ("Min:", "Min.:"), + ("Minimum", "Minimum"), + ("Minimum Space Before First Footnote:", "Legkisebb térköz az első lábjegyzet előtt:"), + ("Minus Back", "Hátsó alakzat kivonása"), + ("Missing Glyphs", "Hiányzó karakterjelek"), + ("Misspelled word → Correction (one per line):", "Hibás szó → Javítás (soronként egy):"), + ("Miter", "Hegyes vonalcsatlakozás"), + ("Miter Limit:", "Saroknyúlás korlátja:"), + ("Mobile", "Mobil"), + ("Monitor RGB", "Monitor RGB-színtere"), + ("More ArtCraft apps", "További ArtCraft-alkalmazások"), + ("More Options", "További beállítások"), + ("Move", "Áthelyezés"), + ("Move Anchors", "Csomópontok áthelyezése"), + ("Move Caret", "Szövegkurzor áthelyezése"), + ("Move Column", "Oszlop áthelyezése"), + ("Move Gap", "Hézag áthelyezése"), + ("Move Guide", "Segédvonal áthelyezése"), + ("Move Layer", "Réteg áthelyezése"), + ("Move Pages", "Oldalak áthelyezése"), + ("Move Pages…", "Oldalak áthelyezése…"), + ("Move Panel", "Panel áthelyezése"), + ("Move Row", "Sor áthelyezése"), + ("Move Selection Here", "Kijelölés áthelyezése ide"), + ("Move to Color Group", "Áthelyezés színcsoportba"), + ("Move to Group", "Áthelyezés csoportba"), + ("Move to Layer", "Áthelyezés rétegre"), + ("Multiline", "Többsoros"), + ("Multiple Objects", "Több objektum"), + ("Multiple records", "Több rekord"), + ("Name", "Név"), + ("Name:", "Név:"), + ("Native", "Natív"), + ("Nested Line Styles", "Beágyazott sorstílusok"), + ("Nested Styles", "Beágyazott stílusok"), + ("Never (continuous)", "Soha (folyamatos)"), + ("New", "Új"), + ("New Article", "Új cikk"), + ("New Article from Selection", "Új cikk a kijelölésből"), + ("New Book", "Új könyv"), + ("New Bookmark", "Új könyvjelző"), + ("New Bookmark for This Page", "Új könyvjelző ehhez az oldalhoz"), + ("New Book…", "Új könyv…"), + ("New Cell Style…", "Új cellastílus…"), + ("New Character Style…", "Új karakterstílus…"), + ("New Color Group", "Új színcsoport"), + ("New Color Group with Swatch", "Új színcsoport színmintával"), + ("New Color Swatch…", "Új színminta…"), + ("New Condition", "Új feltétel"), + ("New Condition…", "Új feltétel…"), + ("New Cross-Reference", "Új kereszthivatkozás"), + ("New Document", "Új dokumentum"), + ("New Document…", "Új dokumentum…"), + ("New GREP Style", "Új GREP-stílus"), + ("New Guide", "Új segédvonal"), + ("New Hyperlink", "Új hiperhivatkozás"), + ("New Hyperlink Destination", "Új hivatkozási cél"), + ("New Hyperlink Destination…", "Új hivatkozási cél…"), + ("New Hyperlink…", "Új hiperhivatkozás…"), + ("New Layer", "Új réteg"), + ("New Layer…", "Új réteg…"), + ("New Library", "Új könyvtár"), + ("New Library…", "Új könyvtár…"), + ("New Line Style", "Új sorstílus"), + ("New Nested Style", "Új beágyazott stílus"), + ("New Note", "Új jegyzet"), + ("New Note at Cursor", "Új jegyzet a kurzornál"), + ("New Object Style…", "Új objektumstílus…"), + ("New Page Reference", "Új oldalhivatkozás"), + ("New Page Reference…", "Új oldalhivatkozás…"), + ("New Paragraph Style…", "Új bekezdésstílus…"), + ("New Parent", "Új mesteroldal"), + ("New Parent…", "Új mesteroldal…"), + ("New Stroke Style", "Új körvonalstílus"), + ("New Stroke Style…", "Új körvonalstílus…"), + ("New Swatch", "Új színminta"), + ("New Table Style…", "Új táblázatstílus…"), + ("New Tag", "Új címke"), + ("New Window", "Új ablak"), + ("New Workspace", "Új munkaterület"), + ("New Workspace…", "Új munkaterület…"), + ("New condition name", "Új feltétel neve"), + ("New group…", "Új csoport…"), + ("New tag", "Új címke"), + ("Next", "Következő"), + ("Next Cell", "Következő cella"), + ("Next Document", "Következő dokumentum"), + ("Next Object Above", "Következő felső objektum"), + ("Next Object Below", "Következő alsó objektum"), + ("Next Object in Group", "Következő objektum a csoportban"), + ("Next Page", "Következő oldal"), + ("Next Page Number", "Következő oldalszám"), + ("Next Spread", "Következő oldalpár"), + ("No Break", "Nem törhető"), + ("No Selection", "Nincs kijelölés"), + ("No Text Wrap", "Nincs körbefuttatás"), + ("No articles: exports follow page order.", "Nincs cikk: az exportálás az oldalak sorrendjét követi."), + ("No book open.", "Nincs megnyitott könyv."), + ("No bookmarks. They become the PDF outline.", "Nincsenek könyvjelzők. Ezekből készül a PDF navigációs vázlata."), + ("No changes.", "Nincs módosítás."), + ( + "No conditions. Create one, then select text and click it to apply.", + "Nincs feltétel. Hozzon létre egyet, majd jelöljön ki szöveget, és kattintással alkalmazza rá.", + ), + ("No conflicts", "Nincs ütközés"), + ("No data source", "Nincs adatforrás"), + ("No document open", "Nincs megnyitott dokumentum"), + ("No document.", "Nincs dokumentum."), + ("No errors", "Nincs hiba"), + ( + "No hyperlinks. Select text or a frame, type a URL and click New.", + "Nincs hiperhivatkozás. Jelöljön ki szöveget vagy keretet, írjon be egy URL-t, és kattintson az Új gombra.", + ), + ("No library open.", "Nincs megnyitott könyvtár."), + ("No notes in this document.", "Nincs jegyzet ebben a dokumentumban."), + ("No placed graphics.", "Nincs elhelyezett grafika."), + ("No scripts. Add one below.", "Nincs parancsfájl. Alább hozzáadhat egyet."), + ("No spot inks", "Nincs direktszín"), + ("No text anchors in this document.", "Nincs szöveghorgony ebben a dokumentumban."), + ("No text selected", "Nincs kijelölt szöveg"), + ("Nonbreaking Hyphen", "Nem törhető kötőjel"), + ("Nonbreaking Space", "Nem törhető szóköz"), + ("None", "Nincs"), + ("None (keep transparency)", "Nincs (átlátszóság megőrzése)"), + ("Nonprinting", "Nem nyomtatandó"), + ("Normal", "Normál"), + ("Normal (W)", "Normál (W)"), + ("Norwegian: Bokmål", "Norvég: bokmål"), + ("Not a button", "Nem gomb"), + ("Not a form field", "Nem űrlapmező"), + ("Note text", "Jegyzet szövege"), + ("Notes", "Jegyzetek"), + ("Number of Pages", "Oldalak száma"), + ("Number of Pages:", "Oldalak száma:"), + ("Number of Sides:", "Sokszög oldalszáma:"), + ("Number of columns", "Oszlopok száma"), + ("Number:", "Darabszám:"), + ("Numbered List", "Számozott lista"), + ("Numbering", "Számozás"), + ("Numbering & Section Options", "Számozás és szakaszbeállítások"), + ("Numbering & Section Options…", "Számozás és szakaszbeállítások…"), + ("Numbering and Formatting", "Számozás és formázás"), + ("OK", "OK"), + ("OK changes all: text and paragraph/character styles.", "Az OK mindenhol cserél: a szövegben, valamint a bekezdés- és karakterstílusokban is."), + ("OVERSET", "TÚLCSORDULT SZÖVEG"), + ("Object", "Objektum"), + ("Object & Layout", "Objektum és elrendezés"), + ("Object Constraints:", "Objektum méret- és helyzetkötései:"), + ("Object Export Options", "Objektum exportálási beállításai"), + ("Object Export Options…", "Objektum exportálási beállításai…"), + ("Object Layer Options", "Objektum rétegbeállításai"), + ("Object Layer Options…", "Objektum rétegbeállításai…"), + ("Object Layers", "Objektumrétegek"), + ("Object Liquid Settings", "Objektum alkalmazkodási beállításai"), + ("Object States", "Objektumállapotok"), + ("Object Styles", "Objektumstílusok"), + ("Object is pinned relative to the page edge. Click to release.", "Az objektum az oldalszélhez van rögzítve. Kattintson a feloldáshoz."), + ("Object-Based", "Objektumalapú"), + ("Object-based", "Objektumalapú"), + ("Object/Anchored Object", "Objektum/Rögzített objektum"), + ("Object/Arrange", "Objektum/Sorrend"), + ("Object/Captions", "Objektum/Képaláírások"), + ("Object/Clipping Path", "Objektum/Vágógörbe"), + ("Object/Content", "Objektum/Tartalom"), + ("Object/Convert Shape", "Objektum/Alakzat átalakítása"), + ("Object/Effects", "Objektum/Hatások"), + ("Object/Fitting", "Objektum/Illesztés"), + ("Object/Pathfinder", "Objektum/Görbeműveletek"), + ("Object/Paths", "Objektum/Görbék"), + ("Object/Select", "Objektum/Kijelölés"), + ("Object/Transform", "Objektum/Átalakítás"), + ("Object/Transform Again", "Objektum/Átalakítás ismétlése"), + ("Object:", "Objektum:"), + ("Odd Page Break", "Páratlan oldaltörés"), + ("Off", "Ki"), + ("Offset", "Eltolás"), + ("Offset:", "Eltolás:"), + ("Offsets", "Eltolások"), + ("On", "Be"), + ("On Release", "Felengedéskor"), + ("On Screen:", "Képernyőn:"), + ("One record", "Egy rekord"), + ("Opacity", "Fedettség"), + ("Open", "Megnyitás"), + ("Open Book", "Könyv megnyitása"), + ("Open Bytes", "Megnyitás bájtokból"), + ("Open IDML", "IDML megnyitása"), + ("Open Library", "Könyvtár megnyitása"), + ("Open Path", "Görbe megnyitása"), + ("Open source. No telemetry.", "Nyílt forráskód. Nincs telemetria."), + ("OpenType", "OpenType"), + ("OpenType All Small Caps", "OpenType kiskapitális minden betűre"), + ("OpenType Denominator", "OpenType nevező"), + ("OpenType Features", "OpenType-funkciók"), + ("OpenType Numerator", "OpenType számláló"), + ("OpenType Subscript", "OpenType alsó index"), + ("OpenType Superscript", "OpenType felső index"), + ("Open…", "Megnyitás…"), + ("Optical", "Optikai"), + ("Optical Margin Alignment", "Optikai margóigazítás"), + ("Options", "Beállítások"), + ("Options for Black on RGB and Grayscale Devices", "Fekete beállításai RGB- és szürkeárnyalatos eszközökön"), + ("Ordinal", "Sorszámjel"), + ("Orientation", "Tájolás"), + ("Orientation:", "Tájolás:"), + ("Orthogonal Line", "Merőleges vonal"), + ("Other", "Egyéb"), + ("Outer Glow", "Külső ragyogás"), + ("Output", "Kimenet"), + ("Output All Blacks Accurately", "Minden fekete pontos kimenete"), + ("Output All Blacks as Rich Black", "Minden fekete kimenete dús feketeként"), + ("Outside", "Kívül"), + ("Outside:", "Kívül:"), + ("Overprint Fill", "Kitöltés felülnyomása"), + ("Overprint Gap", "Hézag felülnyomása"), + ("Overprint Preview", "Felülnyomási előnézet"), + ("Overprint Stroke", "Körvonal felülnyomása"), + ("Overprint [Black] Swatch at 100%", "A [Fekete] színminta felülnyomása 100%-nál"), + ("Override All Parent Page Items", "Mesteroldal összes elemének helyi felülírása"), + ("PDF/A-2b", "PDF/A-2b"), + ("PDF/X-4", "PDF/X-4"), + ("Package", "Csomagolás"), + ("Package…", "Csomagolás…"), + ("Page", "Oldal"), + ("Page (1–{count}):", "Oldal (1–{count}):"), + ("Page Break", "Oldaltörés"), + ("Page Information", "Oldalinformációk"), + ("Page Numbering", "Oldalszámozás"), + ("Page Options", "Oldalbeállítások"), + ("Page Range", "Oldaltartomány"), + ("Page Range:", "Oldaltartomány:"), + ("Page Size", "Oldalméret"), + ("Page Transition", "Oldaláttűnés"), + ("Page Transitions", "Oldaláttűnések"), + ( + "Page layout for print and screen — fast, open and scriptable.", + "Kiadványszerkesztés nyomtatáshoz és képernyőre – gyors, nyílt és parancsfájlokkal vezérelhető.", + ), + ("Page · PPI", "Oldal · PPI"), + ("Page:", "Oldal:"), + ("Pages", "Oldalak"), + ("Pages:", "Oldalak:"), + ("Panels", "Panelek"), + ("Paragraph", "Bekezdés"), + ("Paragraph Composer", "Bekezdéstördelő"), + ("Paragraph Formatting", "Bekezdésformázás"), + ("Paragraph Return", "Bekezdésjel"), + ("Paragraph Rules", "Bekezdésvonalak"), + ("Paragraph Rules…", "Bekezdésvonalak…"), + ("Paragraph Style Options", "Bekezdésstílus beállításai"), + ("Paragraph Style Options…", "Bekezdésstílus beállításai…"), + ("Paragraph Style:", "Bekezdésstílus:"), + ("Paragraph Styles", "Bekezdésstílusok"), + ("Paragraph Styles panel", "Bekezdésstílusok panel"), + ("Paragraph Symbol", "Bekezdés szimbóluma"), + ("Paragraph alignment", "Bekezdés igazítása"), + ("Parent Pages", "Mesteroldalak"), + ("Part of the ArtCraft suite of open creative tools.", "Az ArtCraft nyílt kreatív eszközcsalád része."), + ("Pass csv, rows, or bytes from the control channel.", "Adjon át csv, rows vagy bytes adatot a vezérlőcsatornán."), + ("Paste", "Beillesztés"), + ("Paste Into", "Beillesztés belülre"), + ("Paste in Place", "Beillesztés az eredeti helyre"), + ("Paste without Formatting", "Beillesztés formázás nélkül"), + ("Pasteboard Options", "Munkaterület beállításai"), + ("Path", "Görbe"), + ("Pathfinder", "Görbeműveletek"), + ("Pathfinder:", "Görbeműveletek:"), + ("Paths", "Görbék"), + ("Paths:", "Görbék:"), + ("Perceptual", "Érzékelési"), + ("Picas", "Pica"), + ("Pick a file in the folder holding the moved graphics", "Válasszon egy fájlt az áthelyezett grafikákat tartalmazó mappában"), + ("Pin Bar Position", "Sáv helyzetének rögzítése"), + ("Pin bottom", "Rögzítés alul"), + ("Pin left", "Rögzítés balra"), + ("Pin right", "Rögzítés jobbra"), + ("Pin top", "Rögzítés felül"), + ("Pin:", "Rögzítés:"), + ("Place", "Elhelyezés"), + ("Place Caret", "Szövegkurzor elhelyezése"), + ("Place Item", "Elem elhelyezése"), + ("Place Loaded Graphic", "Betöltött grafika elhelyezése"), + ("Place PDF", "PDF elhelyezése"), + ("Place Snippet", "Részlet elhelyezése"), + ("Place and Link", "Elhelyezés és csatolás"), + ("Place the insertion point in a table, or create one.", "Helyezze a szövegkurzort egy táblázatba, vagy hozzon létre táblázatot."), + ("Place with Import Options", "Elhelyezés importálási beállításokkal"), + ("Place with Import Options…", "Elhelyezés importálási beállításokkal…"), + ("Place…", "Elhelyezés…"), + ("Plain", "Egyszerű"), + ("Plain Text", "Egyszerű szöveg"), + ("Play on Page Load", "Lejátszás az oldal betöltésekor"), + ("Plays in EPUB export; print and PDF show the poster.", "EPUB-exportban lejátszható; nyomtatásban és PDF-ben a poszterkép látható."), + ("Polish", "Lengyel"), + ("Polygon", "Sokszög"), + ("Polygon Settings", "Sokszög beállításai"), + ("Polygon Settings…", "Sokszög beállításai…"), + ("Portrait", "Álló"), + ("Portuguese", "Portugál"), + ("Portuguese: Brazilian", "Portugál: brazíliai"), + ("Português (Brasil)", "Português (Brasil)"), + ("Position", "Pozíció"), + ("Position:", "Pozíció:"), + ("Poster", "Poszter"), + ("Preferences", "Beállítások"), + ("Preferences…", "Beállítások…"), + ("Prefix:", "Előtag:"), + ("Preflight", "Nyomdai előellenőrzés"), + ("Preflight Document", "Dokumentum nyomdai előellenőrzése"), + ("Presentation", "Bemutató"), + ("Preset", "Készlet"), + ("Preset:", "Készlet:"), + ("Preview", "Előnézet"), + ("Preview (W)", "Előnézet (W)"), + ("Preview Record", "Rekord előnézete"), + ("Previous", "Előző"), + ("Previous Cell", "Előző cella"), + ("Previous Document", "Előző dokumentum"), + ("Previous Object in Group", "Előző objektum a csoportban"), + ("Previous Page", "Előző oldal"), + ("Previous Page Number", "Előző oldalszám"), + ("Previous Spread", "Előző oldalpár"), + ("Primary Text Frame", "Elsődleges szövegkeret"), + ("Print", "Nyomtatás"), + ("Print Booklet", "Füzet nyomtatása"), + ("Print Booklet…", "Füzet nyomtatása…"), + ("Printer's Marks", "Nyomdai jelek"), + ("Printer:", "Nyomtató:"), + ("Printers", "Nyomtatók"), + ("Printing / Exporting:", "Nyomtatás / exportálás:"), + ("Printing and Proofing", "Nyomtatás és próbanyomat"), + ("Print…", "Nyomtatás…"), + ("Projecting", "Túlnyúló"), + ("Proof Colors", "Próbaszínek"), + ("Proof Setup", "Próbanyomat beállításai"), + ("Properties", "Tulajdonságok"), + ("Properties Panel", "Tulajdonságok panel"), + ("Proportional Lining", "Arányos verzálszámok"), + ("Proportional Oldstyle", "Arányos ugráló számok"), + ("Punctuation Space", "Írásjelköz"), + ("Push", "Eltolás"), + ("Quarter Space", "Negyedkvirt szóköz"), + ("Quick Actions", "Gyorsműveletek"), + ("Quick Apply", "Gyors alkalmazás"), + ("Quick Apply…", "Gyors alkalmazás…"), + ("Quit DesignCraft", "Kilépés a DesignCraftból"), + ("Quotation Marks", "Idézőjelek"), + ("RGB", "RGB"), + ("RGB:", "RGB:"), + ("Radial", "Sugárirányú"), + ("Radio Button", "Választógomb"), + ("Range", "Tartomány"), + ("Re-center", "Újra középre"), + ("Recently Used", "Legutóbb használt"), + ("Record", "Rekord"), + ("Records per page", "Rekordok oldalanként"), + ("Recover Documents", "Dokumentumok helyreállítása"), + ("Recovery Data", "Helyreállítási adatok"), + ("Rectangle", "Téglalap"), + ("Redo", "Újra"), + ("Registered Trademark Symbol", "Bejegyzett védjegy jele"), + ("Reject", "Elutasítás"), + ("Reject All", "Összes elutasítása"), + ("Reject All Changes", "Összes módosítás elutasítása"), + ("Reject Change", "Módosítás elutasítása"), + ("Relative Colorimetric", "Relatív kolorimetrikus"), + ("Relative To:", "Viszonyítási alap:"), + ("Release", "Feloldás"), + ("Release Compound Path", "Összetett görbe feloldása"), + ("Release State to Objects", "Állapot feloldása objektumokra"), + ("Release to Objects", "Feloldás objektumokra"), + ("Relink", "Újracsatolás"), + ("Relink to Folder", "Újracsatolás mappához"), + ("Relink to Folder…", "Újracsatolás mappához…"), + ("Relink…", "Újracsatolás…"), + ("Remove", "Eltávolítás"), + ("Remove All Local Overrides", "Összes helyi felülírás törlése"), + ("Remove Conditions from Selection", "Feltételek eltávolítása a kijelölésből"), + ("Remove Data Source", "Adatforrás eltávolítása"), + ("Remove Document", "Dokumentum eltávolítása"), + ("Remove Field", "Mező eltávolítása"), + ("Remove Flex Layout", "Rugalmas elrendezés eltávolítása"), + ("Remove Frame from Thread", "Keret kivétele a láncból"), + ("Remove Hyphenation Exception", "Elválasztási kivétel eltávolítása"), + ("Remove Styles and Formatting from Text and Tables", "Stílusok és formázás eltávolítása a szövegből és a táblázatokból"), + ("Remove from Article", "Eltávolítás a cikkből"), + ("Rename", "Átnevezés"), + ("Rename Bookmark", "Könyvjelző átnevezése"), + ("Render", "Renderelés"), + ("Reorder in Flex Layout", "Átrendezés a rugalmas elrendezésben"), + ("Repeat", "Ismétlés"), + ("Repeat Header Rows", "Fejlécsorok ismétlése"), + ("Replace With", "Csere erre"), + ("Replace with:", "Csere erre:"), + ("Report an Issue", "Hiba bejelentése"), + ("Report an Issue…", "Hiba bejelentése…"), + ("Required", "Kötelező"), + ("Reset", "Visszaállítás"), + ("Reset All to Defaults", "Összes alapérték visszaállítása"), + ("Reset Bar Position", "Sáv pozíciójának visszaállítása"), + ("Reset Shortcuts", "Gyorsbillentyűk visszaállítása"), + ("Reset Workspace", "Munkaterület visszaállítása"), + ("Resize", "Átméretezés"), + ("Resize Page", "Oldal átméretezése"), + ("Resize height", "Magasság módosítása"), + ("Resize panel window", "Panelablak átméretezése"), + ("Resize panels", "Panelek átméretezése"), + ("Resize width", "Szélesség módosítása"), + ("Resize with Page:", "Átméretezés az oldallal:"), + ("Restart Numbering Every:", "Számozás újrakezdése:"), + ("Reverse", "Megfordítás"), + ("Reverse Column", "Fordított oszlopsorrend"), + ("Reverse Path", "Görbeirány megfordítása"), + ("Reverse Row", "Fordított sorsorrend"), + ("Reverse the gradient", "Színátmenet megfordítása"), + ("Revert", "Visszaállítás a mentett változatra"), + ("Right", "Jobbra"), + ("Right Indent Tab", "Jobbra behúzó tabulátor"), + ("Right Indent:", "Jobb behúzás:"), + ("Right to Left", "Jobbról balra"), + ("Right-click: stroke, groups", "Jobb kattintás: körvonal, csoportok"), + ("Right:", "Jobbra:"), + ("Rotate", "Forgatás"), + ("Rotate 90° Clockwise", "Forgatás 90°-kal jobbra"), + ("Rotate 90° Counterclockwise", "Forgatás 90°-kal balra"), + ("Rotate Spread", "Oldalpár elforgatása"), + ("Rotation", "Elforgatás"), + ("Rotation Angle", "Elforgatás szöge"), + ("Round", "Kerek"), + ("Rounded Rectangle", "Lekerekített téglalap"), + ("Row", "Sor"), + ("Row Height", "Sormagasság"), + ("Row spacing", "Sorköz"), + ("Row ↑", "Sor ↑"), + ("Row ↓", "Sor ↓"), + ("Rows", "Sorok"), + ("Rows first", "Először a sorok"), + ("Rows:", "Sorok:"), + ("Ruby", "Ruby"), + ("Ruby:", "Ruby:"), + ("Ruby…", "Ruby…"), + ("Rule Above", "Bekezdés feletti vonal"), + ("Rule Below", "Vonal alatta"), + ("Rule On", "Vonal bekapcsolása"), + ("Ruler Units", "Vonalzó mértékegységei"), + ("Rulers", "Vonalzók"), + ("Rulers & Grids", "Vonalzók és rácsok"), + ("Run", "Futtatás"), + ("Run Script", "Szkript futtatása"), + ("Russian", "Orosz"), + ("Sample Color (Eyedropper)", "Színmintavétel (pipetta)"), + ("Sample Document", "Mintadokumentum"), + ("Satin", "Szatén"), + ("Saturation", "Telítettség"), + ("Save", "Mentés"), + ("Save As", "Mentés másként"), + ("Save As…", "Mentés másként…"), + ("Save Recovery Data", "Helyreállítási adatok mentése"), + ("Save Swatches for Exchange", "Színminták mentése cseréhez"), + ("Save Swatches for Exchange…", "Színtár mentése cseréhez…"), + ("Save a Copy", "Másolat mentése"), + ("Save a Copy…", "Másolat mentése…"), + ("Save recovery data every:", "Helyreállítási adatok mentési időköze:"), + ("Saves which bars show and where the panels are.", "Menti a látható sávokat és a panelek helyét."), + ("Scale", "Méretezés"), + ("Scale X Percentage", "Vízszintes méretezés százalékban"), + ("Scale Y Percentage", "Függőleges méretezés százalékban"), + ("Scale for Descenders", "Méretezés a lenyúló szárakhoz"), + ("Screen Mode", "Képernyőmód"), + ("Screen and Email", "Képernyő és e-mail"), + ("Script Label", "Szkriptcímke"), + ("Scripts", "Szkriptek"), + ("Search", "Keresés"), + ("Search commands", "Parancsok keresése"), + ("Search fonts", "Betűtípusok keresése"), + ("Search styles and commands…", "Stílusok és parancsok keresése…"), + ("Search:", "Keresés:"), + ("Search: character or U+code", "Keresés: karakter vagy U+kód"), + ("Section", "Szakasz"), + ("Section Marker", "Szakaszjelölő"), + ("Section Symbol", "Paragrafusjel"), + ("Select", "Kijelölés"), + ("Select All", "Az összes kijelölése"), + ("Select All on Layer", "Összes kijelölése a rétegen"), + ("Select Cells", "Cellák kijelölése"), + ("Select Column", "Oszlop kijelölése"), + ("Select Data Source…", "Adatforrás kiválasztása…"), + ("Select Frame", "Keret kijelölése"), + ("Select Page", "Oldal kijelölése"), + ("Select Paragraph", "Bekezdés kijelölése"), + ("Select Row", "Sor kijelölése"), + ("Select Sentence", "Mondat kijelölése"), + ("Select Story", "Szövegfolyam kijelölése"), + ("Select Table", "Táblázat kijelölése"), + ("Select Text", "Szöveg kijelölése"), + ("Select Text Unit", "Szövegegység kijelölése"), + ("Select Tool", "Eszköz kiválasztása"), + ("Select Word", "Szó kijelölése"), + ( + "Select a placed video or sound (File › Place a .mp4, .mov, .mp3, .wav …).", + "Jelölj ki egy elhelyezett videót vagy hangot (Fájl › Elhelyezés: .mp4, .mov, .mp3, .wav …).", + ), + ("Select an object or text to color its fill or stroke.", "Jelölj ki egy objektumot vagy szöveget a kitöltés vagy a körvonal színezéséhez."), + ("Select an object to color its fill or stroke.", "Jelölj ki egy objektumot a kitöltés vagy a körvonal színezéséhez."), + ("Select an object to give its fill a gradient.", "Jelölj ki egy objektumot a színátmenetes kitöltéshez."), + ("Select an object to make it a button.", "Jelölj ki egy objektumot a gombbá alakításhoz."), + ("Select an object.", "Jelölj ki egy objektumot."), + ("Select text or a text frame.", "Jelölj ki szöveget vagy szövegkeretet."), + ("Select text to apply a condition", "Jelölj ki szöveget a feltétel alkalmazásához"), + ( + "Select two or more objects (or a group) to make a multi-state object.", + "Jelölj ki legalább két objektumot (vagy egy csoportot) a többállapotú objektum létrehozásához.", + ), + ("Selection", "Kijelölés"), + ("Selection Attributes", "Kijelölés tulajdonságai"), + ("Send Backward", "Eggyel hátrébb"), + ("Send to Back", "Leghátra"), + ("Sentence case", "Mondatkezdő nagybetű"), + ("Separated by commas", "Vesszővel elválasztva"), + ("Separations Preview", "Színkivonatok előnézete"), + ("Separator:", "Elválasztó:"), + ("Serialize", "Szerializálás"), + ("Set", "Beállítás"), + ("Set Active Layer", "Aktív réteg beállítása"), + ("Set Anchor", "Csomópont beállítása"), + ("Set Composite Font", "Összetett betűtípus beállítása"), + ("Set Cross-Reference Format", "Kereszthivatkozás formátumának beállítása"), + ("Set Footnote Text", "Lábjegyzetszöveg beállítása"), + ("Set OpenType Features", "OpenType-funkciók beállítása"), + ("Set Shortcut", "Gyorsbillentyű beállítása"), + ("Set Story Text", "Szövegfolyam szövegének beállítása"), + ("Set Visibility / Lock", "Láthatóság / zárolás beállítása"), + ("Set over the selected text; empty removes it.", "A kijelölt szöveg fölé kerül; üresen hagyva eltávolítható."), + ("Shading", "Háttérszínezés"), + ("Share", "Megosztás"), + ("Shear", "Döntés"), + ("Shear X Angle", "Vízszintes döntés szöge"), + ("Show", "Megjelenítés"), + ("Show / hide text with this condition", "Az ezzel a feltétellel jelölt szöveg megjelenítése / elrejtése"), + ("Show All Layers", "Összes réteg megjelenítése"), + ("Show All Menu Items", "Összes menüpont megjelenítése"), + ("Show All on Spread", "Összes megjelenítése az oldalpáron"), + ("Show Baseline Grid", "Alapvonalrács megjelenítése"), + ("Show Controls", "Vezérlők megjelenítése"), + ("Show Document Grid", "Dokumentumrács megjelenítése"), + ("Show Favorites Only", "Csak a kedvencek megjelenítése"), + ("Show Flex Controls", "Rugalmas elrendezés vezérlőinek megjelenítése"), + ("Show Font Names in English", "Betűtípusnevek megjelenítése angolul"), + ("Show Guides", "Segédvonalak megjelenítése"), + ("Show Hidden Characters", "Rejtett karakterek megjelenítése"), + ("Show Note Anchors in Layout View", "Jegyzethorgonyok megjelenítése elrendezési nézetben"), + ("Show Panel", "Panel megjelenítése"), + ("Show Parent Page Overlay", "Mesteroldal fedvényének megjelenítése"), + ("Show Prefix/Suffix in:", "Előtag/utótag megjelenítése itt:"), + ("Show Rulers", "Vonalzók megjelenítése"), + ("Show State", "Állapot megjelenítése"), + ("Show Tag Markers", "Címkejelölők megjelenítése"), + ("Show in the text", "Megjelenítés a szövegben"), + ("Show the Typography panel", "Tipográfia panel megjelenítése"), + ("Show/Hide Baseline Grid", "Alapvonalrács megjelenítése/elrejtése"), + ("Show/Hide Frame Edges", "Keretélek megjelenítése/elrejtése"), + ("Show/Hide Guides", "Segédvonalak megjelenítése/elrejtése"), + ("Show/Hide Hidden Characters", "Rejtett karakterek megjelenítése/elrejtése"), + ("Show/Hide Panels", "Panelek megjelenítése/elrejtése"), + ("Show/Hide Panels Except Tools", "Panelek megjelenítése/elrejtése az eszközök kivételével"), + ("Show/Hide Rulers", "Vonalzók megjelenítése/elrejtése"), + ("Show/Hide Tagged Frames", "Címkézett keretek megjelenítése/elrejtése"), + ("Show/Hide Text Threads", "Szövegláncok megjelenítése/elrejtése"), + ("Signature", "Aláírás"), + ("Signature Field", "Aláírásmező"), + ("Simple", "Egyszerű"), + ("Simple - Wide", "Egyszerű – széles"), + ("Simulate Paper Color", "Papírszín szimulálása"), + ("Single Left Quotation Mark", "Bal oldali szimpla idézőjel"), + ("Single Right Quotation Mark", "Jobb oldali szimpla idézőjel"), + ("Single record", "Egy rekord"), + ("Single-line", "Egysoros"), + ("Single-line Composer", "Egysoros tördelő"), + ("Sixth Space", "Hatodkvirt szóköz"), + ("Size", "Méret"), + ("Size:", "Méret:"), + ("Skew:", "Dőlés:"), + ("Slashed Zero", "Áthúzott nulla"), + ("Slug", "Infóterület"), + ("Slug:", "Infóterület:"), + ("Small Caps", "Kiskapitális"), + ("Smart Dimensions", "Intelligens méretek"), + ("Smart Guide Options", "Intelligens segédvonalak beállításai"), + ("Smart Guides", "Intelligens segédvonalak"), + ("Smart Spacing", "Intelligens térközök"), + ("Smart Text Reflow", "Intelligens szövegáttördelés"), + ("Smooth", "Simítás"), + ("Snap to Document Grid", "Igazítás a dokumentumrácshoz"), + ("Snap to Guides", "Igazítás a segédvonalakhoz"), + ("Snap to Zone", "Zónához igazítás"), + ("Solid", "Folytonos"), + ("Sort", "Rendezés"), + ("Space After", "Térköz utána"), + ("Space After:", "Térköz utána:"), + ("Space Around", "Térköz az elemek körül"), + ("Space Before", "Térköz előtte"), + ("Space Before:", "Térköz előtte:"), + ("Space Between", "Térköz az elemek között"), + ("Space Between Footnotes:", "Lábjegyzetek közötti térköz:"), + ("Space Evenly", "Egyenletes térköz"), + ("Spacing", "Térköz"), + ("Spacing Options", "Térköz beállításai"), + ("Span Footnotes Across Columns", "Lábjegyzetek több hasábon át"), + ("Spanish", "Spanyol"), + ("Speed", "Sebesség"), + ("Spelling", "Helyesírás"), + ("Split", "Felosztás"), + ("Split Cell Horizontally", "Cella vízszintes felosztása"), + ("Split Cell Vertically", "Cella függőleges felosztása"), + ("Split Path", "Görbe felosztása"), + ("Split Window", "Ablak felosztása"), + ("Spread", "Oldalpár"), + ("Spreads", "Oldalpárok"), + ("Spreads of up to 10 pages", "Legfeljebb 10 oldalas oldalcsoportok"), + ("Square", "Négyzet"), + ("Square - Solid", "Négyzet – tömör"), + ("Standard", "Szabványos"), + ("Standard:", "Szabvány:"), + ("Star Inset:", "Csillag behúzása:"), + ("Start Page #", "Kezdőoldalszám"), + ("Start Page #:", "Kezdő oldalszám:"), + ("Start a new document", "Új dokumentum létrehozása"), + ("Start at:", "Kezdőérték:"), + ("Start/End:", "Kezdet/vég:"), + ("Start:", "Kezdet:"), + ("State Options", "Állapot beállításai"), + ("Status", "Állapot"), + ("Step and Repeat", "Ismételt másolás"), + ("Step and Repeat…", "Ismételt másolás…"), + ("Stop Color:", "Színpont színe:"), + ("Stop Preview", "Előnézet leállítása"), + ("Story", "Szövegfolyam"), + ("Story Column Direction", "Szövegfolyam hasábiránya"), + ("Story Direction", "Szövegfolyam iránya"), + ("Story Editor", "Szövegfolyam-szerkesztő"), + ("Story Editor Display", "Szövegfolyam-szerkesztő megjelenítése"), + ("Straight Double Quotation Marks", "Egyenes dupla idézőjel"), + ("Straight Hash", "Egyenes rovátkázás"), + ("Straight Single Quotation Mark (Apostrophe)", "Egyenes szimpla idézőjel (aposztróf)"), + ("Strikethrough", "Áthúzás"), + ("Strikethrough On", "Áthúzás bekapcsolása"), + ("Strikethrough Options", "Áthúzás beállításai"), + ("Stripes", "Csíkok"), + ("Stroke", "Körvonal"), + ("Stroke (X)", "Körvonal (X)"), + ("Stroke Behind Fill", "Körvonal a kitöltés mögött"), + ("Stroke Styles", "Körvonalstílusok"), + ("Structure", "Struktúra"), + ("Style", "Stílus"), + ("Style Mapping", "Stílusok megfeleltetése"), + ("Style Name Conflicts", "Stílusnevek ütközései"), + ("Style Name:", "Stílus neve:"), + ("Style Settings:", "Stílus beállításai:"), + ("Style Source", "Stílusforrás"), + ("Style source", "Stílusforrás"), + ("Style:", "Stílus:"), + ("Styles", "Stílusok"), + ("Styles panel", "Stílusok panel"), + ("Stylistic Set", "Stílusváltozat-készlet"), + ("Stylistic Sets", "Stílusváltozat-készletek"), + ("Subdivisions:", "Felosztások:"), + ("Subscript", "Alsó index"), + ("Subscript:", "Alsó index:"), + ("Substituted Fonts", "Helyettesített betűtípusok"), + ("Subtract", "Kivonás"), + ("Suffix:", "Utótag:"), + ("Superscript", "Felső index"), + ("Superscript:", "Felső index:"), + ("Swap Fill and Stroke", "Kitöltés és körvonal felcserélése"), + ("Swap Fill and Stroke (Shift+X)", "Kitöltés és körvonal felcserélése (Shift+X)"), + ("Swap Text Fill and Stroke", "Szöveg kitöltésének és körvonalának felcserélése"), + ("Swash", "Díszes betűváltozat"), + ("Swatches", "Színtár"), + ("Swedish", "Svéd"), + ("Symbols", "Szimbólumok"), + ("Symmetrical", "Szimmetrikus"), + ("Synchronize", "Szinkronizálás"), + ("Synchronize Book", "Könyv szinkronizálása"), + ("System default", "Rendszer alapértelmezése"), + ("Tab", "Tabulátor"), + ("Table", "Táblázat"), + ("Table Dimensions", "Táblázat méretei"), + ("Table Options", "Táblázat beállításai"), + ("Table Panel", "Táblázat panel"), + ("Table Style", "Táblázatstílus"), + ("Table Style Options…", "Táblázatstílus beállításai…"), + ("Table of Contents", "Tartalomjegyzék"), + ("Table of Contents Entries", "Tartalomjegyzék bejegyzései"), + ("Table of Contents…", "Tartalomjegyzék…"), + ("Table/Convert Cell Type", "Táblázat/Cellatípus átalakítása"), + ("Table/Delete", "Táblázat/Törlés"), + ("Table/Insert", "Táblázat/Beszúrás"), + ("Table/Select", "Táblázat/Kijelölés"), + ("Tabs", "Tabulátorok"), + ("Tabular Lining", "Táblázatos verzálszámok"), + ("Tabular Oldstyle", "Táblázatos ugráló számok"), + ("Tag Frame", "Keret címkézése"), + ("Tag Text", "Szöveg címkézése"), + ("Tag the selection", "Kijelölés címkézése"), + ("Tag:", "Címke:"), + ("Tagged PDF", "Címkézett PDF"), + ("Tags", "Címkék"), + ("Tate-Chu-Yoko", "Tate-Chu-Yoko"), + ("Text", "Szöveg"), + ("Text Anchors", "Szöveghorgonyok"), + ("Text Display Options", "Szöveg megjelenítési beállításai"), + ("Text Field", "Szövegmező"), + ("Text Frame", "Szövegkeret"), + ("Text Frame Options", "Szövegkeret beállításai"), + ("Text Frame Options…", "Szövegkeret beállításai…"), + ("Text Message", "Szöveges üzenet"), + ("Text Style", "Szövegstílus"), + ("Text Variables", "Szövegváltozók"), + ("Text Wrap", "Szöveg körbefuttatása"), + ("The fill isn't a gradient.", "A kitöltés nem színátmenetes."), + ( + "The library is empty. Select objects and Add Selection.", + "A könyvtár üres. Jelölj ki objektumokat, majd válaszd a Kijelölés hozzáadása parancsot.", + ), + ( + "The object changes height as the page changes height. Click to lock the object height.", + "Az objektum magassága az oldal magasságával együtt változik. Kattints az objektum magasságának rögzítéséhez.", + ), + ( + "The object changes width as the page changes width. Click to lock the object width.", + "Az objektum szélessége az oldal szélességével együtt változik. Kattints az objektum szélességének rögzítéséhez.", + ), + ( + "The object's height is locked. Click to let the object change height as the page changes height.", + "Az objektum magassága rögzített. Kattints, hogy a magassága az oldal magasságával együtt változhasson.", + ), + ( + "The object's width is locked. Click to let the object change width as the page changes width.", + "Az objektum szélessége rögzített. Kattints, hogy a szélessége az oldal szélességével együtt változhasson.", + ), + ("The selected fonts have no OpenType features.", "A kijelölt betűtípusoknak nincsenek OpenType-funkcióik."), + ("The stroke isn't a gradient.", "A körvonal nem színátmenetes."), + ("The structure is valid.", "A struktúra érvényes."), + ("Thick - Thick", "Vastag – vastag"), + ("Thick - Thin", "Vastag – vékony"), + ("Thick - Thin - Thick", "Vastag – vékony – vastag"), + ("Thin - Thick", "Vékony – vastag"), + ("Thin - Thick - Thin", "Vékony – vastag – vékony"), + ("Thin - Thin", "Vékony – vékony"), + ("Thin Space", "Keskeny szóköz"), + ("Third Space", "Harmadkvirt szóköz"), + ("This PDF has no layers.", "Ez a PDF nem tartalmaz rétegeket."), + ( + "This story was edited in the layout: you'll be asked before those edits are replaced", + "Ezt a szövegfolyamot az elrendezésben módosítottad: a módosítások felülírása előtt megerősítést kérünk", + ), + ("This style is in use. Text using it will be reassigned to:", "Ez a stílus használatban van. Az ezt használó szöveg új stílusa:"), + ("Thread Text Frames", "Szövegkeretek összeláncolása"), + ("Tint %", "Árnyalat %"), + ("Tint:", "Százalékos árnyalat:"), + ("Title Case", "Szavak kezdőbetűje nagy"), + ("Titling Alternates", "Címbetűváltozatok"), + ("To Text:", "Erre a szövegre:"), + ("Toggle Fill and Stroke", "Váltás a kitöltés és a körvonal között"), + ("Toggle Normal/Preview", "Váltás normál nézet és előnézet között"), + ("Toggle Selection", "Kijelölés átváltása"), + ("Toggle double column", "Váltás két oszlopra"), + ("Tools: Double Column", "Eszközök: két oszlop"), + ("Top", "Fent"), + ("Top Inset", "Felső belső margó"), + ("Top Margin", "Felső margó"), + ("Top of Page", "Oldal teteje"), + ("Top:", "Fent:"), + ("Track Changes", "Változások követése"), + ("Track Changes in All Stories", "Változások követése minden szövegfolyamban"), + ("Tracking", "Betűköz"), + ("Tracking:", "Betűköz:"), + ("Trademark Symbol", "Védjegy jele"), + ("Transform", "Átalakítás"), + ("Transform Again", "Átalakítás ismétlése"), + ("Transform Again Individually", "Átalakítás ismétlése egyenként"), + ("Transform Panel", "Átalakítás panel"), + ("Transform Sequence Again", "Átalakítássorozat ismétlése"), + ("Transform Sequence Again Individually", "Átalakítássorozat ismétlése egyenként"), + ("Transform Values", "Átalakítási értékek"), + ("Transformations are Totals", "Összesített átalakítások"), + ("Transitions play in interactive PDF (full-screen mode).", "Az áttűnések interaktív PDF-ben, teljes képernyős módban játszhatók le."), + ("Transparency", "Átlátszóság"), + ("Transparency Blend Space", "Átlátszóság keverési színtere"), + ("Transparency Flattener Presets", "Átlátszóság-összeolvasztási készletek"), + ("Triangle", "Háromszög"), + ("Triangle - Wide", "Háromszög – széles"), + ("Trim", "Körülvágás"), + ("Turkish", "Török"), + ("Type", "Szöveg"), + ("Type & Tables", "Szöveg és táblázatok"), + ("Type Options", "Szövegbeállítások"), + ( + "Type a word with ~ at each allowed break (ex~am~ple); without ~ the word is never hyphenated.", + "Írjon be egy szót, és jelölje ~ karakterrel az engedélyezett elválasztásokat (el~vá~lasz~tás); ~ nélkül a szó nem választható el.", + ), + ("Type on a Path", "Szöveg görbén"), + ("Type on a Path Options", "Görbére írt szöveg beállításai"), + ("Type on a Path Options…", "Görbére írt szöveg beállításai…"), + ("Type/Bulleted and Numbered Lists", "Szöveg/Felsorolások és számozott listák"), + ("Type/Change Case", "Szöveg/Kis- és nagybetűk"), + ("Type/Hyperlinks & Cross-References", "Szöveg/Hiperhivatkozások és kereszthivatkozások"), + ("Type/Insert Break Character", "Szöveg/Töréskarakter beszúrása"), + ("Type/Insert Special Character/Hyphens and Dashes", "Szöveg/Különleges karakter beszúrása/Kötőjelek és gondolatjelek"), + ("Type/Insert Special Character/Markers", "Szöveg/Különleges karakter beszúrása/Jelölők"), + ("Type/Insert Special Character/Other", "Szöveg/Különleges karakter beszúrása/Egyéb"), + ("Type/Insert Special Character/Quotation Marks", "Szöveg/Különleges karakter beszúrása/Idézőjelek"), + ("Type/Insert Special Character/Symbols", "Szöveg/Különleges karakter beszúrása/Szimbólumok"), + ("Type/Insert White Space", "Szöveg/Szóköz beszúrása"), + ("Type/Proof Setup", "Szöveg/Próbanyomat beállítása"), + ("Type/Rotate Spread", "Szöveg/Oldalpár elforgatása"), + ("Type/Separations Preview", "Szöveg/Színkivonatok előnézete"), + ("Type/Story Direction", "Szöveg/Szövegfolyam iránya"), + ("Type/Text Variables", "Szöveg/Szövegváltozók"), + ("Type/Track Changes", "Szöveg/Változások követése"), + ("Type/Type on a Path", "Szöveg/Szöveg görbén"), + ("Type:", "Típus:"), + ("Typical", "Szokásos"), + ("Typical Display", "Szokásos megjelenítés"), + ("Typography", "Tipográfia"), + ("UI Scaling", "Felület méretezése"), + ("UI Size:", "Felület mérete:"), + ("UPPERCASE", "NAGYBETŰK"), + ("URL:", "URL:"), + ("US Business Card", "Amerikai névjegykártya"), + ("Ukrainian", "Ukrán"), + ("Unassigned", "Nincs hozzárendelve"), + ("Uncover", "Felfedés"), + ("Underline", "Aláhúzás"), + ("Underline On", "Aláhúzás bekapcsolása"), + ("Underline Options", "Aláhúzás beállításai"), + ("Undo", "Visszavonás"), + ("Ungroup", "Csoportbontás"), + ("Ungroup Color Group", "Színcsoport felbontása"), + ("Units", "Mértékegységek"), + ("Units & Increments", "Mértékegységek és lépésközök"), + ("Unlink", "Csatolás megszüntetése"), + ("Unlock", "Zárolás feloldása"), + ("Unlock All Layers", "Összes réteg zárolásának feloldása"), + ("Unlock All on Spread", "Összes zárolás feloldása az oldalpáron"), + ("Unmerge", "Egyesítés felbontása"), + ("Unmerge Cells", "Cellák egyesítésének megszüntetése"), + ("Unsaved Changes", "Nem mentett módosítások"), + ("Untag Selection", "Címke eltávolítása a kijelölésről"), + ("Update", "Frissítés"), + ("Update Data Source", "Adatforrás frissítése"), + ("Update Index", "Tárgymutató frissítése"), + ("Update Link", "Csatolás frissítése"), + ("Update Numbering", "Számozás frissítése"), + ("Update Table of Contents", "Tartalomjegyzék frissítése"), + ("Use Black Point Compensation", "Feketepont-kompenzáció használata"), + ("Use Global Light", "Globális fény használata"), + ("Use Typographer's Quotes", "Tipográfiai idézőjelek használata"), + ("User Dictionary", "Felhasználói szótár"), + ("User Dictionary (this document)", "Felhasználói szótár (ez a dokumentum)"), + ("User Dictionary Words", "Felhasználói szótár szavai"), + ("User Dictionary…", "Felhasználói szótár…"), + ("Utilities", "Segédeszközök"), + ("VA", "VA"), + ("Validate", "Érvényesítés"), + ("Validate from Root Element", "Érvényesítés a gyökérelemtől"), + ("Variable Font", "Változtatható betűtípus"), + ("Version", "Verzió"), + ("Vertical", "Függőleges"), + ("Vertical Gridline Every:", "Függőleges rácsvonalak távolsága:"), + ("Vertical Margins:", "Függőleges margók:"), + ("Vertical Scale:", "Függőleges méretezés:"), + ("Vertical Spacing", "Függőleges térköz"), + ("Vertical justification", "Függőleges igazítás"), + ("Vertical:", "Függőleges:"), + ("View", "Nézet"), + ("View Options", "Nézetbeállítások"), + ("View Threshold:", "Megjelenítési küszöb:"), + ("View/Display Performance", "Nézet/Megjelenítési teljesítmény"), + ("View/Extras", "Nézet/Extrák"), + ("View/Grids & Guides", "Nézet/Rácsok és segédvonalak"), + ("View/Proof Setup", "Nézet/Próbanyomat beállítása"), + ("View/Rotate Spread", "Nézet/Oldalpár elforgatása"), + ("View/Screen Mode", "Nézet/Képernyőmód"), + ("View/Separations Preview", "Nézet/Színkivonatok előnézete"), + ("View/Structure", "Nézet/Struktúra"), + ("W:", "Sz.:"), + ("Warichu", "Warichu"), + ("Wavy", "Hullámos"), + ("Web", "Web"), + ("Web Hyperlink", "Webes hiperhivatkozás"), + ("Weight", "Vonalvastagság"), + ("Weight:", "Vonalvastagság:"), + ("Welcome to DesignCraft", "Üdvözöl a DesignCraft"), + ("When Scaling", "Méretezéskor"), + ("Whole word", "Teljes szó"), + ("Width", "Szélesség"), + ("Width Only", "Csak szélesség"), + ("Width only", "Csak szélesség"), + ("Width:", "Szélesség:"), + ("Window", "Ablak"), + ("Window/Arrange", "Ablak/Elrendezés"), + ("Window/Interactive", "Ablak/Interaktív"), + ("Window/Interface Color Theme", "Ablak/Felület színtémája"), + ("Window/Output", "Ablak/Kimenet"), + ("Window/Panels", "Ablak/Panelek"), + ("Window/Type & Tables", "Ablak/Szöveg és táblázatok"), + ("Window/Utilities", "Ablak/Segédeszközök"), + ("Wipe", "Törléses áttűnés"), + ("Word Spacing:", "Szóköz:"), + ("Words with at Least:", "Szavak legalább ennyi betűvel:"), + ("Words, one per line:", "Szavak, soronként egy:"), + ("Working CMYK", "Munka CMYK"), + ("Workspace", "Munkaterület"), + ("Wrap Around Bounding Box", "Körbefuttatás a határolókeret körül"), + ("Wrap Around Object Shape", "Körbefuttatás az objektum alakja körül"), + ("Wrap Content", "Tartalom tördelése"), + ("X:", "X:"), + ("Y:", "Y:"), + ("Yellow", "Sárga"), + ("Zoom In", "Nagyítás"), + ("Zoom Out", "Kicsinyítés"), + ("Zoom To", "Nagyítás erre"), + ("[Automatic]", "[Automatikus]"), + ("[Basic Paragraph]", "[Alapbekezdés]"), + ("[Basic] (working) ▾", "[Alap] (munka) ▾"), + ("[Black] Overprint", "[Fekete] felülnyomás"), + ("[No Group]", "[Nincs csoport]"), + ("[No Paragraph Style]", "[Nincs bekezdésstílus]"), + ("[None]", "[Nincs]"), + ("[None] — choose a colour on the ramp.", "[Nincs] — válassz színt a színátmeneten."), + ("a, b, c, d...", "a, b, c, d..."), + ("bottom", "lent"), + ("center", "középen"), + ("characters", "karakter"), + ("chars", "karakter"), + ("digits", "számjegy"), + ("discord.gg/artcraft — help, feedback and show-and-tell", "discord.gg/artcraft — segítség, visszajelzés és bemutatók"), + ("document", "dokumentum"), + ("dropcap", "iniciálé"), + ("emSpace", "Kvirt szóköz"), + ("enSpace", "Félkvirt szóköz"), + ("error", "hiba"), + ("errors", "hiba"), + ("firstTint", "Első árnyalat"), + ("for", "ehhez"), + ("forcedLineBreak", "Kényszerített sortörés"), + ("graphic", "grafika"), + ("i, ii, iii, iv...", "i, ii, iii, iv..."), + ("items", "elem"), + ("justify", "sorkizárt"), + ("letters", "betű"), + ("line", "sor"), + ("lines", "sor"), + ("lowercase", "kisbetűk"), + ("nextTint", "Következő árnyalat"), + ("objects", "objektum"), + ("pages", "oldal"), + ("selection", "kijelölés"), + ("sentences", "mondat"), + ("stories", "szövegfolyam"), + ("story", "szövegfolyam"), + ("tab", "tabulátor"), + ("text", "szöveg"), + ("through", "beleértve"), + ("top", "fent"), + ("unassigned", "nincs hozzárendelve"), + ("unused", "nem használt"), + ("up to", "kizárva"), + ("warnings", "figyelmeztetés"), + ("words", "szó"), + ("x Height", "x-magasság"), + ("{count} characters don't fit", "{count} karakter nem fér el"), + ("{count} glyphs — click to insert at the text cursor", "{count} karakterjel — kattintással beszúrható a szövegkurzornál"), + ("{pages} Pages in {spreads} Spreads", "{pages} oldal, {spreads} oldalpáron"), + ("Українська", "Українська"), + ("العربية", "العربية"), + ("ქართული", "ქართული"), + ( + "“{}” has been edited in this layout. Updating replaces those edits with the text from the file.", + "A(z) „{}” szövegét módosítottad ebben az elrendezésben. A frissítés a fájl szövegével felülírja ezeket a módosításokat.", + ), + ("“{}” has changes that are not saved. Save them before closing?", "A(z) „{}” nem mentett módosításokat tartalmaz. Menti őket bezárás előtt?"), + ("⇧", "⇧"), + ("⌃", "⌃"), + ("⌘", "⌘"), + ("⌥", "⌥"), + ("⌥-click clears overrides", "⌥-kattintás törli a felülírásokat"), + ("▲ Modified", "▲ Módosítva"), + ("⚠ Missing", "⚠ Hiányzik"), + ("⚠ out of date", "⚠ elavult"), + ("日本語", "日本語"), + ("简体中文", "简体中文"), + ("Selection Tool", "Kijelölő eszköz"), + ("Direct Selection Tool", "Közvetlen kijelölő eszköz"), + ("Page Tool", "Oldaleszköz"), + ("Gap Tool", "Térközeszköz"), + ("Content Collector Tool", "Tartalomgyűjtő eszköz"), + ("Content Placer Tool", "Tartalomelhelyező eszköz"), + ("Type Tool", "Szövegeszköz"), + ("Type on a Path Tool", "Görbére író szövegeszköz"), + ("Vertical Type Tool", "Függőleges szövegeszköz"), + ("Vertical Type on a Path Tool", "Görbére író függőleges szövegeszköz"), + ("Line Tool", "Vonaleszköz"), + ("Pen Tool", "Toll eszköz"), + ("Add Anchor Point Tool", "Csomópont-hozzáadó eszköz"), + ("Delete Anchor Point Tool", "Csomóponttörlő eszköz"), + ("Convert Direction Point Tool", "Csomópont-átalakító eszköz"), + ("Pencil Tool", "Ceruza eszköz"), + ("Smooth Tool", "Simító eszköz"), + ("Erase Tool", "Radír eszköz"), + ("Rectangle Frame Tool", "Téglalapkeret eszköz"), + ("Ellipse Frame Tool", "Ellipsziskeret eszköz"), + ("Polygon Frame Tool", "Sokszögkeret eszköz"), + ("Rectangle Tool", "Téglalap eszköz"), + ("Ellipse Tool", "Ellipszis eszköz"), + ("Polygon Tool", "Sokszög eszköz"), + ("Scissors Tool", "Olló eszköz"), + ("Free Transform Tool", "Szabad átalakítás eszköz"), + ("Rotate Tool", "Forgató eszköz"), + ("Scale Tool", "Méretező eszköz"), + ("Shear Tool", "Döntő eszköz"), + ("Gradient Swatch Tool", "Színátmenet eszköz"), + ("Gradient Feather Tool", "Színátmenetes lágyítás eszköz"), + ("Note Tool", "Jegyzeteszköz"), + ("Color Theme Tool", "Színtéma eszköz"), + ("Eyedropper Tool", "Pipetta eszköz"), + ("Measure Tool", "Mérőeszköz"), + ("Hand Tool", "Kéz eszköz"), + ("Zoom Tool", "Nagyító eszköz"), +]; + +pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Columns", "table", "Oszlopok"), ("Group", "selection", "Csoport")]; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index acb8e3e42..59ad49d27 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -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()); diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 6b7d2c1c2..e0ec6f46c 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -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] diff --git a/docs/localization-parity.md b/docs/localization-parity.md index fa75a20d8..c24a8b36a 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -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 | From f3f6f721cb6d9f6fdcf5002617e827c672b34dee Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:01:12 +0200 Subject: [PATCH 02/75] Update contributors/people.toml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 21034681c..fb0a156bc 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -66,5 +66,8 @@ real_name = "Marco Arnulfo" display_name = "Marco Arnulfo" sync_github_name = false +[people.4waymedia] +real_name = "Paul Gemignani" + [people.macrobi] sync_github_name = true From 8e93096070fbc01cf09f762f967b399d9f87c832 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:41:55 +0200 Subject: [PATCH 03/75] HU1: address Hungarian localization review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Distinguish numbers from counts and text columns from grid columns, use formal Hungarian address consistently, and align menu paths with the visible labels. Add context and menu-path regression coverage and correct the localization language count and revision history. Set my chosen real name and display name to Veress Róbert and disable GitHub profile-name synchronization. Preserve all other contributor entries. --- ROADMAP.md | 2 ++ contributors/people.toml | 4 ++- crates/ui-egui/src/i18n.rs | 4 +++ crates/ui-egui/src/i18n/hu.rs | 61 ++++++++++++++++++----------------- crates/ui-egui/src/menus.rs | 16 +++++++++ docs/localization-parity.md | 13 +++++--- 6 files changed, 65 insertions(+), 35 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e939f29bc..cd2a76bf8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-20** · HU1: Hungarian review corrections distinguish numbers/counts and text/grid columns (four context captions), consistently use formal address, and align menu paths with visible labels. Added context and menu-path regression coverage; corrected localization documentation. No readiness percentage change. + - **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. diff --git a/contributors/people.toml b/contributors/people.toml index fb0a156bc..d37757905 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -70,4 +70,6 @@ sync_github_name = false real_name = "Paul Gemignani" [people.macrobi] -sync_github_name = true +real_name = "Veress Róbert" +display_name = "Veress Róbert" +sync_github_name = false diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 71861bcf9..18d0d9ea7 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4661,6 +4661,9 @@ mod tests { 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"); @@ -4722,6 +4725,7 @@ mod tests { #[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", "Кількість:", "Номер:"), diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs index 332f3c038..f31d9d010 100644 --- a/crates/ui-egui/src/i18n/hu.rs +++ b/crates/ui-egui/src/i18n/hu.rs @@ -503,7 +503,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Edit/Interface Language", "Szerkesztés/Felület nyelve"), ("Edit/Spelling", "Szerkesztés/Helyesírás"), ("Edit/Transparency Blend Space", "Szerkesztés/Átlátszóság keverési színtere"), - ("Edit/Transparency Flattener Presets", "Szerkesztés/Lapítási készletek"), + ("Edit/Transparency Flattener Presets", "Szerkesztés/Átlátszóság-összeolvasztási készletek"), ("Editorial", "Szerkesztőség"), ("Effects", "Hatások"), ("Effects: Drop Shadow", "Hatások: vetett árnyék"), @@ -1074,7 +1074,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Number of Pages:", "Oldalak száma:"), ("Number of Sides:", "Sokszög oldalszáma:"), ("Number of columns", "Oszlopok száma"), - ("Number:", "Darabszám:"), + ("Number:", "Szám:"), ("Numbered List", "Számozott lista"), ("Numbering", "Számozás"), ("Numbering & Section Options", "Számozás és szakaszbeállítások"), @@ -1438,18 +1438,18 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Select Word", "Szó kijelölése"), ( "Select a placed video or sound (File › Place a .mp4, .mov, .mp3, .wav …).", - "Jelölj ki egy elhelyezett videót vagy hangot (Fájl › Elhelyezés: .mp4, .mov, .mp3, .wav …).", + "Jelöljön ki egy elhelyezett videót vagy hangot (Fájl › Elhelyezés: .mp4, .mov, .mp3, .wav …).", ), - ("Select an object or text to color its fill or stroke.", "Jelölj ki egy objektumot vagy szöveget a kitöltés vagy a körvonal színezéséhez."), - ("Select an object to color its fill or stroke.", "Jelölj ki egy objektumot a kitöltés vagy a körvonal színezéséhez."), - ("Select an object to give its fill a gradient.", "Jelölj ki egy objektumot a színátmenetes kitöltéshez."), - ("Select an object to make it a button.", "Jelölj ki egy objektumot a gombbá alakításhoz."), - ("Select an object.", "Jelölj ki egy objektumot."), - ("Select text or a text frame.", "Jelölj ki szöveget vagy szövegkeretet."), - ("Select text to apply a condition", "Jelölj ki szöveget a feltétel alkalmazásához"), + ("Select an object or text to color its fill or stroke.", "Jelöljön ki egy objektumot vagy szöveget a kitöltés vagy a körvonal színezéséhez."), + ("Select an object to color its fill or stroke.", "Jelöljön ki egy objektumot a kitöltés vagy a körvonal színezéséhez."), + ("Select an object to give its fill a gradient.", "Jelöljön ki egy objektumot a színátmenetes kitöltéshez."), + ("Select an object to make it a button.", "Jelöljön ki egy objektumot a gombbá alakításhoz."), + ("Select an object.", "Jelöljön ki egy objektumot."), + ("Select text or a text frame.", "Jelöljön ki szöveget vagy szövegkeretet."), + ("Select text to apply a condition", "Jelöljön ki szöveget a feltétel alkalmazásához"), ( "Select two or more objects (or a group) to make a multi-state object.", - "Jelölj ki legalább két objektumot (vagy egy csoportot) a többállapotú objektum létrehozásához.", + "Jelöljön ki legalább két objektumot (vagy egy csoportot) a többállapotú objektum létrehozásához.", ), ("Selection", "Kijelölés"), ("Selection Attributes", "Kijelölés tulajdonságai"), @@ -1660,23 +1660,23 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("The fill isn't a gradient.", "A kitöltés nem színátmenetes."), ( "The library is empty. Select objects and Add Selection.", - "A könyvtár üres. Jelölj ki objektumokat, majd válaszd a Kijelölés hozzáadása parancsot.", + "A könyvtár üres. Jelöljön ki objektumokat, majd válassza a Kijelölt elemek hozzáadása parancsot.", ), ( "The object changes height as the page changes height. Click to lock the object height.", - "Az objektum magassága az oldal magasságával együtt változik. Kattints az objektum magasságának rögzítéséhez.", + "Az objektum magassága az oldal magasságával együtt változik. Kattintson az objektum magasságának rögzítéséhez.", ), ( "The object changes width as the page changes width. Click to lock the object width.", - "Az objektum szélessége az oldal szélességével együtt változik. Kattints az objektum szélességének rögzítéséhez.", + "Az objektum szélessége az oldal szélességével együtt változik. Kattintson az objektum szélességének rögzítéséhez.", ), ( "The object's height is locked. Click to let the object change height as the page changes height.", - "Az objektum magassága rögzített. Kattints, hogy a magassága az oldal magasságával együtt változhasson.", + "Az objektum magassága rögzített. Kattintson, hogy a magassága az oldal magasságával együtt változhasson.", ), ( "The object's width is locked. Click to let the object change width as the page changes width.", - "Az objektum szélessége rögzített. Kattints, hogy a szélessége az oldal szélességével együtt változhasson.", + "Az objektum szélessége rögzített. Kattintson, hogy a szélessége az oldal szélességével együtt változhasson.", ), ("The selected fonts have no OpenType features.", "A kijelölt betűtípusoknak nincsenek OpenType-funkcióik."), ("The stroke isn't a gradient.", "A körvonal nem színátmenetes."), @@ -1692,7 +1692,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("This PDF has no layers.", "Ez a PDF nem tartalmaz rétegeket."), ( "This story was edited in the layout: you'll be asked before those edits are replaced", - "Ezt a szövegfolyamot az elrendezésben módosítottad: a módosítások felülírása előtt megerősítést kérünk", + "Ezt a szövegfolyamot az elrendezésben módosította: a módosítások felülírása előtt megerősítést kérünk", ), ("This style is in use. Text using it will be reassigned to:", "Ez a stílus használatban van. Az ezt használó szöveg új stílusa:"), ("Thread Text Frames", "Szövegkeretek összeláncolása"), @@ -1743,16 +1743,16 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Type on a Path Options", "Görbére írt szöveg beállításai"), ("Type on a Path Options…", "Görbére írt szöveg beállításai…"), ("Type/Bulleted and Numbered Lists", "Szöveg/Felsorolások és számozott listák"), - ("Type/Change Case", "Szöveg/Kis- és nagybetűk"), + ("Type/Change Case", "Szöveg/Kis- és nagybetűk váltása"), ("Type/Hyperlinks & Cross-References", "Szöveg/Hiperhivatkozások és kereszthivatkozások"), ("Type/Insert Break Character", "Szöveg/Töréskarakter beszúrása"), - ("Type/Insert Special Character/Hyphens and Dashes", "Szöveg/Különleges karakter beszúrása/Kötőjelek és gondolatjelek"), - ("Type/Insert Special Character/Markers", "Szöveg/Különleges karakter beszúrása/Jelölők"), - ("Type/Insert Special Character/Other", "Szöveg/Különleges karakter beszúrása/Egyéb"), - ("Type/Insert Special Character/Quotation Marks", "Szöveg/Különleges karakter beszúrása/Idézőjelek"), - ("Type/Insert Special Character/Symbols", "Szöveg/Különleges karakter beszúrása/Szimbólumok"), - ("Type/Insert White Space", "Szöveg/Szóköz beszúrása"), - ("Type/Proof Setup", "Szöveg/Próbanyomat beállítása"), + ("Type/Insert Special Character/Hyphens and Dashes", "Szöveg/Speciális karakter beszúrása/Kötőjelek és gondolatjelek"), + ("Type/Insert Special Character/Markers", "Szöveg/Speciális karakter beszúrása/Jelölők"), + ("Type/Insert Special Character/Other", "Szöveg/Speciális karakter beszúrása/Egyéb"), + ("Type/Insert Special Character/Quotation Marks", "Szöveg/Speciális karakter beszúrása/Idézőjelek"), + ("Type/Insert Special Character/Symbols", "Szöveg/Speciális karakter beszúrása/Szimbólumok"), + ("Type/Insert White Space", "Szöveg/Szóközkarakter beszúrása"), + ("Type/Proof Setup", "Szöveg/Próbanyomat beállításai"), ("Type/Rotate Spread", "Szöveg/Oldalpár elforgatása"), ("Type/Separations Preview", "Szöveg/Színkivonatok előnézete"), ("Type/Story Direction", "Szöveg/Szövegfolyam iránya"), @@ -1817,9 +1817,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("View Options", "Nézetbeállítások"), ("View Threshold:", "Megjelenítési küszöb:"), ("View/Display Performance", "Nézet/Megjelenítési teljesítmény"), - ("View/Extras", "Nézet/Extrák"), + ("View/Extras", "Nézet/Kiegészítők"), ("View/Grids & Guides", "Nézet/Rácsok és segédvonalak"), - ("View/Proof Setup", "Nézet/Próbanyomat beállítása"), + ("View/Proof Setup", "Nézet/Próbanyomat beállításai"), ("View/Rotate Spread", "Nézet/Oldalpár elforgatása"), ("View/Screen Mode", "Nézet/Képernyőmód"), ("View/Separations Preview", "Nézet/Színkivonatok előnézete"), @@ -1868,7 +1868,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("[No Group]", "[Nincs csoport]"), ("[No Paragraph Style]", "[Nincs bekezdésstílus]"), ("[None]", "[Nincs]"), - ("[None] — choose a colour on the ramp.", "[Nincs] — válassz színt a színátmeneten."), + ("[None] — choose a colour on the ramp.", "[Nincs] — válasszon színt a színátmeneten."), ("a, b, c, d...", "a, b, c, d..."), ("bottom", "lent"), ("center", "középen"), @@ -1918,7 +1918,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("ქართული", "ქართული"), ( "“{}” has been edited in this layout. Updating replaces those edits with the text from the file.", - "A(z) „{}” szövegét módosítottad ebben az elrendezésben. A frissítés a fájl szövegével felülírja ezeket a módosításokat.", + "A(z) „{}” szövegét módosította ebben az elrendezésben. A frissítés a fájl szövegével felülírja ezeket a módosításokat.", ), ("“{}” has changes that are not saved. Save them before closing?", "A(z) „{}” nem mentett módosításokat tartalmaz. Menti őket bezárás előtt?"), ("⇧", "⇧"), @@ -1970,4 +1970,5 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Zoom Tool", "Nagyító eszköz"), ]; -pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Columns", "table", "Oszlopok"), ("Group", "selection", "Csoport")]; +pub(super) const CONTEXT: &[(&str, &str, &str)] = + &[("Columns", "table", "Oszlopok"), ("Columns:", "grid", "Oszlopok:"), ("Group", "selection", "Csoport"), ("Number:", "count", "Darabszám:")]; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index bfd4a94f9..299a5dcc3 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -3315,6 +3315,22 @@ mod tests { } } + #[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::>().join("/"); + assert_eq!(translated, visible, "{path}"); + } + } + } + fn walk(items: &[Item], out: &mut Vec<(String, String, Value)>) { for it in items { match it { diff --git a/docs/localization-parity.md b/docs/localization-parity.md index c24a8b36a..709a8f8aa 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **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) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-20 · **Change:** minor (Hungarian review corrections; 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), @@ -48,26 +48,29 @@ 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 +Hungarian follow-up (2026-10-17): `hu.rs` adds **1,917 entries and four 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. +Review corrections (2026-10-20) distinguish numbers from counts and text columns from grid +columns, use formal Hungarian address consistently, and align translated menu paths with +their visible labels. These corrections do not add new catalog keys or change coverage. ## Other shipped languages | Language | Code | UI strings translated | Status | |---|---|---|---| -| Hungarian | hu | 1,917 entries + 2 context captions (2026-10-17); initial native review | partial | +| Hungarian | hu | 1,917 entries + 4 context captions (2026-10-20); 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 | | 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 @@ -94,6 +97,8 @@ locale shaping. See [typography-parity.md](typography-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-20 | minor | Hungarian review: four context captions, formal address, consistent menu paths; corrected other-language count to six | +| 2026-10-17 | minor | Hungarian (`hu`) interface added: 1,917 entries and two context captions; initial native-speaker review | | 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 | From 89f7162457099ec6c3030dfcacf6a92ec6586abb Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:41:05 +0200 Subject: [PATCH 04/75] HU1: restore localization tests after merging main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the separate catalog and caption tests, preserve both merge parents’ assertions, and include Hungarian in the shared integrity checks. Part of the four-file update validated by all eight local CI stages (2,993 tests passed). --- crates/ui-egui/src/i18n.rs | 93 ++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 1fc7f4611..097cda295 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4613,6 +4613,7 @@ mod tests { ("cs", cs::TABLE), ("de", de::TABLE), ("fr", fr::TABLE), + ("hu", hu::TABLE), ("it", it::TABLE), ("ja", ja::TABLE), ("ka", ka::TABLE), @@ -4623,8 +4624,15 @@ mod tests { ]; /// 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"]; @@ -4661,12 +4669,12 @@ mod tests { assert_eq!(tr("", key), *key); } } - for (key, expected) in hu::TABLE { - assert_eq!(tr("hu", key), *expected, "hu: {key}"); - } + // Strings no catalog has stay as they are, in every language. let unknown = String::from("A user-defined untranslated label"); - 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())); + for lang in LANGUAGES.iter().map(|(code, _)| *code).chain(["en", "unknown"]) { + assert!(std::ptr::eq(tr(lang, &unknown), unknown.as_str()), "{lang}"); + // Fixed standard names stay identical. + assert_eq!(tr(lang, "PDF/X-4"), "PDF/X-4", "{lang}"); } } @@ -4739,6 +4747,25 @@ mod tests { 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}"); + } + } + + #[test] + fn context_captions_come_from_their_context_or_the_plain_translation() { + for (lang, table) in CONTEXT_TABLES { + for (key, scope, text) in *table { + assert_eq!(tr_context(lang, key, scope), *text, "{lang}: {key} ({scope})"); + } + } for (key, scope, row) in CONTEXT { for (lang, shared) in SHARED_COLUMNS.into_iter().zip(row) { // A language's own context table is looked up before the shared one. @@ -4761,9 +4788,6 @@ mod tests { } } - /// Every catalog: no duplicate or stale keys, no empty translations, and placeholders, padding, - /// ellipses and label colons as in English. A string a catalog doesn't translate yet falls back - /// to English, so missing translations don't fail here. #[test] fn a_column_count_and_a_list_number_read_differently() { for (lang, count, numeral) in [ @@ -4777,6 +4801,38 @@ mod tests { ] { 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"); + } + + /// Every catalog: no duplicate or stale keys, no empty translations, and placeholders, padding, + /// ellipses and label colons as in English. A string a catalog doesn't translate yet falls back + /// to English, so missing translations don't fail here. + #[test] + fn every_catalog_is_unique_current_and_keeps_placeholders_and_markers() { + // (language, key, translation) of every catalog, and every catalog's keys by name. + let mut entries: Vec<(&str, &str, &str)> = Vec::new(); + let mut catalogs: Vec<(&str, HashSet<&str>)> = Vec::new(); + for (lang, table) in LANGUAGE_TABLES { + let keys: HashSet<_> = table.iter().map(|(key, _)| *key).collect(); + assert_eq!(keys.len(), table.len(), "{lang}: duplicate keys"); + catalogs.push((*lang, keys)); + entries.extend(table.iter().map(|(key, text)| (*lang, *key, *text))); + } let shared: HashSet<_> = TABLE.iter().map(|(key, _)| *key).collect(); assert_eq!(shared.len(), TABLE.len(), "duplicate keys in the shared table"); catalogs.push(("shared", shared.clone())); @@ -4919,6 +4975,23 @@ mod tests { keys.extend(crate::credits::SortKey::ALL.iter().flat_map(|key| [key.label().0, key.label().1])); // 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]", From 99761bdbadb0fd20c43e69f39661ea396c762f64 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:45:54 +0200 Subject: [PATCH 05/75] HU1: remove unused Hungarian catalog entries Remove only 13 unused entries identified by the shared integrity test; retain all active translated UI labels. Validated with the complete four-file update: 14 localization tests and all eight CI stages passed. --- crates/ui-egui/src/i18n/hu.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs index f31d9d010..d890555c1 100644 --- a/crates/ui-egui/src/i18n/hu.rs +++ b/crates/ui-egui/src/i18n/hu.rs @@ -436,7 +436,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Desktop Printing", "Asztali nyomtatás"), ("Detach All Objects from Parent", "Összes objektum leválasztása a mesteroldalról"), ("Detect Edges", "Szélek felismerése"), - ("Deutsch", "Deutsch"), ("Dialog", "Párbeszédablak"), ("Dictionary", "Szótár"), ("Digital Publishing", "Digitális kiadványkészítés"), @@ -521,13 +520,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Enable Autocorrect", "Automatikus javítás engedélyezése"), ("Enable Dynamic Spelling (underline misspelled words)", "Folyamatos helyesírás-ellenőrzés (hibás szavak aláhúzása)"), ("Endnotes", "Végjegyzetek"), - ("English", "English"), ("English: Canadian", "Angol: kanadai"), ("English: UK", "Angol: brit"), ("English: USA", "Angol: amerikai"), ("Entire Pasteboard", "Teljes munkaterület"), ("Erase", "Radírozás"), - ("Español", "Español"), ("Essentials", "Alapok"), ("Essentials Classic", "Klasszikus alapok"), ("Even Page Break", "Páros oldaltörés"), @@ -648,7 +645,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Frame Fitting", "Keretillesztés"), ("Frame Fitting Options", "Keretillesztés beállításai"), ("Frame Fitting Options…", "Keretillesztés beállításai…"), - ("Français", "Français"), ("French", "Francia"), ("French: Canadian", "Francia: kanadai"), ("GREP", "GREP"), @@ -825,7 +821,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Inward", "Befelé"), ("Isolate Blending", "Keverés elkülönítése"), ("Italian", "Olasz"), - ("Italiano", "Italiano"), ("Italic", "Dőlt"), ("Japanese", "Japán"), ("Join", "Összekapcsolás"), @@ -1238,7 +1233,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Portrait", "Álló"), ("Portuguese", "Portugál"), ("Portuguese: Brazilian", "Portugál: brazíliai"), - ("Português (Brasil)", "Português (Brasil)"), ("Position", "Pozíció"), ("Position:", "Pozíció:"), ("Poster", "Poszter"), @@ -1882,7 +1876,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("enSpace", "Félkvirt szóköz"), ("error", "hiba"), ("errors", "hiba"), - ("firstTint", "Első árnyalat"), ("for", "ehhez"), ("forcedLineBreak", "Kényszerített sortörés"), ("graphic", "grafika"), @@ -1893,7 +1886,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("line", "sor"), ("lines", "sor"), ("lowercase", "kisbetűk"), - ("nextTint", "Következő árnyalat"), ("objects", "objektum"), ("pages", "oldal"), ("selection", "kijelölés"), @@ -1913,9 +1905,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("{count} characters don't fit", "{count} karakter nem fér el"), ("{count} glyphs — click to insert at the text cursor", "{count} karakterjel — kattintással beszúrható a szövegkurzornál"), ("{pages} Pages in {spreads} Spreads", "{pages} oldal, {spreads} oldalpáron"), - ("Українська", "Українська"), - ("العربية", "العربية"), - ("ქართული", "ქართული"), ( "“{}” has been edited in this layout. Updating replaces those edits with the text from the file.", "A(z) „{}” szövegét módosította ebben az elrendezésben. A frissítés a fájl szövegével felülírja ezeket a módosításokat.", @@ -1929,8 +1918,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("▲ Modified", "▲ Módosítva"), ("⚠ Missing", "⚠ Hiányzik"), ("⚠ out of date", "⚠ elavult"), - ("日本語", "日本語"), - ("简体中文", "简体中文"), ("Selection Tool", "Kijelölő eszköz"), ("Direct Selection Tool", "Közvetlen kijelölő eszköz"), ("Page Tool", "Oldaleszköz"), From 306aceb9c8f286616d5be9dd9857bb99609817c5 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:46:55 +0200 Subject: [PATCH 06/75] HU1: document localization test integration Record the merge test repairs and the verified catalog count of 1,904 entries plus four context captions. Part of the locally validated four-file update. --- docs/localization-parity.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 709a8f8aa..cc12c0ce3 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-20 · **Change:** minor (Hungarian review corrections; older catalog counts retain their dated measurement) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (Hungarian catalog test integration; 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), @@ -58,12 +58,17 @@ follow-up work and are not enabled by selecting this interface language. Review corrections (2026-10-20) distinguish numbers from counts and text columns from grid columns, use formal Hungarian address consistently, and align translated menu paths with their visible labels. These corrections do not add new catalog keys or change coverage. +Test integration (2026-10-23) registers Hungarian in the shared catalog checks and restores +assertions from both merge parents. The catalog now has **1,904 entries and four context +captions** after removing 11 untranslated interface-language names (the selector displays their +native names directly) and two unused internal tint keys. Seventeen live UI labels are retained +and added to the integrity test's known-label list. No used Hungarian translation was removed. ## Other shipped languages | Language | Code | UI strings translated | Status | |---|---|---|---| -| Hungarian | hu | 1,917 entries + 4 context captions (2026-10-20); initial native review | partial | +| Hungarian | hu | 1,904 entries + 4 context captions (2026-10-23); 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 | @@ -99,6 +104,7 @@ locale shaping. See [typography-parity.md](typography-parity.md). |---|---|---| | 2026-10-20 | minor | Hungarian review: four context captions, formal address, consistent menu paths; corrected other-language count to six | | 2026-10-17 | minor | Hungarian (`hu`) interface added: 1,917 entries and two context captions; initial native-speaker review | +| 2026-10-23 | minor | Hungarian test integration restored; 1,904 active catalog entries and four context captions after removing unused keys | | 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 | From 5614ce58ea82d5c6700d16d046d709d31ad295e5 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:47:57 +0200 Subject: [PATCH 07/75] HU1: record localization merge test repairs Record the test integration fixes, preserve both sides of the merge, and align the current Hungarian catalog count with the validated sources. --- ROADMAP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index d24f2e21e..7b2d81c90 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -346,6 +346,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- 2026-10-23 (HU1): repair localization test integration after merging main: restore separate all-catalog integrity, context-caption and numeral/count tests, retaining assertions from both branches; register Hungarian in the shared language and context catalogs. Retain 17 live labels missing from the known-label test list and remove 13 unused catalog entries (1,904 entries + four context captions remain). + Newest first. - **2026-10-20** · HU1: Hungarian review corrections distinguish numbers/counts and text/grid columns (four context captions), consistently use formal address, and align menu paths with visible labels. Added context and menu-path regression coverage; corrected localization documentation. No readiness percentage change. From e4fc34e2bb51ff13873dbfc610f773f40645f5df Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:19:46 +0200 Subject: [PATCH 08/75] HU1: correct historical context-caption count Correct only the 2026-10-17 narrative from four to two context captions, matching the dated revision history and ROADMAP. The current catalog still has four context captions. All eight local CI stages passed. --- docs/localization-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index cc12c0ce3..505b58ec6 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -48,7 +48,7 @@ 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 four context-specific +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 From 06da9c8f1503e3a588ee260cf74ca970459a183e Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:59:34 +0200 Subject: [PATCH 09/75] HU1: document translation lookup and catalog tests Add concise documentation for the cached lookup and three localization regression tests. Documentation only; runtime code, translations and test assertions are unchanged. --- crates/ui-egui/src/i18n.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 0038833dc..ba06b27ab 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4466,6 +4466,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> = OnceLock::new(); static JAPANESE: OnceLock> = OnceLock::new(); @@ -4695,6 +4697,8 @@ mod tests { } } + /// 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(); @@ -4737,6 +4741,8 @@ mod tests { assert_eq!(workspace_name("hu", "Saját munkaterület"), "Saját munkaterület"); } + /// 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"); @@ -4809,6 +4815,7 @@ mod tests { } } + /// 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 [ From 353e4188b32822dd6e27274f0d01afdfc982fcf9 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:02:30 +0200 Subject: [PATCH 10/75] HU1: document menu and language regression tests Describe the purpose of five existing menu-path and language-selection tests. Documentation only; test assertions and application behavior are unchanged. --- crates/ui-egui/src/menus.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index ba531aa48..f8fd21f6d 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -3375,6 +3375,8 @@ mod tests { 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 @@ -3799,6 +3801,8 @@ mod tests { 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()); @@ -3884,6 +3888,8 @@ mod tests { assert!(untranslated.is_empty(), "untranslated Georgian menu labels: {untranslated:?}"); } + /// 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()); @@ -3904,6 +3910,8 @@ mod tests { 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()); @@ -3924,6 +3932,8 @@ mod tests { 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()); From c0b67abefd72f8bf4ac4e0813c2ba4f87d6c9226 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:03:14 +0200 Subject: [PATCH 11/75] HU1: document Tools panel and localized tooltip test Describe adaptive Tools-panel rendering and the existing Hungarian tooltip regression test. Documentation only; rendering and test assertions are unchanged. --- crates/ui-egui/src/toolbar.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index e0ec6f46c..953c8feb0 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -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)); @@ -666,6 +668,8 @@ mod tests { h.get_by_label(label).rect() } + /// Checks every tool's translated tooltip and unchanged shortcut in both Tools-panel + /// column layouts, including the Hungarian Direct Selection caption. #[test] fn hungarian_tool_buttons_translate_every_tool_and_keep_shortcuts() { for double in [false, true] { From e6ca917e2b38050bb58ff23132e9c35619a687a6 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:03:47 +0200 Subject: [PATCH 12/75] HU1: record function documentation update Record the documentation-only follow-up for localization helpers and regression tests. Existing roadmap entries are preserved. --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index edf4c861b..213daa779 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -360,6 +360,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. - **2026-10-24** · Custom-positioned anchored objects honour Relative to Spine (`anchored.options {spineRelative}`, IDML `SpineRelative`): the settings describe a left-hand page and mirror on a right-hand page, so marginal notes stay in the outside margin on both pages of a spread. On right-hand pages they took the left-hand placement and landed in the inside margin, across the spine or over the body text. No readiness percentage change. - **2026-10-23** · Save and export dialogs (Save As, Save a Copy, Export PDF and the other exports, Package, libraries and books) start in the active document's folder once it has been saved, and for a document never saved in the folder of the last file saved or exported, remembered across sessions (#1653). A folder deleted since falls back to the system's choice. No readiness percentage change. - **2026-10-22** · Autoflow (Shift-click with the loaded text cursor, autoflow on Place, `text.thread {autoflow}`) fills the pages that follow before it adds any, as InDesign does: one frame per column on each following page, whatever else the page holds, then new pages at the end of the document. It inserted pages right after the story's last frame, in front of the document's own pages. Frames it set past the end of the text go again; a page the document had stays, and a page autoflow added only for such a frame goes with it. Smart Text Reflow of the primary text frame is unchanged. No readiness percentage change. From 7065cb602d0bbfadaf625d34419f280a00ca8060 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:13:51 +0200 Subject: [PATCH 13/75] HU1: keep documentation log clear of upstream additions Relocate the documentation-only progress entry beside the existing Hungarian entry. Preserve every line and allow a clean merge with main; no code or tests changed. All nine cargo xtask ci gates passed. --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 213daa779..09ab4c3c8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -356,11 +356,11 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. - 2026-10-23 (HU1): repair localization test integration after merging main: restore separate all-catalog integrity, context-caption and numeral/count tests, retaining assertions from both branches; register Hungarian in the shared language and context catalogs. Retain 17 live labels missing from the known-label test list and remove 13 unused catalog entries (1,904 entries + four context captions remain). Newest first. -- **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. - **2026-10-24** · Custom-positioned anchored objects honour Relative to Spine (`anchored.options {spineRelative}`, IDML `SpineRelative`): the settings describe a left-hand page and mirror on a right-hand page, so marginal notes stay in the outside margin on both pages of a spread. On right-hand pages they took the left-hand placement and landed in the inside margin, across the spine or over the body text. No readiness percentage change. - **2026-10-23** · Save and export dialogs (Save As, Save a Copy, Export PDF and the other exports, Package, libraries and books) start in the active document's folder once it has been saved, and for a document never saved in the folder of the last file saved or exported, remembered across sessions (#1653). A folder deleted since falls back to the system's choice. No readiness percentage change. - **2026-10-22** · Autoflow (Shift-click with the loaded text cursor, autoflow on Place, `text.thread {autoflow}`) fills the pages that follow before it adds any, as InDesign does: one frame per column on each following page, whatever else the page holds, then new pages at the end of the document. It inserted pages right after the story's last frame, in front of the document's own pages. Frames it set past the end of the text go again; a page the document had stays, and a page autoflow added only for such a frame goes with it. Smart Text Reflow of the primary text frame is unchanged. No readiness percentage change. From 775ba1c6ad41f7c21a3530818aa6176f844c6414 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:39:20 +0100 Subject: [PATCH 14/75] HU1: document cached and context translation tests Add purpose descriptions only. All nine CI gates passed; test assertions and executable code are unchanged. --- crates/ui-egui/src/i18n.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index ba06b27ab..b4a81f666 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4667,6 +4667,7 @@ mod tests { 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| { @@ -4782,6 +4783,7 @@ mod tests { } } + /// 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 { From 18c47868dc23ea5b47930794af28ff71a54e1759 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:40:03 +0100 Subject: [PATCH 15/75] HU1: document screen-mode and document-grid tests Add purpose descriptions only. All nine CI gates passed; existing test assertions remain unchanged. --- crates/ui-egui/src/menus.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index f8fd21f6d..46144caa1 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -3366,6 +3366,7 @@ mod tests { 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()); @@ -3783,6 +3784,7 @@ mod tests { 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()); From 84d84f9b1eace2cc0bb058177896d1b1b6c25cb3 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:43:37 +0100 Subject: [PATCH 16/75] HU1: document toolbar test widget bounds helper Document the existing labeled-widget lookup helper. Documentation only; all nine CI gates passed. --- crates/ui-egui/src/toolbar.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 953c8feb0..e03c89133 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -664,6 +664,7 @@ mod tests { 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() } From 790fd6a0983a28236800916c109de2ff7262c8b2 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:56:05 +0100 Subject: [PATCH 17/75] HU1: avoid contributor insertion conflict without changing names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder the existing Hungarian contributor block so it merges with the new upstream contributor. Preserve every name and setting, including Veress Róbert and disabled GitHub-name synchronization. All nine CI gates passed. --- contributors/people.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contributors/people.toml b/contributors/people.toml index d37757905..b832279f5 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -66,10 +66,10 @@ real_name = "Marco Arnulfo" display_name = "Marco Arnulfo" sync_github_name = false -[people.4waymedia] -real_name = "Paul Gemignani" - [people.macrobi] real_name = "Veress Róbert" display_name = "Veress Róbert" sync_github_name = false + +[people.4waymedia] +real_name = "Paul Gemignani" From bab48520589772afa7641659ac32fba43ad68584 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:37:15 +0100 Subject: [PATCH 18/75] HU1: cover Hungarian tool flyouts through pointer input Extend the existing Hungarian toolbar test with real right-click flyouts, translated row labels, painted shortcuts, clipping and overlap checks. Preserve original assertions and production code. Focused test and all nine CI gates passed. --- crates/ui-egui/src/toolbar.rs | 66 +++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index e03c89133..e55b57f5e 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -669,10 +669,21 @@ mod tests { h.get_by_label(label).rect() } - /// Checks every tool's translated tooltip and unchanged shortcut in both Tools-panel - /// column layouts, including the Hungarian Direct Selection caption. + /// 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}; + + /// 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)), + _ => {} + } + } + for double in [false, true] { let mut app = app(double); app.ui.language = "hu".into(); @@ -688,6 +699,57 @@ mod tests { }; assert!(rect(&h, &tip).is_finite(), "{tip}"); } + 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_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); + if let Some(shortcut) = tool.shortcut { + 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"); } } From ac8c187f9905846ce0663db9bd347a8e2faf6e97 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:37:59 +0100 Subject: [PATCH 19/75] HU1: record Hungarian flyout regression coverage Record tested right-click flyout coverage for translated row labels, shortcuts, clipping and overlap. No runtime or readiness change. --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index 09ab4c3c8..0a023d061 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -356,6 +356,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- **2026-10-25** · HU1: extend Hungarian toolbar regression coverage through real right-click flyouts in both column layouts, checking translated row labels, painted shortcuts, clipping and overlap. Existing assertions and runtime behavior are unchanged. No readiness percentage change. - **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. - 2026-10-23 (HU1): repair localization test integration after merging main: restore separate all-catalog integrity, context-caption and numeral/count tests, retaining assertions from both branches; register Hungarian in the shared language and context catalogs. Retain 17 live labels missing from the known-label test list and remove 13 unused catalog entries (1,904 entries + four context captions remain). From 2937a52babb30e2f94c148ba90c1b99b85bfcccc Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:00:29 +0100 Subject: [PATCH 20/75] HU1: translate the Data Merge Arrangement caption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Arrangement → Elrendezés to the Hungarian catalog. The retained cross-locale regression and Hungarian catalog tests pass. Full local validation and the separate pre-existing font/Georgian findings are documented in the PR. --- crates/ui-egui/src/i18n/hu.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs index d890555c1..29406978c 100644 --- a/crates/ui-egui/src/i18n/hu.rs +++ b/crates/ui-egui/src/i18n/hu.rs @@ -130,6 +130,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Apply to Stroke", "Alkalmazás a körvonalra"), ("Arabic", "Arab"), ("Arrange", "Sorrend"), + ("Arrangement", "Elrendezés"), ("Art", "Grafika"), ("ArtCraft App Page", "ArtCraft alkalmazásoldal"), ("ArtCraft App Page…", "ArtCraft alkalmazásoldal…"), From 1244e84f61bb6d5fd357167fa863293ba5c125a4 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:01:16 +0100 Subject: [PATCH 21/75] HU1: document the Arrangement translation and targeted locale checks Updated Hungarian catalog entry count to 1,905 and added Data Merge arrangement caption. Minor adjustments made to translation tests and review history.Record 1,905 Hungarian catalog entries and four context captions, preserving historical counts. Clarify that general missing-translation reports are non-blocking while targeted caption regressions can fail tests. --- docs/localization-parity.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 2ca5fe202..137279c90 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (Hungarian catalog test integration; older catalog counts retain their dated measurement) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (Hungarian Data Merge arrangement caption; 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), @@ -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 @@ -63,16 +65,21 @@ Review corrections (2026-10-20) distinguish numbers from counts and text columns columns, use formal Hungarian address consistently, and align translated menu paths with their visible labels. These corrections do not add new catalog keys or change coverage. Test integration (2026-10-23) registers Hungarian in the shared catalog checks and restores -assertions from both merge parents. The catalog now has **1,904 entries and four context +assertions from both merge parents. That snapshot has **1,904 entries and four context captions** after removing 11 untranslated interface-language names (the selector displays their native names directly) and two unused internal tint keys. Seventeen live UI labels are retained and added to the integrity test's known-label list. No used Hungarian translation was removed. +Data Merge follow-up (2026-10-25): `Arrangement` is translated as `Elrendezés`, bringing the +Hungarian catalog to **1,905 entries and four context captions**. The shared all-language +arrangement check and the Hungarian catalog and toolbar tests are retained alongside the +main-branch egui API update. This is a targeted addition, not a new coverage measurement. + ## Other shipped languages | Language | Code | UI strings translated | Status | |---|---|---|---| -| Hungarian | hu | 1,904 entries + 4 context captions (2026-10-23); initial native review | partial | +| Hungarian | hu | 1,905 entries + 4 context captions (2026-10-25); 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 | @@ -106,6 +113,7 @@ locale shaping. See [typography-parity.md](typography-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-25 | minor | Hungarian Data Merge arrangement caption added; 1,905 entries and four context captions; both merge parents' tests retained | | 2026-10-20 | minor | Hungarian review: four context captions, formal address, consistent menu paths; corrected other-language count to six | | 2026-10-17 | minor | Hungarian (`hu`) interface added: 1,917 entries and two context captions; initial native-speaker review | | 2026-10-23 | minor | Hungarian test integration restored; 1,904 active catalog entries and four context captions after removing unused keys | From 09ed93579c4080d529c79a389adf78828315a01d Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:02:22 +0100 Subject: [PATCH 22/75] HU1: record conflict resolution and Arrangement localization Updated progress log with new localization and regression test details for HU1.Preserve every progress entry from both branches and record the Hungarian Arrangement caption, updated egui button role, and retained localization and toolbar tests. --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index 21e7b8915..8178c82e4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -380,6 +380,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log - **2026-10-25** · HU1: extend Hungarian toolbar regression coverage through real right-click flyouts in both column layouts, checking translated row labels, painted shortcuts, clipping and overlap. Existing assertions and runtime behavior are unchanged. No readiness percentage change. +- **2026-10-25** · HU1: retain both branches' localization and toolbar regression tests while adopting the updated egui button role; translate Data Merge Arrangement as Elrendezés (1,905 Hungarian entries and four context captions). No readiness percentage change. - **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. - 2026-10-23 (HU1): repair localization test integration after merging main: restore separate all-catalog integrity, context-caption and numeral/count tests, retaining assertions from both branches; register Hungarian in the shared language and context catalogs. Retain 17 live labels missing from the known-label test list and remove 13 unused catalog entries (1,904 entries + four context captions remain). From 9c67851892c8a49a8d19b731c4c896c0c6974296 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:20:54 +0100 Subject: [PATCH 23/75] HU1: complete panel test imports after main integration Restore the explicit names required by the upstream panel-docking tests and format the imports. All test bodies and assertions are byte-for-byte unchanged. fmt, pdf-deps and clippy pass; 3,375 tests pass, with four font/Georgian findings documented in the PR and six ignored. The remaining i18n-report, doctest, assets, layers and 16 WASM checks pass. One worker under a 2 GiB memory guard. --- crates/ui-egui/src/panel_docking_tests.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 9de7a41f7..44fdc1a39 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -1,5 +1,8 @@ -use super::{DesignApp, Layout, Node, cancel_gesture, json, valid}; -use crate::panel_docking::{area_id, close_visible, default_layout, dock_columns, hit_id, icon_column, in_iconic_column, layout_of, modal_open, open_window_drops, panel_menu, set_iconic, showing, tab_group, tab_id, window_groups}; +use super::{Column, DesignApp, Floating, Layout, Node, Side, Value, cancel_gesture, json, valid}; +use crate::panel_docking::{ + CANVAS, DOCK_LABELS, ICON_GROUPS, PanelContent, Placed, area_id, close_visible, default_layout, dock_columns, hit_id, icon_column, + in_iconic_column, layout_of, modal_open, open_window_drops, panel_menu, set_iconic, showing, tab_group, tab_id, window_groups, +}; use egui::{Event, Modifiers, PointerButton, Pos2, vec2}; use egui_kittest::{Harness, kittest::Queryable}; From de8825dd7c30131d0ef9a3623e6dca2f4c7272f3 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:34:24 +0100 Subject: [PATCH 24/75] HU1: align equivalent test imports with the upstream repair Adopt upstream blob 73f98f61 for panel_docking_tests.rs to avoid a parallel-fix conflict. The same 31 names are imported from the same parent module; every byte after the imports is unchanged. cargo fmt --all -- --check and cargo clippy --workspace --all-targets -- -D warnings both pass. No new main-branch changes are merged. --- crates/ui-egui/src/panel_docking_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 44fdc1a39..73f98f612 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -1,7 +1,7 @@ -use super::{Column, DesignApp, Floating, Layout, Node, Side, Value, cancel_gesture, json, valid}; -use crate::panel_docking::{ - CANVAS, DOCK_LABELS, ICON_GROUPS, PanelContent, Placed, area_id, close_visible, default_layout, dock_columns, hit_id, icon_column, - in_iconic_column, layout_of, modal_open, open_window_drops, panel_menu, set_iconic, showing, tab_group, tab_id, window_groups, +use super::{ + CANVAS, Column, DOCK_LABELS, DesignApp, Floating, ICON_GROUPS, Layout, Node, PanelContent, Placed, Side, Value, area_id, cancel_gesture, + close_visible, default_layout, dock_columns, hit_id, icon_column, in_iconic_column, json, layout_of, modal_open, open_window_drops, panel_menu, + set_iconic, showing, tab_group, tab_id, valid, window_groups, }; use egui::{Event, Modifiers, PointerButton, Pos2, vec2}; use egui_kittest::{Harness, kittest::Queryable}; From a6f1081db8f7f7e5ebed92f1c26733043fe7854a Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:22:52 +0100 Subject: [PATCH 25/75] HU1: complete the two missing Georgian menu captions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Georgian translations for File Info… and Check for Updates…. Keep the complete-menu assertion unchanged; interface_language_switches_and_validates now passes. The Hungarian regressions also pass. The full test stage records 3,376 passed, three previously documented missing-glyph failures and six ignored. --- crates/ui-egui/src/i18n/ka.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index e4136880b..68323c1d7 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -5,6 +5,7 @@ //! the code or the command table use. pub(super) const TABLE: &[(&str, &str)] = &[ ("File", "ფაილი"), + ("File Info…", "ფაილის ინფორმაცია…"), ("Edit", "რედაქტირება"), ("Layout", "განლაგება"), ("Type", "ტექსტი"), @@ -431,6 +432,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Ungroup Color Group", "ფერების ჯგუფის დაშლა"), ("Unmerge", "შერწყმის გაუქმება"), ("Update", "განახლება"), + ("Check for Updates…", "განახლებების შემოწმება…"), ("Update Numbering", "დანომვრის განახლება"), ("Use Global Light", "გლობალური განათების გამოყენება"), ("User Dictionary", "მომხმარებლის ლექსიკონი"), From 0317c797587364a75d086fc467f4193d7a16f091 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:31:51 +0100 Subject: [PATCH 26/75] HU1: document the Georgian menu caption correction Document the two added Georgian menu captions and the unchanged complete-menu regression. Preserve historical coverage counts and make the separate native-speaker review status explicit. --- docs/localization-parity.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 137279c90..d430085df 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (Hungarian Data Merge arrangement caption; older catalog counts retain their dated measurement) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (Hungarian arrangement and Georgian menu captions; 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), @@ -75,6 +75,11 @@ Hungarian catalog to **1,905 entries and four context captions**. The shared all arrangement check and the Hungarian catalog and toolbar tests are retained alongside the main-branch egui API update. This is a targeted addition, not a new coverage measurement. +Georgian menu follow-up (2026-10-25): `File Info…` and `Check for Updates…` now have +Georgian catalog entries. The existing complete-menu translation assertion is unchanged. +This targeted addition retains the dated coverage figures below; the two new captions have +not had a separate native-speaker review. + ## Other shipped languages | Language | Code | UI strings translated | Status | @@ -114,6 +119,7 @@ locale shaping. See [typography-parity.md](typography-parity.md). | Date | Change | Summary | |---|---|---| | 2026-10-25 | minor | Hungarian Data Merge arrangement caption added; 1,905 entries and four context captions; both merge parents' tests retained | +| 2026-10-25 | minor | Georgian File Info and Check for Updates captions added; complete-menu regression retained | | 2026-10-20 | minor | Hungarian review: four context captions, formal address, consistent menu paths; corrected other-language count to six | | 2026-10-17 | minor | Hungarian (`hu`) interface added: 1,917 entries and two context captions; initial native-speaker review | | 2026-10-23 | minor | Hungarian test integration restored; 1,904 active catalog entries and four context captions after removing unused keys | From 0d001fbae67633ecf3370f369f49850722823441 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:32:35 +0100 Subject: [PATCH 27/75] HU1: record the Georgian menu regression fix Record the two Georgian menu captions and preservation of the existing complete-menu regression. Retain all other roadmap entries and readiness percentages. --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index 8178c82e4..c274f19a0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -381,6 +381,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m - **2026-10-25** · HU1: extend Hungarian toolbar regression coverage through real right-click flyouts in both column layouts, checking translated row labels, painted shortcuts, clipping and overlap. Existing assertions and runtime behavior are unchanged. No readiness percentage change. - **2026-10-25** · HU1: retain both branches' localization and toolbar regression tests while adopting the updated egui button role; translate Data Merge Arrangement as Elrendezés (1,905 Hungarian entries and four context captions). No readiness percentage change. +- **2026-10-25** · HU1: add the two missing Georgian File Info and Check for Updates menu captions while retaining the complete-menu regression unchanged. No readiness percentage change. - **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. - 2026-10-23 (HU1): repair localization test integration after merging main: restore separate all-catalog integrity, context-caption and numeral/count tests, retaining assertions from both branches; register Hungarian in the shared language and context catalogs. Retain 17 live labels missing from the known-label test list and remove 13 unused catalog entries (1,904 entries + four context captions remain). From 450789d627c865c488dd3826afc4568b536bb6d0 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:05:17 +0100 Subject: [PATCH 28/75] HU1: align the moved panel test path for the module merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream d77fdf2d moved these tests to crates/ui-egui/src/panel_docking/tests.rs. The tested local merge preserves every test body from both parents there. Remove the obsolete path to unblock GitHub’s web conflict resolver; the remaining module integration follows immediately. --- crates/ui-egui/src/panel_docking_tests.rs | 1281 --------------------- 1 file changed, 1281 deletions(-) delete mode 100644 crates/ui-egui/src/panel_docking_tests.rs diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs deleted file mode 100644 index 73f98f612..000000000 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ /dev/null @@ -1,1281 +0,0 @@ -use super::{ - CANVAS, Column, DOCK_LABELS, DesignApp, Floating, ICON_GROUPS, Layout, Node, PanelContent, Placed, Side, Value, area_id, cancel_gesture, - close_visible, default_layout, dock_columns, hit_id, icon_column, in_iconic_column, json, layout_of, modal_open, open_window_drops, panel_menu, - set_iconic, showing, tab_group, tab_id, valid, window_groups, -}; -use egui::{Event, Modifiers, PointerButton, Pos2, vec2}; -use egui_kittest::{Harness, kittest::Queryable}; - -fn app() -> DesignApp { - DesignApp::new(designcraft_engine::Session::new(), Default::default()) -} - -#[test] -fn panel_commands_group_split_reorder_float_and_restore_without_duplicate_ownership() { - let mut app = app(); - app.run("window.panel.move", json!({"panel":"swatches", "anchor":"layers", "zone":"top"})).unwrap(); - app.run("window.panel.move", json!({"panel":"properties", "anchor":"swatches", "before":"swatches"})).unwrap(); - let before_float = app.ui.docking.clone().unwrap(); - app.run("window.panel.float", json!({"panel":"properties", "x":50,"y":70,"width":330,"height":410})).unwrap(); - assert!(app.ui.docking.as_ref().unwrap().floating.iter().flat_map(window_groups).any(|g| g.panels == ["properties"])); - app.run("window.panel.dock", json!({"panel":"properties"})).unwrap(); - assert_eq!(app.ui.docking.as_ref().unwrap(), &before_float); - app.run("window.panel.close", json!({"panel":"properties"})).unwrap(); - assert!(!app.ui.docking.as_ref().unwrap().contains(&"properties".into())); - app.run("window.panel.activate", json!({"panel":"properties"})).unwrap(); - assert_eq!(app.ui.docking.as_ref().unwrap(), &before_float); - let layout = app.ui.docking.as_ref().unwrap(); - assert!(valid(layout)); - let unique: std::collections::HashSet<_> = layout.panels().into_iter().collect(); - assert_eq!(unique.len(), layout.panels().len()); - let saved = serde_json::to_value(&app.ui).unwrap(); - let restored: crate::UiState = serde_json::from_value(saved).unwrap(); - assert_eq!(restored.docking, app.ui.docking); - assert_eq!(restored.docking_hidden, app.ui.docking_hidden); -} - -#[test] -fn invalid_panel_requests_are_atomic_and_do_not_enter_a_custom_workspace() { - let mut app = app(); - for (id, params) in [ - ("window.panel.move", json!({"panel":"layers","anchor":"unknown"})), - ("window.panel.move", json!({"panel":"layers","anchor":"properties","before":"unknown"})), - ("window.panel.move", json!({"panel":"layers","anchor":"properties","zone":42})), - ("window.panel.float", json!({"panel":"layers","width":-10})), - ("window.panel.float", json!({"panel":"unknown"})), - ("window.panel.float", json!({"panel":"layers","x":"invalid"})), - ] { - let old = app.ui.docking.clone(); - let hidden = app.ui.docking_hidden.clone(); - assert!(app.run(id, params).is_err(), "{id}"); - assert_eq!(app.ui.docking, old); - assert_eq!(app.ui.docking_hidden, hidden); - } -} - -fn harness(app: DesignApp, size: egui::Vec2) -> Harness<'static, DesignApp> { - let mut ready = false; - let mut harness = Harness::builder().with_size(size).with_step_dt(1.0 / 60.0).build_ui_state( - move |ui, app: &mut DesignApp| { - if !ready { - crate::theme::install_fonts(ui.ctx(), &app.ui.language); - crate::theme::apply(ui.ctx(), &crate::theme::Tokens::for_brightness(crate::theme::Brightness::MediumDark)); - ready = true; - return; - } - egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| crate::panel_docking::show(app, ui)); - }, - app, - ); - harness.run_steps(4); - harness -} - -fn drag(h: &mut Harness<'static, DesignApp>, from: Pos2, to: Pos2) { - h.event(Event::PointerMoved(from)); - h.run_steps(1); - h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - for step in 1..=8 { - h.event(Event::PointerMoved(from + (to - from) * (step as f32 / 8.0))); - h.run_steps(1); - } - h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(3); -} - -#[test] -fn ordinary_panel_tab_drag_enters_shared_docking_and_can_redock_into_another_tab_group() { - let mut h = harness(app(), vec2(1280.0, 800.0)); - let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center(); - drag(&mut h, source, egui::pos2(160.0, 140.0)); - let layout = h.state().ui.docking.as_ref().expect("ordinary tab drag starts shared docking"); - assert!(layout.floating.iter().flat_map(window_groups).any(|g| g.panels.contains(&"layers".into())), "{layout:?}"); - let source = h.ctx.read_response(tab_id("layers")).unwrap().rect.center(); - let destination = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); - drag(&mut h, source, destination); - let layout = h.state().ui.docking.as_ref().unwrap(); - assert!(!layout.floating.iter().flat_map(window_groups).any(|g| g.panels.contains(&"layers".into()))); - assert!(valid(layout)); - assert_eq!(h.state().ui.docking_focused.as_deref(), Some("layers"), "the user's drag keeps the focus on the panel it moved"); -} - -#[test] -fn saved_workspace_restores_the_custom_tree_and_hidden_panel_placement() { - let mut app = app(); - - app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap(); - app.run("window.panel.float", json!({"panel":"properties","x":50,"y":80})).unwrap(); - app.run("window.panel.close", json!({"panel":"swatches"})).unwrap(); - let layout = app.ui.docking.clone(); - let hidden = app.ui.docking_hidden.clone(); - app.run("window.newWorkspace", json!({"name":"Custom docking"})).unwrap(); - app.run("window.workspace", json!({"name":"Essentials"})).unwrap(); - assert!(app.ui.docking.is_none()); - app.run("window.workspace", json!({"name":"Custom docking"})).unwrap(); - assert_eq!(app.ui.docking, layout); - assert_eq!(app.ui.docking_hidden, hidden); -} - -/// Explicit offscreen evidence; this creates no native windows. -#[test] -fn capture_custom_panel_docking_visual_fixtures() { - let Some(directory) = std::env::var_os("CRAFT_UI_DOCKING_FIXTURES").map(std::path::PathBuf::from) else { return }; - std::fs::create_dir_all(&directory).unwrap(); - for theme in crate::theme::Brightness::ALL { - for width in [800.0, 1280.0] { - for scale in [1.0, 1.5, 2.0] { - let mut app = app(); - app.run("file.new", json!({})).unwrap(); - app.run("frame.create", json!({"rect":[40,40,280,180],"content":"text","text":"Synthetic layout — مرحبا"})).unwrap(); - let language = if width == 800.0 && scale == 1.0 { "ar" } else { "en" }; - app.run("app.language", json!({"lang":if language == "en" { "" } else { language }})).unwrap(); - app.run("window.panel.move", json!({"panel":"swatches", "anchor":"layers", "zone":"top"})).unwrap(); - app.run("window.panel.float", json!({"panel":"properties","x":30,"y":90,"width":300,"height":440})).unwrap(); - let mut ready = false; - let mut harness = Harness::builder().with_size(vec2(width, 800.0)).with_pixels_per_point(scale).wgpu().build_ui_state( - move |ui, app: &mut DesignApp| { - if !ready { - crate::theme::install_fonts(ui.ctx(), &app.ui.language); - crate::theme::apply(ui.ctx(), &crate::theme::Tokens::for_brightness(theme)); - ready = true; - return; - } - egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| crate::panel_docking::show(app, ui)); - }, - app, - ); - harness.input_mut().max_texture_side = Some(8192); - harness.run_steps(5); - assert!(valid(harness.state().ui.docking.as_ref().unwrap())); - harness - .render() - .unwrap() - .save(directory.join(format!("designcraft-shared-docking-{}-{language}-{width}-{scale}x.png", theme.id()))) - .unwrap(); - if language == "ar" { - let tab = tab_id("swatches"); - let at = harness.ctx.read_response(tab).unwrap().rect.center(); - harness.input_mut().events.extend([ - Event::PointerMoved(at), - Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE }, - ]); - harness.run_steps(1); - harness.input_mut().events.push(Event::PointerButton { - pos: at, - button: PointerButton::Secondary, - pressed: false, - modifiers: Modifiers::NONE, - }); - harness.run_steps(2); - assert!(egui::Popup::is_any_open(&harness.ctx), "actual tab context menu must be open"); - for action in ["Float Panel", "Group with", "Close Panel"] { - assert!(harness.query_all_by_label(crate::i18n::tr("ar", action)).count() >= 1, "the menu lists {action}"); - } - harness.render().unwrap().save(directory.join(format!("designcraft-dock-context-{}-ar.png", theme.id()))).unwrap(); - } - } - } - } -} - -#[test] -fn escape_cancels_a_tab_drag_without_customizing_the_workspace() { - let mut h = harness(app(), vec2(1280.0, 800.0)); - let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center(); - h.event(Event::PointerMoved(source)); - h.run_steps(1); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerMoved(source - vec2(90.0, 0.0))); - h.run_steps(2); - h.event(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }); - h.run_steps(1); - assert!(h.state().ui.docking.is_none()); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - assert!(h.state().ui.docking.is_none()); -} - -#[test] -fn serialized_layout_actions_cover_geometry_reorder_accordion_and_atomic_rejection() { - let mut app = app(); - app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap(); - app.run("window.panel.layout", json!({"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}})).unwrap(); - assert!( - matches!(app.ui.docking.as_ref().unwrap().root, Some(Node::Split { size: craft_ui::layout::SplitSize::Ratio(value), .. }) if (value - 0.35).abs() < 0.001) - ); - app.run("window.panel.float", json!({"panel":"properties"})).unwrap(); - app.run("window.panel.layout", json!({"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}})).unwrap(); - assert_eq!( - app.ui.docking.as_ref().unwrap().floating.iter().find(|window| window.panels().any(|p| p == "properties")).unwrap().rect, - [44.0, 66.0, 360.0, 450.0] - ); - app.run("window.panel.layout", json!({"action":{"Move":{"panel":"properties","anchor":"swatches","placement":{"Tab":{"before":"swatches"}}}}})) - .unwrap(); - assert_eq!(group_members_for_test(app.ui.docking.as_ref().unwrap(), "swatches"), ["properties", "swatches"]); - let before = app.ui.docking.clone(); - let hidden = app.ui.docking_hidden.clone(); - for action in [ - json!({"MoveFloating":{"panel":"properties","rect":[0,0,-5,30]}}), - json!({"ResizeSplit":{"path":[true,true,true],"size":{"Ratio":0.2}}}), - json!({"Open":{"panel":"unknown","anchor":null}}), - json!({"Move":{"panel":"properties","anchor":"swatches","placement":{"Tab":{"before":"unknown"}}}}), - ] { - assert!(app.run("window.panel.layout", json!({"action":action})).is_err()); - assert_eq!(app.ui.docking, before); - assert_eq!(app.ui.docking_hidden, hidden); - } - let stack = Node::Stack { entries: vec![craft_ui::docking::StackEntry { panel: "layers".into(), open: true, height: Some(100.0) }] }; - app.ui.docking = Some(Layout { - root: Some(Node::Split { - axis: craft_ui::layout::SplitAxis::Horizontal, - size: craft_ui::layout::SplitSize::Ratio(0.7), - first: Box::new(Node::Tabs { panels: vec![CANVAS.into()], active: 0 }), - second: Box::new(stack), - }), - floating: Vec::new(), - }); - app.run("window.panel.layout", json!({"action":{"SetStackOpen":{"panel":"layers","open":false}}})).unwrap(); - app.run("window.panel.layout", json!({"action":{"ResizeStack":{"panel":"layers","height":150}}})).unwrap(); - let Some(Node::Split { second, .. }) = &app.ui.docking.as_ref().unwrap().root else { panic!("expected split") }; - let Node::Stack { entries } = &**second else { panic!("expected stack") }; - assert!(!entries[0].open); - assert_eq!(entries[0].height, Some(150.0)); -} - -fn group_members_for_test(layout: &Layout, panel: &str) -> Vec { - tab_group(layout, panel) -} - -#[test] -fn populated_canvas_keeps_a_real_stroke_edit_after_floating_and_redocking() { - let mut app = app(); - app.run("file.new", json!({"width":480,"height":360})).unwrap(); - let id = app.run("frame.create", json!({"rect":[80,70,330,230],"content":"none"})).unwrap()["id"].as_u64().unwrap(); - app.run("object.fill", json!({"swatch":"C=100 M=0 Y=0 K=0"})).unwrap(); - app.run("object.stroke", json!({"weight":8})).unwrap(); - app.run("window.panel.float", json!({"panel":"stroke","x":40,"y":140,"width":310,"height":490})).unwrap(); - let directory = std::env::var_os("CRAFT_UI_DOCKING_FIXTURES").map(std::path::PathBuf::from); - let builder = Harness::builder().with_size(vec2(1280.0, 800.0)); - let builder = if directory.is_some() { builder.wgpu() } else { builder }; - let mut h = builder.build_ui_state( - |ui, app: &mut DesignApp| { - app.logic(ui.ctx()); - app.ui(ui); - }, - app, - ); - h.input_mut().max_texture_side = Some(8192); - h.run_steps(5); - let at = h.get_by_label("Bevel").rect().center(); - h.hover_at(at); - h.run_steps(1); - for pressed in [true, false] { - h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); - h.run_steps(1); - } - h.run_steps(2); - assert_eq!(h.state().session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().stroke.join, designcraft_doc::Join::Bevel); - if let Some(directory) = &directory { - std::fs::create_dir_all(directory).unwrap(); - h.render().unwrap().save(directory.join("designcraft-populated-stroke-floating.png")).unwrap(); - } - let from = h.ctx.read_response(tab_id("stroke")).unwrap().rect.center(); - let to = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); - drag(&mut h, from, to); - assert!(h.state().ui.docking.as_ref().unwrap().floating.is_empty()); - assert_eq!(h.state().session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().stroke.join, designcraft_doc::Join::Bevel); - if let Some(directory) = &directory { - h.render().unwrap().save(directory.join("designcraft-populated-stroke-redocked.png")).unwrap(); - } -} - -#[test] -fn properties_stroke_link_opens_the_stroke_panel_in_a_custom_workspace() { - let mut app = app(); - app.run("file.new", json!({"width":480,"height":360})).unwrap(); - app.run("frame.create", json!({"rect":[80,70,330,230],"content":"none"})).unwrap(); - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - // Floating Layers leaves Pages the active tab beside Properties. - app.run("window.panel", json!({"panel":"properties"})).unwrap(); - assert!(in_iconic_column(app.ui.docking.as_ref().unwrap(), "stroke")); - let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state( - |ui, app: &mut DesignApp| { - app.logic(ui.ctx()); - app.ui(ui); - }, - app, - ); - h.run_steps(5); - let at = h.get_by_role_and_label(egui::accesskit::Role::Link, "Stroke").rect().center(); - h.hover_at(at); - h.run_steps(1); - for pressed in [true, false] { - h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); - h.run_steps(1); - } - h.run_steps(2); - // Stroke stays in the iconic column and opens as its flyout. - assert!(in_iconic_column(h.state().ui.docking.as_ref().unwrap(), "stroke")); - assert_eq!(h.state().ui.docking_flyout.as_deref(), Some("stroke")); - assert!(showing(h.state(), "stroke")); - assert_eq!(h.state().ui.docking_opened.as_deref(), Some("stroke")); -} - -#[test] -fn changing_workspace_during_a_drag_cancels_the_stale_gesture() { - let mut app = app(); - app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap(); - let destination = app.ui.docking.clone(); - app.run("window.newWorkspace", json!({"name":"Destination"})).unwrap(); - app.run("window.workspace", json!({"name":"Essentials"})).unwrap(); - let mut h = harness(app, vec2(1280.0, 800.0)); - let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center(); - h.event(Event::PointerMoved(source)); - h.run_steps(1); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerMoved(source - vec2(90.0, 0.0))); - h.run_steps(2); - h.state_mut().run("window.workspace", json!({"name":"Destination"})).unwrap(); - h.event(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }); - h.run_steps(1); - assert_eq!(h.state().ui.docking, destination); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - assert_eq!(h.state().ui.docking, destination); -} - -#[test] -fn first_customization_keeps_the_panel_the_workspace_opened() { - let mut app = app(); - app.ui.open_panel = Some("paragraphStyles".into()); - app.ui.dock_expanded = false; - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - let layout = app.ui.docking.as_ref().unwrap(); - assert!(layout.contains(&"paragraphStyles".into())); - assert!(valid(layout)); - assert_eq!(layout.panels().iter().filter(|p| p.as_str() == "paragraphStyles").count(), 1); -} - -#[test] -fn hiding_before_first_destination_frame_does_not_restore_an_old_drag_snapshot() { - let mut app = app(); - app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap(); - app.run("window.newWorkspace", json!({"name":"Destination"})).unwrap(); - app.run("window.workspace", json!({"name":"Essentials"})).unwrap(); - let mut h = harness(app, vec2(1280.0, 800.0)); - let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center(); - h.event(Event::PointerMoved(source)); - h.run_steps(1); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerMoved(source - vec2(90.0, 0.0))); - h.run_steps(2); - h.state_mut().run("window.workspace", json!({"name":"Destination"})).unwrap(); - let destination = h.state().ui.docking.clone(); - let open_panel = h.state().ui.open_panel.clone(); - let floating = h.state().ui.floating.clone(); - // The app calls this before dock rendering when panels are hidden or Presentation starts. - let ctx = h.ctx.clone(); - cancel_gesture(&ctx); - assert_eq!(h.state().ui.docking, destination); - assert_eq!(h.state().ui.open_panel, open_panel); - assert_eq!(h.state().ui.floating, floating); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - assert_eq!(h.state().ui.docking, destination); -} - -#[test] -fn table_and_control_visibility_work_after_customization_without_document_edits() { - let mut app = app(); - app.run("file.new", json!({})).unwrap(); - let doc = serde_json::to_value(&app.session.active().unwrap().doc).unwrap(); - let undo = app.session.active().unwrap().history.undo.len(); - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - app.run("app.tablePanel", json!({})).unwrap(); - assert!(showing(&app, "table")); - // The Table flyout closes; Table stays in its icon group. - app.run("app.tablePanel", json!({})).unwrap(); - assert!(!showing(&app, "table")); - assert!(in_iconic_column(app.ui.docking.as_ref().unwrap(), "table")); - app.run("app.tablePanel", json!({})).unwrap(); - assert!(showing(&app, "table")); - let ctx = egui::Context::default(); - let (req, _) = crate::control::ControlRequest::new("ui.set", json!({"closePanel":true,"dockExpanded":false})); - let crate::control::Outcome::Done(result) = crate::control::handle(&mut app, &ctx, &req) else { panic!("expected synchronous control result") }; - assert_eq!(result["ok"], true); - assert!(!app.ui.dock_expanded); - assert!(!app.ui.docking.as_ref().unwrap().contains(&"table".into())); - app.run("window.panel", json!({"panel":"properties"})).unwrap(); - assert_eq!(app.ui.docking_flyout.as_deref(), Some("properties"), "it opens as a flyout from the collapsed dock"); - assert_eq!(app.ui.docking_raise.as_deref(), Some("properties")); - set_iconic(&mut app, false).unwrap(); - assert_eq!(app.ui.docking_flyout, None, "expanding the column closes its flyout"); - assert_eq!(serde_json::to_value(&app.session.active().unwrap().doc).unwrap(), doc); - assert_eq!(app.session.active().unwrap().history.undo.len(), undo); -} - -#[test] -fn floating_activation_queues_one_transient_raise_and_workspace_switch_clears_it() { - let mut app = app(); - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - app.run("window.panel.activate", json!({"panel":"layers"})).unwrap(); - assert_eq!(app.ui.docking_raise.as_deref(), Some("layers")); - assert!(serde_json::to_value(&app.ui).unwrap().get("docking_raise").is_none()); - app.run("window.workspace", json!({"name":"Typography"})).unwrap(); - assert!(app.ui.docking_raise.is_none()); -} - -#[test] -fn shaped_panel_label_keeps_the_logical_accessibility_text() { - let ctx = egui::Context::default(); - let logical = "خصائص Pages"; - crate::theme::install_fonts(&ctx, "ar"); - let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { - let galley = crate::rtl::plain(ui.ctx(), logical, egui::FontId::proportional(12.0), egui::Color32::WHITE); - assert_eq!(galley.job.text, logical); - }); - output.textures_delta.clear(); -} - -#[test] -fn modal_overlay_blocks_a_real_tab_drag_release() { - let mut app = app(); - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - let mut h = harness(app, vec2(1280.0, 800.0)); - let source = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); - h.event(Event::PointerMoved(source)); - h.run_steps(1); - h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerMoved(source - vec2(90.0, 0.0))); - h.run_steps(2); - assert_eq!(h.ctx.dragged_id(), Some(tab_id("properties")), "test must start the owned tab gesture"); - let before = h.state().ui.docking.clone(); - h.state_mut().ui.dialog = Some(crate::dialogs::Dialog::new("newDocument", json!({}))); - let destination = egui::pos2(150.0, 150.0); - h.event(Event::PointerMoved(destination)); - h.run_steps(1); - h.event(Event::PointerButton { pos: destination, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - assert_eq!(h.state().ui.docking, before, "release under a modal must not float or dock the panel"); - h.state_mut().ui.dialog = None; - h.run_steps(2); - assert_eq!(h.state().ui.docking, before, "closing the overlay must not revive its blocked drag"); -} - -#[test] -fn shift_f9_toggles_table_after_customization_through_the_real_shortcut_path() { - let mut app = app(); - app.run("file.new", json!({})).unwrap(); - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - let before = serde_json::to_value(&app.session.active().unwrap().doc).unwrap(); - let mut h = crate::test_window::open(app, vec2(1440.0, 900.0)); - for visible in [true, false] { - let modifiers = Modifiers { shift: true, ..Modifiers::NONE }; - h.event(Event::Key { key: egui::Key::F9, physical_key: None, pressed: true, repeat: false, modifiers }); - h.run_steps(1); - h.event(Event::Key { key: egui::Key::F9, physical_key: None, pressed: false, repeat: false, modifiers }); - h.run_steps(2); - assert_eq!(showing(&h.state().app, "table"), visible); - assert!(h.state().app.ui.docking.as_ref().unwrap().contains(&"table".into()), "Table stays in its icon group"); - } - assert_eq!(serde_json::to_value(&h.state().app.session.active().unwrap().doc).unwrap(), before); -} - -#[test] -fn first_story_editor_frame_conservatively_blocks_owned_tab_release() { - let mut app = app(); - app.run("file.new", json!({})).unwrap(); - let story = app.run("frame.create", json!({"rect":[72,72,300,200],"content":"text","text":"Story editor overlay fixture"})).unwrap()["story"] - .as_u64() - .unwrap(); - app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); - let doc = serde_json::to_value(&app.session.active().unwrap().doc).unwrap(); - let mut h = crate::test_window::open(app, vec2(1440.0, 900.0)); - let tab = tab_id("properties"); - let from = h.ctx.read_response(tab).unwrap().rect.center(); - h.event(Event::PointerMoved(from)); - h.run_steps(1); - h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerMoved(from - vec2(90.0, 0.0))); - h.run_steps(2); - assert_eq!(h.ctx.dragged_id(), Some(tab), "must own a real tab drag before opening editor"); - assert!(h.ctx.memory(|m| m.area_rect(egui::Id::unique("story_editor"))).is_none()); - let layout = h.state().app.ui.docking.clone(); - let frame_before = h.ctx.cumulative_frame_nr(); - h.state_mut().app.run("app.storyEditor", json!({"story":story})).unwrap(); - // The unmeasured first frame blocks the full content, even outside the eventual editor. - let release = egui::pos2(150.0, 150.0); - h.input_mut().events.extend([ - Event::PointerMoved(release), - Event::PointerButton { pos: release, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }, - ]); - h.run_steps(1); - assert_eq!(h.ctx.cumulative_frame_nr() - frame_before, 1, "release must occur in the first editor frame"); - let measured = h.ctx.memory(|m| m.area_rect(egui::Id::unique("story_editor"))).unwrap(); - assert!(!measured.contains(release), "fixture exercises the conservative first-frame guard outside eventual editor bounds"); - assert_eq!(h.state().app.ui.docking, layout, "first editor frame must block release before it has a measured rectangle"); - assert_eq!(serde_json::to_value(&h.state().app.session.active().unwrap().doc).unwrap(), doc); - h.run_steps(3); - let editor = h.ctx.memory(|m| m.area_rect(egui::Id::unique("story_editor"))).unwrap(); - let screen = h.ctx.content_rect(); - let outside = [screen.left_bottom() + vec2(80.0, -80.0), screen.right_bottom() + vec2(-450.0, -80.0)] - .into_iter() - .find(|p| !editor.contains(*p)) - .expect("fixture has space outside editor"); - let before_outside = h.state().app.ui.docking.clone(); - let tab = tab_id("layers"); - let from = h.ctx.read_response(tab).unwrap().rect.center(); - h.event(Event::PointerMoved(from)); - h.run_steps(1); - h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerMoved(from - vec2(90.0, 0.0))); - h.run_steps(2); - assert_eq!(h.ctx.dragged_id(), Some(tab)); - h.event(Event::PointerMoved(outside)); - h.event(Event::PointerButton { pos: outside, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - assert_ne!(h.state().app.ui.docking, before_outside, "measured editor must permit ordinary releases outside its bounds"); - let after = h.state().app.ui.docking.clone(); - h.state_mut().app.run("app.storyEditor", json!({"story":story})).unwrap(); - h.run_steps(3); - assert_eq!(h.state().app.ui.docking, after, "closing editor must not revive a blocked drag"); - assert_eq!(serde_json::to_value(&h.state().app.session.active().unwrap().doc).unwrap(), doc); -} - -#[test] -fn dock_menu_button_opens_the_active_panel_menu() { - let mut app = app(); - app.run("file.new", json!({})).unwrap(); - app.ui.dock_tab = "pages".into(); - let pages = app.session.active().unwrap().doc.page_count(); - let mut h = harness(app, vec2(1280.0, 800.0)); - assert!(h.state().ui.docking.is_none()); - let tab = h.query_all_by_label("Pages").find(|node| node.rect().height() <= 45.0).unwrap().rect(); - // The outer dock column is at the window's right edge; the ≡ ends its tab strip. - let at = egui::pos2(1280.0 - 12.0, tab.center().y); - h.event(Event::PointerMoved(at)); - h.run_steps(1); - for pressed in [true, false] { - h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); - h.run_steps(1); - } - h.run_steps(2); - assert!(egui::Popup::is_any_open(&h.ctx), "the three-line button must open a menu"); - h.get_by_label("Duplicate Spread").click(); - h.run_steps(3); - assert_eq!(h.state().session.active().unwrap().doc.page_count(), pages + 1); - assert!(!egui::Popup::is_any_open(&h.ctx)); -} - -/// InDesign's panel tab menu (Learn Skills Daily's InDesign course, 6:20): Close, Close Tab Group, -/// Minimize and Collapse to Icons. Close Tab Group hides the panels tabbed together. -#[test] -fn close_tab_group_hides_the_panels_tabbed_together() { - let mut app = app(); - // The default dock: Properties, Pages and Layers are one tab group. - let r = app.run("window.panel.closeGroup", json!({"panel":"pages"})).unwrap(); - assert_eq!(r["closed"], json!(["properties", "pages", "layers"])); - let layout = app.ui.docking.clone().unwrap(); - assert!(["properties", "pages", "layers"].iter().all(|p| !layout.contains(&p.to_string()))); - // Window brings one back. - app.run("window.panel.activate", json!({"panel":"pages"})).unwrap(); - assert!(app.ui.docking.as_ref().unwrap().contains(&"pages".to_string())); - // A floating group goes as a whole; the dock keeps its own. - app.run("window.panel.float", json!({"panel":"swatches","x":40,"y":60})).unwrap(); - app.run("window.panel.activate", json!({"panel":"color"})).unwrap(); - app.run("window.panel.move", json!({"panel":"color", "anchor":"swatches"})).unwrap(); - app.run("window.panel.closeGroup", json!({"panel":"color"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(!layout.contains(&"color".to_string()) && !layout.contains(&"swatches".to_string())); - assert!(layout.contains(&"pages".to_string())); - assert!(app.run("window.panel.closeGroup", json!({"panel":"nope"})).is_err()); -} - -#[test] -fn collapse_to_icons_shrinks_the_dock_to_its_icons() { - let mut app = app(); - assert!(app.ui.dock_expanded); - app.run("window.collapseToIcons", json!({})).unwrap(); - assert!(!app.ui.dock_expanded); - // A panel chosen from Window opens as a flyout beside its icon; the dock stays collapsed. - app.run("window.panel", json!({"panel":"pages"})).unwrap(); - assert_eq!(app.ui.docking_flyout.as_deref(), Some("pages")); - assert_eq!(app.ui.docking_raise.as_deref(), Some("pages")); - assert!(showing(&app, "pages")); - assert!(in_iconic_column(app.ui.docking.as_ref().unwrap(), "pages")); - // Choosing it again closes the flyout, and a third time opens it again. - app.run("window.panel", json!({"panel":"pages"})).unwrap(); - assert_eq!(app.ui.docking_flyout, None); - assert!(!showing(&app, "pages")); - app.run("window.panel", json!({"panel":"pages"})).unwrap(); - assert_eq!(app.ui.docking_flyout.as_deref(), Some("pages")); -} - -#[test] -fn a_tabs_context_menu_lists_indesigns_tab_commands_and_close_works() { - let mut h = harness(app(), vec2(1280.0, 800.0)); - let tab = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center(); - h.event(Event::PointerMoved(tab)); - h.run_steps(1); - h.event(Event::PointerButton { pos: tab, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::PointerButton { pos: tab, button: PointerButton::Secondary, pressed: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - // The column bar's » button is also labelled Collapse to Icons. - for item in ["Close", "Close Tab Group", "Minimize", "Collapse to Icons", "Float Panel"] { - assert!(h.query_all_by_label(item).next().is_some(), "{item}"); - } - assert_eq!(h.query_all_by_label("Minimize").count(), 1, "the tab menu lists Minimize once"); - assert!(h.query_all_by_label("Dock Panel").next().is_none(), "a docked panel has no Dock Panel"); - h.get_by_label("Close").click(); - h.run_steps(2); - assert!(!h.state().ui.docking.as_ref().unwrap().contains(&"layers".to_string())); -} - -/// A floating panel's tab menu and three-line menu have Dock Panel, which puts it back where it -/// floated from. -#[test] -fn dock_panel_in_a_floating_panels_menus_returns_it_to_the_dock() { - let mut app = app(); - app.run("window.panel.activate", json!({"panel":"layers"})).unwrap(); - let docked = app.ui.docking.clone().unwrap(); - app.run("window.panel.float", json!({"panel":"layers","x":200,"y":150})).unwrap(); - let mut h = harness(app, vec2(1280.0, 800.0)); - let tab = h.ctx.read_response(tab_id("layers")).unwrap().rect; - click(&mut h, tab.center(), PointerButton::Secondary); - h.get_by_label("Dock Panel").click(); - h.run_steps(3); - assert_eq!(h.state().ui.docking.as_ref(), Some(&docked), "the tab menu docks it back in its group"); - let mut app = h.into_state(); - app.run("window.panel.float", json!({"panel":"layers","x":200,"y":150})).unwrap(); - let mut menu = Harness::builder().with_size(vec2(300.0, 400.0)).build_ui_state(|ui, app: &mut DesignApp| panel_menu(app, ui, "layers"), app); - menu.run_steps(1); - menu.get_by_label("Dock Panel").click(); - menu.run_steps(2); - assert_eq!(menu.state().ui.docking.as_ref(), Some(&docked), "so does the three-line menu"); -} - -/// Window › Styles › Paragraph Styles opens InDesign's floating Character Styles | Paragraph Styles -/// group, which stays open while other panels open (Learn Skills Daily's InDesign course, 7:55). -#[test] -fn paragraph_styles_open_floating_with_character_styles() { - let mut app = app(); - app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - let group = layout.floating.iter().flat_map(window_groups).find(|g| g.panels.iter().any(|p| p == "paragraphStyles")).expect("a floating group"); - assert_eq!(group.panels, ["characterStyles", "paragraphStyles"]); - assert_eq!(group.panels[group.active], "paragraphStyles", "the one chosen is in front"); - // The dock still shows Properties in front. - assert_eq!(layout.panel_state(&"properties".to_string()), craft_ui::docking::PanelState::Front, "{:?}", layout.root); - assert_eq!(layout.floating.len(), 1, "one floating window holds both tabs"); - // Other panels from Window leave it open. - app.run("window.panel", json!({"panel":"color"})).unwrap(); - app.run("window.panel", json!({"panel":"links"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(layout.floating.iter().flat_map(window_groups).any(|g| g.panels == ["characterStyles", "paragraphStyles"])); - // Once showing, Window › Character Styles brings that tab forward. - app.run("window.panel", json!({"panel":"characterStyles"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - let group = layout.floating.iter().flat_map(window_groups).find(|g| g.panels.iter().any(|p| p == "characterStyles")).unwrap(); - assert_eq!(group.panels[group.active], "characterStyles"); - assert_eq!(layout.panels().iter().filter(|p| p.as_str() == "characterStyles").count(), 1); -} - -/// Window › Styles floats the styles icon group out of the iconic column; Dock Panel puts the -/// group back as one, at the same place in that column. -#[test] -fn dock_panel_returns_the_floated_styles_group_to_its_icon_group() { - let mut app = app(); - let docked = app.ui.docking.clone().unwrap_or_else(|| default_layout(&app.ui)); - let styles = ["characterStyles", "paragraphStyles"].map(String::from).to_vec(); - let icon_groups = |layout: &Layout| -> Vec> { - dock_columns(layout).into_iter().filter(|c| c.iconic).flat_map(|c| &c.groups).map(|g| g.panels.clone()).collect() - }; - let before = icon_groups(&docked); - let at = before.iter().position(|g| *g == styles).expect("the styles are an icon group"); - app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(layout.floating.iter().flat_map(window_groups).any(|g| g.panels == styles), "{:?}", layout.floating); - assert!(!icon_groups(&layout).contains(&styles)); - app.run("window.panel.dock", json!({"panel":"paragraphStyles"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(layout.floating.is_empty(), "{:?}", layout.floating); - assert_eq!(icon_groups(&layout), before, "both styles are back in their icon group, at the same place"); - assert_eq!(icon_groups(&layout).get(at), Some(&styles)); - assert!(valid(&layout)); -} - -/// Style panels the user docked and then closed come back in the dock, not floating. -#[test] -fn paragraph_styles_closed_from_the_dock_reopen_there() { - let mut app = app(); - app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); - for panel in ["characterStyles", "paragraphStyles"] { - app.run("window.panel.move", json!({"panel": panel, "anchor": "properties"})).unwrap(); - } - for panel in ["characterStyles", "paragraphStyles"] { - app.run("window.panel.close", json!({"panel": panel})).unwrap(); - } - app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(layout.contains(&"paragraphStyles".to_string())); - assert!(layout.floating.iter().flat_map(window_groups).all(|g| !g.panels.iter().any(|p| p == "paragraphStyles")), "{:?}", layout.floating); -} - -fn click(h: &mut Harness<'static, DesignApp>, at: Pos2, button: PointerButton) { - h.event(Event::PointerMoved(at)); - h.run_steps(1); - for pressed in [true, false] { - h.event(Event::PointerButton { pos: at, button, pressed, modifiers: Modifiers::NONE }); - h.run_steps(1); - } - h.run_steps(2); -} - -/// Window › Paragraph Styles switches to the docking layout. After Window › Pages the Pages panel -/// keeps its three-line panel menu, and its tab menu lists InDesign's tab commands. -#[test] -fn pages_keeps_its_panel_menu_in_the_docking_layout() { - let mut app = app(); - app.run("file.new", json!({})).unwrap(); - app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); - app.run("window.panel", json!({"panel":"pages"})).unwrap(); - assert!(app.ui.docking.is_some()); - let mut h = harness(app, vec2(1280.0, 800.0)); - let tab = h.query_all_by_label("Pages").find(|node| node.rect().height() <= 45.0).unwrap().rect(); - click(&mut h, egui::pos2(1280.0 - 12.0, tab.center().y), PointerButton::Primary); - assert!(egui::Popup::is_any_open(&h.ctx), "the three-line button must open a menu"); - for item in ["Numbering & Section Options…", "Close Tab Group", "Float Panel"] { - assert!(h.query_by_label(item).is_some(), "{item}"); - } - h.get_by_label("Panel Options…").click(); - h.run_steps(3); - assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "pagesPanelOptions")); - h.state_mut().ui.dialog = None; - h.run_steps(2); - - click(&mut h, tab.center(), PointerButton::Secondary); - for item in ["Close", "Close Tab Group", "Minimize", "Collapse to Icons", "Float Panel", "Group with"] { - assert!(h.query_all_by_label(item).next().is_some(), "{item}"); - } - assert!(h.query_by_label("Close Panel").is_none()); - assert!(h.query_by_label("Panel Options…").is_none(), "the panel's own commands stay in its panel menu"); -} - -fn group_with(layout: &Layout, panel: &str) -> (bool, Vec, usize) { - if let Some(g) = layout.floating.iter().flat_map(window_groups).find(|g| g.panels.iter().any(|p| p == panel)) { - return (true, g.panels, g.active); - } - let mut visit: Vec<&Node> = layout.root.iter().collect(); - while let Some(node) = visit.pop() { - match node { - Node::Split { first, second, .. } => visit.extend([&**first, &**second]), - Node::Tabs { panels, active } if panels.iter().any(|p| p == panel) => return (false, panels.clone(), *active), - Node::Dock { columns, .. } => { - if let Some(g) = columns.iter().flat_map(|c| &c.groups).find(|g| g.panels.iter().any(|p| p == panel)) { - return (false, g.panels.clone(), g.active); - } - } - _ => {} - } - } - panic!("{panel} is not in {layout:?}"); -} - -fn drag_bar(h: &Harness<'static, DesignApp>, first: &str) -> egui::Rect { - h.ctx.read_response(hit_id(&craft_ui::docking::Hit::DragBar(first.to_string()))).expect("the group's drag bar").rect -} - -/// The docked Character Styles | Paragraph Styles group moves as one by its drag bar: dropped on -/// the canvas it floats, dropped on another strip it joins as tabs. Order and the tab in front -/// are kept. -#[test] -fn dragging_a_docked_group_by_its_bar_moves_every_tab() { - let mut app = app(); - app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap(); - let below = json!({"MoveGroup": {"panel": "characterStyles", "to": {"At": {"anchor": "properties", "placement": {"Group": {"below": true}}}}}}); - app.run("window.panel.layout", json!({"action": below})).unwrap(); - let styles = ["characterStyles", "paragraphStyles"]; - assert_eq!(group_with(app.ui.docking.as_ref().unwrap(), "characterStyles"), (false, styles.map(String::from).to_vec(), 1)); - - let mut h = harness(app, vec2(1280.0, 800.0)); - let bar = drag_bar(&h, "characterStyles"); - drag(&mut h, bar.center(), egui::pos2(400.0, 400.0)); - let layout = h.state().ui.docking.as_ref().unwrap(); - assert_eq!(group_with(layout, "paragraphStyles"), (true, styles.map(String::from).to_vec(), 1), "{layout:?}"); - assert!(valid(layout)); - - let bar = drag_bar(&h, "characterStyles"); - let properties = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); - drag(&mut h, bar.center(), properties); - let layout = h.state().ui.docking.as_ref().unwrap(); - let (floating, panels, active) = group_with(layout, "characterStyles"); - assert!(!floating, "{layout:?}"); - let at = panels.iter().position(|p| p == "characterStyles").unwrap(); - assert_eq!(panels.get(at + 1).map(String::as_str), Some("paragraphStyles"), "{panels:?}"); - assert_eq!(panels[active], "paragraphStyles"); - assert!(layout.floating.is_empty(), "{:?}", layout.floating); -} - -fn dock_of(layout: &Layout) -> (Side, &[Column]) { - let Some(Node::Split { first, second, .. }) = &layout.root else { panic!("root is not a split: {layout:?}") }; - assert!(matches!(&**first, Node::Tabs { panels, .. } if panels == &[CANVAS.to_string()]), "{layout:?}"); - let Node::Dock { side, columns } = &**second else { panic!("second is not a dock: {layout:?}") }; - (*side, columns) -} - -#[test] -fn default_workspaces_are_the_canvas_beside_an_end_dock() { - let mut app = app(); - let layout = default_layout(&app.ui); - assert!(valid(&layout)); - let (side, columns) = dock_of(&layout); - assert_eq!(side, Side::End); - let [outer, icons] = columns else { panic!("{columns:?}") }; - assert!(!outer.iconic); - assert_eq!(outer.groups.len(), 1); - assert_eq!(outer.groups[0].panels, ["properties", "pages", "layers"]); - assert!(icons.iconic); - let groups: Vec> = icons.groups.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect(); - let expected: Vec> = ICON_GROUPS.iter().map(|g| g.to_vec()).collect(); - assert_eq!(groups, expected, "the iconic column groups InDesign's panel families"); - // Essentials Classic: only its named icon groups, in one iconic column. - app.run("window.workspace", json!({"name": "Essentials Classic"})).unwrap(); - let layout = default_layout(&app.ui); - assert!(valid(&layout)); - let (_, columns) = dock_of(&layout); - let [column] = columns else { panic!("{columns:?}") }; - assert!(column.iconic); - let expected: Vec> = app.ui.icon_groups.clone().unwrap(); - let groups: Vec> = column.groups.iter().map(|g| g.panels.clone()).collect(); - assert_eq!(groups.len(), expected.len()); - assert!(groups.iter().flatten().all(|p| expected.iter().flatten().any(|e| e == p))); -} - -/// Every dock panel that starts in the iconic column has one place in [`ICON_GROUPS`], so a new -/// panel is not left as a group of its own. -#[test] -fn every_icon_panel_is_in_one_default_icon_group() { - for (id, _, _) in crate::dock::ICON_PANELS { - assert_eq!(ICON_GROUPS.iter().flat_map(|g| g.iter()).filter(|p| *p == id).count(), 1, "{id}"); - } - for id in ICON_GROUPS.iter().flat_map(|g| g.iter()) { - assert!(crate::dock::ICON_PANELS.iter().any(|p| p.0 == *id), "{id} is not a dock panel"); - } -} - -/// A panel the workspace places elsewhere leaves its icon group; a group it empties goes. -#[test] -fn placed_panels_leave_their_icon_groups() { - let mut placed = Placed::default(); - for id in ["properties", "pages", "layers", "swatches", "effects"] { - placed.take(id); - } - let column = icon_column(None, &mut placed).unwrap(); - let groups: Vec> = column.groups.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect(); - assert_eq!(groups.first().unwrap(), &["stroke", "color", "gradient"]); - assert_eq!(groups.get(1).unwrap(), &["characterStyles", "paragraphStyles"]); - assert_eq!(groups.len(), ICON_GROUPS.len() - 1); -} - -#[test] -fn the_dock_is_at_the_right_in_english_and_the_left_in_arabic() { - let tab = |h: &Harness<'static, DesignApp>| h.ctx.read_response(tab_id("properties")).map(|r| r.rect).unwrap(); - let h = harness(app(), vec2(1280.0, 800.0)); - assert!(tab(&h).left() > 640.0, "{:?}", tab(&h)); - let mut arabic = app(); - arabic.ui.language = "ar".into(); - let h = harness(arabic, vec2(1280.0, 800.0)); - assert!(tab(&h).right() < 640.0, "{:?}", tab(&h)); -} - -#[test] -fn ui_docking_reports_the_layout_and_the_focused_panel() { - let mut h = harness(app(), vec2(1280.0, 800.0)); - let ctx = h.ctx.clone(); - let docking = |app: &mut DesignApp| { - let (req, _rx) = crate::control::ControlRequest::new("ui.docking", json!({})); - match crate::control::handle(app, &ctx, &req) { - crate::control::Outcome::Done(v) => v, - crate::control::Outcome::Screenshot { .. } => panic!("ui.docking took a screenshot"), - } - }; - let v = docking(h.state_mut()); - assert_eq!(v["ok"], true, "{v}"); - let result = &v["result"]; - assert_eq!(result["customized"], false); - assert_eq!(result["rightToLeft"], false); - assert!(result["focused"].is_null()); - let layout: Layout = serde_json::from_value(result["layout"].clone()).unwrap(); - assert_eq!(layout, default_layout(&h.state().ui)); - let at = h.ctx.read_response(tab_id("layers")).unwrap().rect.center(); - click(&mut h, at, PointerButton::Primary); - let v = docking(h.state_mut()); - assert_eq!(v["result"]["focused"], "layers", "{v}"); - // Closing the focused panel releases the focus. - h.state_mut().run("window.panel.close", json!({"panel": "layers"})).unwrap(); - let v = docking(h.state_mut()); - assert!(v["result"]["focused"].is_null(), "{v}"); - assert_eq!(v["result"]["customized"], true); - assert_eq!(v["result"]["hidden"], json!(["layers"])); -} - -/// A reset replaces the panels: none of them stays focused, opened for `ui.set closePanel` or -/// queued to raise. -#[test] -fn workspace_reset_forgets_the_focused_opened_and_raised_panel() { - let mut app = app(); - app.run("window.panel.float", json!({"panel": "layers"})).unwrap(); - app.ui.docking_focused = Some("layers".into()); - assert_eq!(app.ui.docking_opened.as_deref(), Some("layers")); - assert_eq!(app.ui.docking_raise.as_deref(), Some("layers")); - app.run("window.workspace.reset", json!({})).unwrap(); - assert_eq!(app.ui.docking_focused, None); - assert_eq!(app.ui.docking_opened, None); - assert_eq!(app.ui.docking_raise, None); - close_visible(&mut app).unwrap(); - assert!(layout_of(&app).contains(&"layers".to_string()), "closePanel after a reset closes nothing"); -} - -/// A command that moves the focused panel takes the focus away and leaves it the panel last -/// opened; one that closes it forgets both. -#[test] -fn moving_or_closing_the_focused_panel_by_command_updates_the_panel_state() { - let mut app = app(); - app.ui.docking_focused = Some("layers".into()); - app.run("window.panel.activate", json!({"panel": "pages"})).unwrap(); - assert_eq!(app.ui.docking_focused.as_deref(), Some("layers"), "a command that leaves Layers in place keeps its focus"); - app.ui.docking_opened = Some("layers".into()); - app.run("window.panel.move", json!({"panel": "layers", "anchor": "swatches", "zone": "tab"})).unwrap(); - assert_eq!(app.ui.docking_focused, None); - assert_eq!(app.ui.docking_opened.as_deref(), Some("layers")); - app.ui.docking_focused = Some("layers".into()); - app.run("window.panel.close", json!({"panel": "layers"})).unwrap(); - assert_eq!(app.ui.docking_focused, None); - assert_eq!(app.ui.docking_opened, None); - assert_eq!(app.ui.docking_raise, None); -} - -/// A divider dragged with the pointer gives the keyboard back: the next arrow key nudges the -/// selection and leaves the dock's size alone. -#[test] -fn arrow_after_dragging_a_dock_divider_nudges_the_selection_not_the_dock() { - let mut app = app(); - app.run("file.new", json!({"width":480,"height":360})).unwrap(); - let id = app.run("frame.create", json!({"rect":[80,70,330,230],"content":"none"})).unwrap()["id"].as_u64().unwrap(); - let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state( - |ui, app: &mut DesignApp| { - app.logic(ui.ctx()); - app.ui(ui); - }, - app, - ); - h.run_steps(5); - let column = craft_ui::docking::Hit::Divider(craft_ui::docking::Divider::Column("properties".to_string())); - let divider = hits(&h).into_iter().find_map(|(hit, rect)| (hit == column).then_some(rect)).expect("the main dock column's divider"); - drag(&mut h, divider.center(), divider.center() - vec2(40.0, 0.0)); - let resized = h.state().ui.docking.clone().expect("the drag customizes the layout"); - let bounds = |h: &Harness<'static, DesignApp>| h.state().session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().bounds(); - let before = bounds(&h); - h.event(Event::Key { key: egui::Key::ArrowRight, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }); - h.run_steps(1); - h.event(Event::Key { key: egui::Key::ArrowRight, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE }); - h.run_steps(2); - assert!(bounds(&h).x0 > before.x0, "the arrow nudges the selection"); - assert_eq!(h.state().ui.docking.as_ref(), Some(&resized), "the arrow doesn't resize the dock"); -} - -fn hits(h: &Harness<'static, DesignApp>) -> Vec<(craft_ui::docking::Hit, egui::Rect)> { - craft_ui::docking::DockArea::::hit_rects(&h.ctx, area_id()) -} - -/// `window.panel.*` ids and DesignCraft's aliases go through craft-ui's `DockCommand`. -#[test] -fn shared_panel_commands_float_dock_and_reject_atomically() { - let mut app = app(); - let docked = default_layout(&app.ui); - app.run("window.floatPanel", json!({"panel": "Layers", "x": 60, "y": 90})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - let window = layout.floating.iter().find(|w| w.contains(&"layers".to_string())).expect("Layers floats"); - assert_eq!(window.rect, [60.0, 90.0, 300.0, 420.0], "at the given place, at its own size"); - app.run("window.dockPanel", json!({"panel": "layers"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(layout.floating.is_empty()); - assert_eq!( - layout.root, - docked.root.map(|mut root| { - // Back in its place, in front of its group. - if let Node::Split { second, .. } = &mut root - && let Node::Dock { columns, .. } = &mut **second - && let Some(group) = columns.first_mut().and_then(|c| c.groups.first_mut()) - { - group.active = 2; - } - root - }), - "Dock Panel returns it where it floated from" - ); - // A docked panel that shares its group leaves it for a new dock at the start edge. - let mut tabbed = self::app(); - tabbed.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); - let layout = tabbed.ui.docking.clone().unwrap(); - assert_eq!(tab_group(&layout, "pages"), ["properties", "pages"]); - assert!(matches!(&layout.root, Some(Node::Split { first, .. }) if matches!(&**first, Node::Dock { side: Side::Start, .. }))); - assert_eq!(tab_group(&layout, "layers"), ["layers"]); - // A whole group, by any of its panels, then into a new column of the end dock. - app.run("window.panel.float", json!({"panel": "pages", "group": true, "x": 80, "y": 80, "width": 300, "height": 400})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert_eq!(layout.floating.len(), 1); - assert_eq!(window_groups(&layout.floating[0])[0].panels, ["properties", "pages", "layers"]); - app.run("window.panel.dock", json!({"panel": "pages", "group": true, "side": "End"})).unwrap(); - let layout = app.ui.docking.clone().unwrap(); - assert!(layout.floating.is_empty()); - assert_eq!(tab_group(&layout, "pages"), ["properties", "pages", "layers"]); - // A lone panel to a new dock at the start edge. - app.run("window.panel.float", json!({"panel": "stroke"})).unwrap(); - app.run("window.panel.dock", json!({"panel": "stroke", "side": "Start"})).unwrap(); - assert!( - matches!(&app.ui.docking.as_ref().unwrap().root, Some(Node::Split { first, .. }) if matches!(&**first, Node::Dock { side: Side::Start, .. })) - ); - // Docking it at that side again leaves it where it is. - let before = app.ui.docking.clone(); - app.run("window.panel.dock", json!({"panel": "stroke", "side": "Start"})).unwrap(); - assert_eq!(app.ui.docking, before, "it already leads the start dock's last column"); - // Close Others and Minimize are shared commands too. - app.run("window.panel.closeOthers", json!({"panel": "pages"})).unwrap(); - assert_eq!(tab_group(app.ui.docking.as_ref().unwrap(), "pages"), ["pages"]); - assert!(app.ui.docking_hidden.contains_key("properties") && app.ui.docking_hidden.contains_key("layers")); - app.run("window.panel.minimize", json!({"panel": "pages"})).unwrap(); - let location = app.ui.docking.as_ref().unwrap().location(&"pages".to_string()).unwrap(); - assert!(location.group.is_some_and(|g| g.minimized)); - // Rejected commands change nothing. - let before = (app.ui.docking.clone(), app.ui.docking_hidden.clone()); - for (id, params) in [ - ("window.workspace.lock", json!({})), - ("window.panel.window", json!({"panel": "pages"})), - ("window.panel.move", json!({"panel": "stroke", "anchor": "pages", "zone": "left", "before": "pages"})), - ("window.panel.dock", json!({"panel": "stroke", "side": "Middle"})), - ("window.panel.dock", json!({"panel": "stroke", "zone": "left"})), - ("window.dockPanel", json!({"panel": "properties", "zone": "bottom"})), - ] { - assert!(app.run(id, params).is_err(), "{id}"); - assert_eq!((app.ui.docking.clone(), app.ui.docking_hidden.clone()), before, "{id}"); - } - // A zone is a side of an anchor; the error names the two ways to say where. - let error = app.run("window.panel.dock", json!({"panel": "stroke", "zone": "right"})).unwrap_err(); - assert!(error.contains("anchor") && error.contains("side"), "{error}"); - // Every shared id is registered for the command list and MCP. - for id in craft_ui::docking::DockCommand::::IDS { - assert!(crate::menus::UI_COMMANDS.iter().any(|c| c.0 == *id), "{id} is not registered"); - } -} - -/// A tab released over the canvas floats there (craft-ui: the protected canvas takes no tabs or -/// splits); a lone panel released in the window's left edge band makes a new dock there. -#[test] -fn tabs_dropped_on_the_canvas_float_and_at_the_far_edge_make_a_dock() { - let mut h = harness(app(), vec2(1280.0, 800.0)); - let tab = |h: &Harness<'static, DesignApp>, panel: &str| { - hits(h).into_iter().find(|(hit, _)| *hit == craft_ui::docking::Hit::Tab(panel.into())).map(|(_, r)| r) - }; - let layers = tab(&h, "layers").expect("the Layers tab"); - drag(&mut h, layers.center(), egui::pos2(500.0, 400.0)); - let layout = h.state().ui.docking.clone().unwrap(); - assert!(layout.floating.iter().any(|w| w.contains(&"layers".to_string())), "{layout:?}"); - // A new dock takes a panel that is alone in its group: the floating Layers, by its title bar. - // The harness's area starts 8 pt in from the window edge. - let title = hits(&h).into_iter().find(|(hit, _)| *hit == craft_ui::docking::Hit::Title("layers".into())).expect("the title bar").1; - drag(&mut h, title.center(), egui::pos2(12.0, 400.0)); - let layout = h.state().ui.docking.clone().unwrap(); - assert!(valid(&layout)); - let mut docks = Vec::new(); - let mut nodes: Vec<&Node> = layout.root.iter().collect(); - while let Some(node) = nodes.pop() { - match node { - Node::Split { first, second, .. } => nodes.extend([&**first, &**second]), - Node::Dock { side, columns } => { - docks.push((*side, columns.iter().flat_map(|c| &c.groups).flat_map(|g| g.panels.clone()).collect::>())) - } - _ => {} - } - } - assert!(docks.iter().any(|(side, panels)| *side == Side::Start && panels == &["layers"]), "{docks:?} {:?}", layout.floating); -} - -/// `ui.docking` lists where each gesture starts: tabs, drag bars and the iconic column's icons. -#[test] -fn ui_docking_reports_hit_rects() { - let h = harness(app(), vec2(1280.0, 800.0)); - let v = crate::panel_docking::inspect(h.state(), &h.ctx); - let rects = v["hitRects"].as_array().expect("hitRects"); - let rect_of = |hit: Value| { - let entry = rects.iter().find(|r| r["hit"] == hit).unwrap_or_else(|| panic!("no {hit} in {rects:?}")); - let r: Vec = serde_json::from_value(entry["rect"].clone()).unwrap(); - egui::Rect::from_min_size(egui::pos2(r[0], r[1]), vec2(r[2], r[3])) - }; - let tab = rect_of(json!({"Tab": "pages"})); - let bar = rect_of(json!({"DragBar": "properties"})); - let icon = rect_of(json!({"Icon": "stroke"})); - assert!(tab.width() > 20.0 && bar.width() >= 24.0 && icon.width() > 20.0, "{tab:?} {bar:?} {icon:?}"); - assert!((tab.center().y - bar.center().y).abs() < 1.0, "the bar is in the tab strip"); - assert!(icon.left() > 640.0, "the iconic column is at the end edge"); -} - -/// Clicking an icon in the iconic column opens its panel as a flyout on the canvas side; a -/// second click closes it. -#[test] -fn clicking_an_iconic_column_icon_opens_its_flyout() { - let mut h = harness(app(), vec2(1280.0, 800.0)); - let icon = hits(&h).into_iter().find(|(hit, _)| *hit == craft_ui::docking::Hit::Icon("swatches".into())).expect("the Swatches icon").1; - click(&mut h, icon.center(), PointerButton::Primary); - assert_eq!(h.state().ui.docking_flyout.as_deref(), Some("swatches")); - let tab = hits(&h).into_iter().find(|(hit, _)| *hit == craft_ui::docking::Hit::Tab("swatches".into())).expect("the flyout's tab").1; - assert!(tab.right() <= icon.left() + 1.0, "the flyout is beside the column, on the canvas side: {tab:?} {icon:?}"); - assert!(showing(h.state(), "swatches")); - click(&mut h, icon.center(), PointerButton::Primary); - assert!(h.state().ui.docking_flyout.is_none()); -} - -/// Iconic columns paint DesignCraft's own panel icons, in the colour craft-ui passes; a panel -/// without an icon paints nothing. -#[test] -fn panel_icons_are_painted_in_code() { - let mut app = app(); - let mut content = PanelContent { app: &mut app }; - let ctx = egui::Context::default(); - let mut paint = |panel: &str| { - let mut painted = false; - let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { - let painter = ui.ctx().layer_painter(egui::LayerId::background()); - let rect = egui::Rect::from_min_size(Pos2::ZERO, vec2(20.0, 20.0)); - painted = craft_ui::docking::DockContent::paint_icon(&mut content, &painter, rect, &panel.to_string(), egui::Color32::RED); - }); - output.textures_delta.clear(); - (painted, output.shapes.len()) - }; - let (painted, shapes) = paint("pages"); - assert!(painted && shapes > 0, "{painted} {shapes}"); - assert_eq!(paint(CANVAS), (false, 0)); -} - -/// Every docking label in DesignCraft's wording is translated in the shared catalog's languages. -#[test] -fn docking_labels_are_translated() { - for language in ["de", "fr", "es", "ja", "zh"] { - for (key, english) in DOCK_LABELS { - assert_ne!(crate::i18n::tr(language, english), *english, "{key} in {language}"); - } - } -} - -/// With DesignCraft's title bar under macOS's transparent one, a floating window saved in the -/// title-bar strip is drawn below it, where its title bar can't move the app window. -#[test] -fn a_floating_window_saved_in_the_title_bar_strip_is_drawn_below_it() { - let mut app = app(); - app.integrated_titlebar = true; - app.run("window.panel.float", json!({"panel":"properties"})).unwrap(); - let window = app.ui.docking.as_mut().unwrap().floating.iter_mut().find(|w| w.panels().any(|p| p == "properties")).unwrap(); - window.rect = [200.0, 0.0, 280.0, 360.0]; - let h = harness(app, egui::vec2(1400.0, 900.0)); - let drawn = h.ctx.memory(|m| m.area_rect(crate::panel_docking::floating_layer("properties").id)).unwrap(); - assert!(drawn.min.y >= crate::chrome::MAC_APP_BAR - 0.5, "{drawn:?}"); -} - -/// A floating window holds splits like the dock: a panel docked below another one in the -/// window splits it, the divider between them drags, and the new size is saved. -#[test] -fn a_split_inside_a_floating_window_resizes_and_is_saved() { - let mut app = app(); - app.run("window.panel.float", json!({"panel":"swatches", "x":300, "y":120, "width":300, "height":500})).unwrap(); - app.run("window.panel.move", json!({"panel":"color", "anchor":"swatches", "zone":"bottom"})).unwrap(); - let window = |app: &DesignApp| app.ui.docking.as_ref().unwrap().floating.iter().find(|w| w.contains("swatches")).cloned().unwrap(); - let ratio = |window: &Floating| match window.root { - Node::Split { size: craft_ui::layout::SplitSize::Ratio(r), .. } => r, - ref root => panic!("expected a split window: {root:?}"), - }; - assert_eq!(window_groups(&window(&app)).into_iter().map(|g| g.panels).collect::>(), [["swatches"], ["color"]]); - let before = ratio(&window(&app)); - let mut h = harness(app, vec2(1400.0, 900.0)); - let divider = hits(&h) - .into_iter() - .find_map(|(hit, rect)| { - matches!(&hit, craft_ui::docking::Hit::Divider(craft_ui::docking::Divider::WindowSplit(_, path)) if path.is_empty()).then_some(rect) - }) - .expect("the window's divider"); - drag(&mut h, divider.center(), divider.center() + vec2(0.0, 60.0)); - let after = ratio(&window(h.state())); - assert!(after > before + 0.05, "{before} -> {after}"); - let saved = serde_json::to_vec(&h.state().ui).unwrap(); - let loaded = crate::UiState::from_preferences_json(&saved).unwrap(); - assert_eq!(loaded.docking, h.state().ui.docking); -} - -/// The default layout is rebuilt every frame: its floating windows keep their ids, so their -/// native windows aren't opened again each frame. -#[test] -fn default_layout_floats_keep_their_window_ids() { - let mut app = app(); - app.ui.floating = vec![("swatches".to_string(), [10.0, 20.0])]; - let ids = |app: &DesignApp| default_layout(&app.ui).floating.iter().map(|w| w.id).collect::>(); - let first = ids(&app); - assert_eq!(first.len(), 1, "Swatches floats"); - assert_eq!(ids(&app), first); -} - -#[test] -fn dialogs_about_and_quick_apply_block_native_floating_windows() { - let mut app = app(); - assert!(!modal_open(&app)); - app.ui.dialog = Some(crate::dialogs::Dialog::new("newDocument", json!({}))); - assert!(modal_open(&app), "dialog"); - app.ui.dialog = None; - app.run("help.about", json!({})).unwrap(); - assert!(modal_open(&app), "About"); - app.run("help.about", json!({"open": false})).unwrap(); - assert!(!modal_open(&app)); - app.ui.palette = Some(String::new()); - assert!(modal_open(&app), "Quick Apply"); - app.ui.palette = None; - assert!(!modal_open(&app)); -} - -#[cfg(not(target_arch = "wasm32"))] -#[test] -fn a_document_dropped_on_a_floating_panel_window_opens() { - #[derive(Debug)] - struct Dropped(std::path::PathBuf); - impl egui::DroppedFile for Dropped { - fn path(&self) -> &std::path::Path { - &self.0 - } - fn bytes(&self) -> Result, String> { - std::fs::read(&self.0).map_err(|e| e.to_string()) - } - } - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("dropped.designcraft"); - let mut saved = app(); - saved.run("file.new", json!({})).unwrap(); - saved.run("file.save", json!({"path": path})).unwrap(); - - let mut app = app(); - assert!(app.session.documents().is_empty()); - let files = craft_ui::docking::WindowFiles { - window: 1, - viewport: egui::ViewportId::from_hash_of("float"), - panel: "layers".to_owned(), - pos: None, - hovered: Vec::new(), - dropped: vec![std::sync::Arc::new(Dropped(path.clone())) as egui::DroppedFileHandle], - }; - open_window_drops(&mut app, &[files]); - assert_eq!(app.session.documents().len(), 1, "the dropped document opens"); -} From c9255c30ae6a98c76065f660234aee2906535132 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:08:51 +0100 Subject: [PATCH 29/75] HU1: preserve localization regressions in the new test module Retain localization checks from both merge parents, including Hungarian context and catalog coverage, and directly check the repaired Georgian captions. This file matches the locally tested shared-module integration. --- crates/ui-egui/src/i18n/tests.rs | 152 ++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index db13138cf..c8dc8578f 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -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| { @@ -52,6 +61,9 @@ fn cached_lookup_preserves_every_translation_across_language_switches() { assert_eq!(tr("", key), *key); } } + // Keep the repaired Georgian menu captions present with their exact translations. + assert_eq!(tr("ka", "File Info…"), "ფაილის ინფორმაცია…"); + assert_eq!(tr("ka", "Check for Updates…"), "განახლებების შემოწმება…"); // Strings no catalog has stay as they are, in every language. let unknown = String::from("A user-defined untranslated label"); for lang in LANGUAGES.iter().map(|(code, _)| *code).chain(["en", "unknown"]) { @@ -61,6 +73,92 @@ 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 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 +175,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 +209,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 +394,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]", From 39109cc0df5d5b4b6ef7802e5873cf799901c642 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:10:55 +0100 Subject: [PATCH 30/75] HU1: retain both parents menu tests in the shared layout Retain the upstream shortcut and navigation tests together with Hungarian selection, persistence, menu-path and document-preservation regressions and the complete Georgian-menu assertion. --- crates/ui-egui/src/menus/tests.rs | 153 ++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 29ad3404a..ef8ad6035 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -54,6 +54,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()); @@ -63,6 +64,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::>().join("/"); + assert_eq!(translated, visible, "{path}"); + } + } +} + fn walk(items: &[Item], out: &mut Vec<(String, String, Value)>) { for it in items { match it { @@ -548,6 +567,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()); @@ -566,6 +586,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()); @@ -583,6 +605,137 @@ 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}"); + } + } + + run_ui(&mut app, "app.language", &json!({"lang": "ka"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "ka"); + assert_eq!(checked(&app, "app.language", &json!({"lang": "ka"})), Some(true)); + // Every menu title, item and submenu is translated into Georgian. + let mut untranslated = Vec::new(); + for (title, entries) in menu_tree() { + let mut labels = vec![title.to_string()]; + let mut stack: Vec<&Item> = entries.iter().collect(); + while let Some(entry) = stack.pop() { + match entry { + Item::Sub(label, children) => { + labels.push(label.clone()); + stack.extend(children.iter()); + } + Item::Cmd { label, id, .. } if id != "app.language" => labels.push(label.clone()), + _ => {} + } + } + // Labels without letters ("180°") or point sizes ("72 pt") read the same in every language. + let size = |l: &str| l.strip_suffix(" pt").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit())); + untranslated.extend(labels.into_iter().filter(|l| l.chars().any(char::is_alphabetic) && !size(l) && crate::i18n::tr("ka", l) == l.as_str())); + } + assert!(untranslated.is_empty(), "untranslated Georgian menu labels: {untranslated:?}"); +} + +/// 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] From f8b05ee3df5c482106b08d24a7486925dc926a7e Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:11:25 +0100 Subject: [PATCH 31/75] HU1: retain toolbar coverage with current keymap captions Preserve existing toolbar assertions and Hungarian right-click flyout coverage in both column layouts. Check platform-formatted default shortcuts and a user-assigned chord, including painted text, clipping and overlap. All 13 focused toolbar tests passed. --- crates/ui-egui/src/toolbar/tests.rs | 93 ++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/toolbar/tests.rs b/crates/ui-egui/src/toolbar/tests.rs index 6a8ea752c..c970f7286 100644 --- a/crates/ui-egui/src/toolbar/tests.rs +++ b/crates/ui-egui/src/toolbar/tests.rs @@ -19,10 +19,100 @@ 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() } +/// 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}; + + /// 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)), + _ => {} + } + } + + 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"); + } +} + /// 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 +287,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 { From 4207debc170e452c59471bdef166f297afec23f7 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:14:01 +0100 Subject: [PATCH 32/75] HU1: retain Hungarian in the moved language command metadata Keep hu in the app.language parameter documentation after the UI command table moved to menus/ui_commands.rs. --- crates/ui-egui/src/menus/ui_commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 3d43beea6..42cfa56b9 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -66,7 +66,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", From e6e8fc126eb17f6fac9527dd21ce0db13fbc96c0 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:15:05 +0100 Subject: [PATCH 33/75] HU1: keep the integrated PDF condition clippy-clean Apply the equivalent De Morgan form of the upstream gradient condition so Rust 1.95 clippy with warnings denied can complete. Short-circuit order and output behavior are unchanged. --- crates/pdf/src/gradient_inks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pdf/src/gradient_inks.rs b/crates/pdf/src/gradient_inks.rs index e889ee1d2..c92b80158 100644 --- a/crates/pdf/src/gradient_inks.rs +++ b/crates/pdf/src/gradient_inks.rs @@ -87,7 +87,7 @@ impl Exporter<'_> { let inks: Vec> = g.stops.iter().map(|s| self.stop_ink(s)).collect(); let has_spot = inks.iter().any(|i| matches!(i, Some(Ink::Spot { .. }))); let on_paper = g.stops.iter().any(|s| s.swatch.as_deref() == Some(designcraft_color::swatch::PAPER)); - if !has_spot && !overprint && !(on_paper && self.for_print() && !self.rgb_only) { + if !(has_spot || overprint || on_paper && self.for_print() && !self.rgb_only) { return None; } let Some(inks) = inks.into_iter().collect::>>() else { From 72a586e0d5f419cf98133f7a5863cd54d7d0ab11 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:15:37 +0100 Subject: [PATCH 34/75] HU1: keep the moved PDF test helpers clippy-clean Simplify two upstream Option predicates to equivalent is_none_or forms. Preserve the None cases, all test cases and assertions; no coverage is removed. --- crates/engine/src/cmd/export/text_tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/export/text_tests.rs b/crates/engine/src/cmd/export/text_tests.rs index e533320f8..2efcb2f92 100644 --- a/crates/engine/src/cmd/export/text_tests.rs +++ b/crates/engine/src/cmd/export/text_tests.rs @@ -204,12 +204,12 @@ fn black_and_gold(pdf: &[u8]) -> Vec<(u8, u8)> { let mut out = Vec::new(); for st in file.objects().into_iter().filter_map(|o| o.into_stream()) { let d = st.dict(); - if !d.get::>(b"Subtype").is_some_and(|n| n.as_ref() == b"Image") || d.get::(b"BitsPerComponent") != Some(8) { + if d.get::>(b"Subtype").is_none_or(|n| n.as_ref() != b"Image") || d.get::(b"BitsPerComponent") != Some(8) { continue; } let Some(cs) = d.get::>(b"ColorSpace") else { continue }; let mut it = cs.iter::>(); - if !it.next().and_then(|o| o.into_name()).is_some_and(|n| n.as_ref() == b"DeviceN") { + if it.next().and_then(|o| o.into_name()).is_none_or(|n| n.as_ref() != b"DeviceN") { continue; } let Some(Object::Array(names)) = it.next() else { continue }; From 7d48e47057f2abba78085d8accfa261221338b53 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:16:04 +0100 Subject: [PATCH 35/75] HU1: record shared-module localization integration Record the preserved Hungarian catalog and both parents localization, menu, toolbar and docking tests, current keymap-aware flyout sizing and direct Georgian lookup checks. Keep every existing progress entry and readiness percentage. --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index b3d3abeb8..52265475c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -378,6 +378,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- **2026-10-27** · HU1: integrate Hungarian localization with the shared module layout; retain both parents' localization, menu, toolbar and panel-docking tests and all Hungarian catalog entries. Adapt toolbar tests and flyout sizing to platform-formatted and user-assigned shortcuts; add direct lookup assertions for the repaired Georgian menu captions. No readiness percentage change. - **2026-10-25** · HU1: extend Hungarian toolbar regression coverage through real right-click flyouts in both column layouts, checking translated row labels, painted shortcuts, clipping and overlap. Existing assertions and runtime behavior are unchanged. No readiness percentage change. - **2026-10-25** · HU1: retain both branches' localization and toolbar regression tests while adopting the updated egui button role; translate Data Merge Arrangement as Elrendezés (1,905 Hungarian entries and four context captions). No readiness percentage change. - **2026-10-25** · HU1: add the two missing Georgian File Info and Check for Updates menu captions while retaining the complete-menu regression unchanged. No readiness percentage change. From 03d6404dcf11d38f5f032dc8403c6b066a3af3f0 Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Thu, 15 Oct 2026 11:38:44 +0200 Subject: [PATCH 36/75] Contributors: credit Y-az --- contributors/people.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 79acd86b0..ec7a75f01 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -71,3 +71,7 @@ real_name = "Paul Gemignani" [people.skylartaylor] display_name = "skylartaylor" + +[people.Y-az] +real_name = "Youssef Azriouil" +display_name = "youssef" From 7a3e69e20a345d957311c9b616b6f90b999795ee Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Wed, 14 Oct 2026 18:35:04 +0200 Subject: [PATCH 37/75] Hyphenation: bundle the Dutch patterns Enable hypher's "dutch" feature: Piet Tutelaers's hyph-nl.tex 1.1 from hyph-utf8, MIT licensed (added to NOTICE). Dutch text, including InDesign's nl_NL_2005, now hyphenates instead of never breaking a word. --- NOTICE | 14 ++++++++++++++ crates/compose/Cargo.toml | 2 +- crates/compose/src/hyphen.rs | 26 ++++++++++++++++++++++---- docs/gaps.md | 2 +- docs/localization-parity.md | 2 +- docs/target-app-parity.md | 4 ++-- docs/typography-parity.md | 2 +- 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/NOTICE b/NOTICE index fc46bcdf3..6f7cb28eb 100644 --- a/NOTICE +++ b/NOTICE @@ -32,6 +32,20 @@ list (author, source, license) is in ASSETS.md. AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- Dutch hyphenation patterns, embedded through the hypher crate (feature "dutch"): + hyph-nl.tex 1.1 from hyph-utf8, https://github.com/hyphenation/tex-hyphen. + Copyright (C) 1996 Piet Tutelaers. MIT License: + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: The above copyright notice + and this permission notice shall be included in all copies or substantial portions of the + Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - Portuguese hyphenation patterns, embedded through the hypher crate (feature "portuguese"): hyph-pt.tex 1.4 from hyph-utf8, https://github.com/hyphenation/tex-hyphen. Copyright (C) 1987, 1994, 1996, 2015 Pedro J. de Rezende, 1996, 2015 J. Joao Dias Almeida, diff --git a/crates/compose/Cargo.toml b/crates/compose/Cargo.toml index b7017f80f..126416a1d 100644 --- a/crates/compose/Cargo.toml +++ b/crates/compose/Cargo.toml @@ -21,7 +21,7 @@ log = { workspace = true } regex = { workspace = true } fancy-regex = { workspace = true } miniz_oxide = "0.8" -hypher = { version = "0.1.8", default-features = false, features = ["alloc", "greek", "portuguese", "spanish", "norwegian"] } +hypher = { version = "0.1.8", default-features = false, features = ["alloc", "greek", "portuguese", "spanish", "norwegian", "dutch"] } [dev-dependencies] designcraft-fonts = { workspace = true, features = ["testing"] } diff --git a/crates/compose/src/hyphen.rs b/crates/compose/src/hyphen.rs index 3f07f4b40..37f7ae598 100644 --- a/crates/compose/src/hyphen.rs +++ b/crates/compose/src/hyphen.rs @@ -2,10 +2,11 @@ //! Liang patterns compiled into `hypher`, one cargo feature per language: Spanish is Javier Bezos's //! `hyph-es.tex` 5.0 from hyph-utf8, MIT; Portuguese is `hyph-pt.tex` 1.4 from hyph-utf8 (Pedro J. //! de Rezende, J. Joao Dias Almeida, Leonardo Araujo, Aline Benevides), BSD-3-Clause; Norwegian is -//! `hyph-no.tex` from hyph-utf8 (Rune Kleveland, Ole Michael Selberg), permissive (see NOTICE); -//! Greek is `hyph-el-monoton.tex` 5.0 from hyph-utf8 (Dimitrios Filippou), LPPL or MIT, taken -//! under MIT. Those are the **monotonic** patterns, which is what `hypher` maps `el` to, so -//! polytonic text hyphenates by them as well. +//! `hyph-no.tex` from hyph-utf8 (Rune Kleveland, Ole Michael Selberg), permissive; Dutch is Piet +//! Tutelaers's `hyph-nl.tex` 1.1 from hyph-utf8, MIT (see NOTICE); Greek is `hyph-el-monoton.tex` +//! 5.0 from hyph-utf8 (Dimitrios Filippou), LPPL or MIT, taken under MIT. Those are the +//! **monotonic** patterns, which is what `hypher` maps `el` to, so polytonic text hyphenates by +//! them as well. //! //! English lookup order for each word: //! 1. the **dictionary** ([`Dictionary::en_us`]) — the public-domain Moby Hyphenator II word list @@ -435,6 +436,23 @@ mod tests { assert_eq!(h("beveger"), "be-ve-ger"); } + #[test] + fn dutch_uses_the_hyph_nl_patterns() { + let nl = Lang::for_language("Dutch").unwrap(); + // InDesign writes the 2005 spelling as `nl_NL_2005`. + assert_eq!(nl, Lang::for_language("nl_NL_2005").unwrap()); + assert_ne!(nl, Lang::for_language("Spanish").unwrap()); + let h = |w: &str| { + let pts = hyphen_points_in(w, &Limits::default(), nl); + w.chars().enumerate().map(|(i, c)| if pts.contains(&i) { format!("-{c}") } else { c.to_string() }).collect::() + }; + assert_eq!(h("uitspraak"), "uit-spraak"); + assert_eq!(h("kinderen"), "kin-de-ren"); + assert_eq!(h("leesvaardigheid"), "lees-vaar-dig-heid"); + assert_eq!(h("zelfvertrouwen"), "zelf-ver-trou-wen"); + assert_eq!(h("Ontwikkelen"), "Ont-wik-ke-len"); + } + #[test] fn heuristic_fallback() { let c: Vec = "happen".chars().collect(); diff --git a/docs/gaps.md b/docs/gaps.md index ea9cafd40..c0cbfa847 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -156,7 +156,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · - **Estimate:** 25–40 h. ### 13. Hyphenation and spelling languages (T, L) -- **Missing:** hyphenation patterns for every language but English, Spanish, Portuguese, Norwegian and Greek; spelling for +- **Missing:** hyphenation patterns for every language but English, Spanish, Portuguese, Norwegian, Greek and Dutch; spelling for every language but English. InDesign ships dictionaries for ~40 languages. - **Estimate:** 30–60 h (permissively licensed patterns exist for most; spelling word lists need licence review). **Doc:** [typography-parity.md](typography-parity.md) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 404cf13a7..599358c61 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -68,7 +68,7 @@ eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, ## Document languages InDesign 2026 hyphenates and spell-checks ~40 languages (Hunspell and Duden dictionaries). -DesignCraft hyphenates **English, Spanish, Portuguese and Norwegian** and spell-checks **English** only. Text language +DesignCraft hyphenates **English, Spanish, Portuguese, Norwegian and Dutch** and spell-checks **English** only. Text language can be set per paragraph and character (Properties, Character panel) and drives quotes and locale shaping. See [typography-parity.md](typography-parity.md). diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 3443ca8e6..2bed89817 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -59,7 +59,7 @@ they receive, setting type exactly, and basic editing that never fails. | Dimension | Weight | Ready | Remaining (Opus 5.5 h) | Evidence | Doc | |---|---|---|---|---|---| | Features (depth of each area) | 25% | 55% | 250–400 | 275 catalogue rows exist; many lack options (e.g. Books is 258 lines of engine code, Liquid Layout 329); see the area table | this file | -| Typography and composition fidelity | 15% | 45% | 120–200 | Knuth–Plass composer, keeps, grids, CJK, Arabic all exist; no comparison with InDesign's line breaks; optical kerning falls back to metrics (#181); overset rule differs (#180); hyphenation patterns for English, Spanish, Portuguese, Norwegian and Greek only | [typography-parity.md](typography-parity.md) | +| Typography and composition fidelity | 15% | 45% | 120–200 | Knuth–Plass composer, keeps, grids, CJK, Arabic all exist; no comparison with InDesign's line breaks; optical kerning falls back to metrics (#181); overset rule differs (#180); hyphenation patterns for English, Spanish, Portuguese, Norwegian, Greek and Dutch only | [typography-parity.md](typography-parity.md) | | File formats | 20% | 40% | 150–250 (+10–30 for an INDD corpus) | INDD and INDT open through indd-utils, measured on synthetic bytes only (#21); IDML both ways and verified in InDesign 2026, but real-file bugs open (#193, #338, #339, #340); PDF export strong; PDF/X-4 only | [file-format-parity.md](file-format-parity.md) | | UI/UX fidelity | 15% | 45% | 120–180 | Measured InDesign 2026 look; but users report object copy/paste (#351, #348, #185, #164), right-click menus (#238, #187, #160), out-port threading (#352, #194, #165) failing | [ui-parity.md](ui-parity.md) | | Stability | 10% | 45% | 60–100 | Never-crash rules and last-resort guards; startup failures on some GPUs and machines (#334, #273, #220, #192, #167, #166); no CI runs the tests on pull requests | [gaps.md](gaps.md) | @@ -362,7 +362,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Linked stories (Edit ▸ Place and Link, `story.placeAndLink/links/updateLink/unlink`): child stories copy a parent, show out of date in the Links panel and update. - Edit ▸ Menus (`window.hideMenuItem`): hide menu items; menus with hidden items end in Show All Menu Items. - View ▸ Separations Preview (`view.separations`, Window ▸ Output ▸ Separations Preview, `ink.separations`): the plates the print PDF puts on film, spot plates included. Each visible spread is exported as Export PDF writes it and separated with craft-prepress, so the Ink Manager's aliases and spots to process, overprint and the flattener show as printed. All inks as printed, one plate as film, or an ink limit in red over gray; the panel lists the plates with each ink's percentage under the pointer. -- Language (Character panel ▸ Language, Properties ▸ Character, `type.char {language}`): text in other languages isn't hyphenated or spell-checked with the English dictionaries, typographer's quotes follow the language (German „…“, French and Russian «…», Swiss, Spanish, Dutch, Nordic, Japanese and Traditional Chinese 「…」, Simplified Chinese “…”), the font's localized forms (OpenType `locl`: Turkish i, Bulgarian Cyrillic, Japanese / Chinese / Korean glyph variants) follow it too, and so does the fallback for CJK characters the font lacks (with Draw Missing Glyphs from Fallback Fonts on): Japanese, Simplified Chinese, Traditional Chinese and Korean each try their own installed system fonts (Japanese starts with the craft-fonts Japanese faces, Mincho first, when built with them; Hangul, kana and Bopomofo pick their language's fonts whatever the language). Language names from IDML keep their spelling; locale codes as InDesign writes them for some languages (`de_DE_2006`, `nl_NL_2005`; any `ll`, `ll_CC`, `ll-CC`) are their language and region for all of the above, and Chinese is also recognised in its common spellings ("Simplified Chinese", "Chinese (Traditional)", `zh_CN`, `zh-Hant`). Spanish is hyphenated with Javier Bezos's Liang patterns (`hyph-es.tex` 5.0, MIT), Portuguese with hyph-utf8's `hyph-pt.tex` 1.4 (BSD-3-Clause) and Norwegian with hyph-utf8's `hyph-no.tex` (permissive), all through the `hypher` crate; hyphenation for other languages isn't bundled yet. +- Language (Character panel ▸ Language, Properties ▸ Character, `type.char {language}`): text in other languages isn't hyphenated or spell-checked with the English dictionaries, typographer's quotes follow the language (German „…“, French and Russian «…», Swiss, Spanish, Dutch, Nordic, Japanese and Traditional Chinese 「…」, Simplified Chinese “…”), the font's localized forms (OpenType `locl`: Turkish i, Bulgarian Cyrillic, Japanese / Chinese / Korean glyph variants) follow it too, and so does the fallback for CJK characters the font lacks (with Draw Missing Glyphs from Fallback Fonts on): Japanese, Simplified Chinese, Traditional Chinese and Korean each try their own installed system fonts (Japanese starts with the craft-fonts Japanese faces, Mincho first, when built with them; Hangul, kana and Bopomofo pick their language's fonts whatever the language). Language names from IDML keep their spelling; locale codes as InDesign writes them for some languages (`de_DE_2006`, `nl_NL_2005`; any `ll`, `ll_CC`, `ll-CC`) are their language and region for all of the above, and Chinese is also recognised in its common spellings ("Simplified Chinese", "Chinese (Traditional)", `zh_CN`, `zh-Hant`). Spanish is hyphenated with Javier Bezos's Liang patterns (`hyph-es.tex` 5.0, MIT), Portuguese with hyph-utf8's `hyph-pt.tex` 1.4 (BSD-3-Clause), Norwegian with hyph-utf8's `hyph-no.tex` (permissive) and Dutch with Piet Tutelaers's `hyph-nl.tex` 1.1 (MIT), all through the `hypher` crate; hyphenation for other languages isn't bundled yet. - Books (`book.new/open/add/remove/list/styleSource/paginate/syncStyles/exportPdf`, Book panel): `.dcbook` files, continuous page numbering, chapter numbers resolved in book order (Automatic, Start Chapter Numbering at, Same as Previous Document; `layout.chapterNumbering`, with a style for Chapter Number variables and cross-references), style sync from a style source, one merged PDF. Section prefixes follow InDesign's rule (up to 8 characters, no + or comma). - Numbering & Section Options window as in InDesign (Start Section, automatic or explicit page numbering up to 99999, prefix, style, marker, Include Prefix; the document's chapter numbering and its book); OK applies both in one undo step (`layout.numberingOptions`). - Book numbering/style updates validate and serialize all targets before changing the first chapter. They write only closed native chapters, checking canonical paths for open aliases; IDML remains a read-only style source and PDF input. Snapshots are staged on disk with at most 4,096 chapter records in memory. Each replacement is atomic; a later write failure explicitly lists completed chapters rather than claiming whole-book rollback. Import/serialization memory limits and advanced book TOC/index work remain separate. diff --git a/docs/typography-parity.md b/docs/typography-parity.md index 8c92e8dfa..e9d5d20f5 100644 --- a/docs/typography-parity.md +++ b/docs/typography-parity.md @@ -36,7 +36,7 @@ Fidelity: **unmeasured** unless stated. | Single-line Composer | greedy | greedy with the same ranges | ✓ | | | World-Ready composers | Paragraph / Single-line World-Ready | RTL, Arabic and bidi in the main composer | ~ | Indic scripts (Devanagari etc.) shape through harfrust but are untested | | Justification | word/letter spacing, glyph scaling min/desired/max, single-word, last line | all of these | ✓ | a justified line with a tab justifies after the tab (#284); set per paragraph in Type › Justification… and the Paragraph panel menu (#531) | -| Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list), Spanish (Bezos), Portuguese (hyph-pt), Norwegian (hyph-no) and Greek (hyph-el-monoton); zone, limits, exceptions in the user dictionary; Hyphenate Across Column off keeps automatic hyphens off the last line of a column or frame | ~ | other languages don't hyphenate (#87 reported "doesn't work") | +| Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list), Spanish (Bezos), Portuguese (hyph-pt), Norwegian (hyph-no), Greek (hyph-el-monoton) and Dutch (Tutelaers); zone, limits, exceptions in the user dictionary; Hyphenate Across Column off keeps automatic hyphens off the last line of a column or frame | ~ | other languages don't hyphenate (#87 reported "doesn't work") | | Spelling | per-language dictionaries | English (Moby) + document/user dictionary | ~ | | | Kerning | Metrics, Optical, manual, none | Metrics, Optical (outline-based: pairs pulled towards the font's stem gap, `crates/compose/src/optical.rs`), manual, none | ~ | Optical is an approximation of Adobe's algorithm (#181), not a match: imported display text can still break differently | | Tracking, scale, skew, baseline shift | ✓ | ✓ | ✓ | | From 5363ef8a4c40f312802c718c6b5e42983282c993 Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Thu, 29 Oct 2026 15:26:14 +0100 Subject: [PATCH 38/75] Docs: Greek in the localization hyphenation list Greek patterns are bundled on main; the localization parity list now names them next to Dutch. --- docs/localization-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 599358c61..d300a0b22 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -68,7 +68,7 @@ eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, ## Document languages InDesign 2026 hyphenates and spell-checks ~40 languages (Hunspell and Duden dictionaries). -DesignCraft hyphenates **English, Spanish, Portuguese, Norwegian and Dutch** and spell-checks **English** only. Text language +DesignCraft hyphenates **English, Spanish, Portuguese, Norwegian, Greek and Dutch** and spell-checks **English** only. Text language can be set per paragraph and character (Properties, Character panel) and drives quotes and locale shaping. See [typography-parity.md](typography-parity.md). From 9e1e9d079564170379c92c0c331f10de90b2bb54 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:04:03 +0100 Subject: [PATCH 39/75] HU1: retain upstream Georgian captions without duplicates Removed Georgian translations for 'File Info...' and 'Check for Updates...'. --- crates/ui-egui/src/i18n/ka.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 7cb8699f7..14a9e6203 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -5,7 +5,6 @@ //! the code or the command table use. pub(super) const TABLE: &[(&str, &str)] = &[ ("File", "ფაილი"), - ("File Info…", "ფაილის ინფორმაცია…"), ("Edit", "რედაქტირება"), ("Layout", "განლაგება"), ("Type", "ტექსტი"), @@ -433,7 +432,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Ungroup Color Group", "ფერების ჯგუფის დაშლა"), ("Unmerge", "შერწყმის გაუქმება"), ("Update", "განახლება"), - ("Check for Updates…", "განახლებების შემოწმება…"), ("Update Numbering", "დანომვრის განახლება"), ("Use Global Light", "გლობალური განათების გამოყენება"), ("User Dictionary", "მომხმარებლის ლექსიკონი"), From 453a3256f9cde17384c31bf77a6dfc818da39248 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:06:41 +0100 Subject: [PATCH 40/75] HU1: translate the Story Editor depth-ruler caption --- crates/ui-egui/src/i18n/hu.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs index 29406978c..890959761 100644 --- a/crates/ui-egui/src/i18n/hu.rs +++ b/crates/ui-egui/src/i18n/hu.rs @@ -1493,6 +1493,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show in the text", "Megjelenítés a szövegben"), ("Show the Typography panel", "Tipográfia panel megjelenítése"), ("Show/Hide Baseline Grid", "Alapvonalrács megjelenítése/elrejtése"), + ("Show/Hide Depth Ruler", "Mélységmérő megjelenítése/elrejtése"), ("Show/Hide Frame Edges", "Keretélek megjelenítése/elrejtése"), ("Show/Hide Guides", "Segédvonalak megjelenítése/elrejtése"), ("Show/Hide Hidden Characters", "Rejtett karakterek megjelenítése/elrejtése"), From 4cb6ad4f8abf693035508f52cac6238d268afd53 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:07:19 +0100 Subject: [PATCH 41/75] HU1: retain menu assertions under the upstream complexity limit Introduced a new test function to check Georgian menu label translations and replaced the previous inline implementation with a call to this new function. --- crates/ui-egui/src/menus/tests.rs | 45 +++++++++++++++++-------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 2f5cdaf09..9fd0247c2 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -587,6 +587,30 @@ fn show_document_grid_toggles_from_the_grids_and_guides_menu() { assert!(!app.ui.document_grid); } +/// Checks every Georgian menu title, item and submenu, allowing language-neutral labels. +fn assert_georgian_menu_labels_are_translated() { + // Every menu title, item and submenu is translated into Georgian. + let mut untranslated = Vec::new(); + for (title, entries) in menu_tree() { + let mut labels = vec![title.to_string()]; + let mut stack: Vec<&Item> = entries.iter().collect(); + while let Some(entry) = stack.pop() { + match entry { + Item::Sub(label, children) => { + labels.push(label.clone()); + stack.extend(children.iter()); + } + Item::Cmd { label, id, .. } if id != "app.language" => labels.push(label.clone()), + _ => {} + } + } + // Labels without letters ("180°") or point sizes ("72 pt") read the same in every language. + let size = |l: &str| l.strip_suffix(" pt").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit())); + untranslated.extend(labels.into_iter().filter(|l| l.chars().any(char::is_alphabetic) && !size(l) && crate::i18n::tr("ka", l) == l.as_str())); + } + assert!(untranslated.is_empty(), "untranslated Georgian menu labels: {untranslated:?}"); +} + /// Checks language selection and rejection of unknown language codes, Preferences dispatch, /// translated menu labels and preservation of document data when switching to Arabic. #[test] @@ -651,26 +675,7 @@ fn interface_language_switches_and_validates() { run_ui(&mut app, "app.language", &json!({"lang": "ka"})).unwrap().unwrap(); assert_eq!(app.ui.language, "ka"); assert_eq!(checked(&app, "app.language", &json!({"lang": "ka"})), Some(true)); - // Every menu title, item and submenu is translated into Georgian. - let mut untranslated = Vec::new(); - for (title, entries) in menu_tree() { - let mut labels = vec![title.to_string()]; - let mut stack: Vec<&Item> = entries.iter().collect(); - while let Some(entry) = stack.pop() { - match entry { - Item::Sub(label, children) => { - labels.push(label.clone()); - stack.extend(children.iter()); - } - Item::Cmd { label, id, .. } if id != "app.language" => labels.push(label.clone()), - _ => {} - } - } - // Labels without letters ("180°") or point sizes ("72 pt") read the same in every language. - let size = |l: &str| l.strip_suffix(" pt").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit())); - untranslated.extend(labels.into_iter().filter(|l| l.chars().any(char::is_alphabetic) && !size(l) && crate::i18n::tr("ka", l) == l.as_str())); - } - assert!(untranslated.is_empty(), "untranslated Georgian menu labels: {untranslated:?}"); + assert_georgian_menu_labels_are_translated(); } /// Checks Hungarian selection, menu captions and saved UI preferences without changing From 745ba77fd00d254511ddc08e7321ec158cbfb065 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:08:20 +0100 Subject: [PATCH 42/75] HU1: document current-main integration and the depth-ruler caption Updated Hungarian localization entries and added Depth Ruler caption. --- docs/localization-parity.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index a42ecb0c3..32a362ae4 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-27 · **Change:** minor (shared-module integration and retained localization regressions; older catalog counts retain their dated measurement) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-30 · **Change:** minor (current-main integration with retained catalogs and regression tests; 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), @@ -87,11 +87,22 @@ Hungarian catalog contents and the dated coverage measurements are unchanged. To coverage follows the current platform-specific keymap captions and checks a user-assigned chord alongside translated flyout text; row sizing measures the displayed shortcut. +Main integration (2026-10-30): retain every prior Hungarian entry and both branches’ +localization, menu and toolbar regressions while adopting upstream’s current file-dialog test +harness and toolbar proxy refactor. Add `Show/Hide Depth Ruler` as `Mélységmérő +megjelenítése/elrejtése`, bringing the catalog to **1,906 entries and four context captions**. +The upstream all-language depth-ruler assertion is retained. The unchanged complete Georgian +menu assertions run through a helper so the existing language-switch test meets upstream’s +new complexity limit. The two Georgian menu captions now also exist in main; retain each +identical translation once, keeping the direct lookup and complete-menu assertions. The +progress log keeps both branches’ dated entries; this targeted addition is not a new coverage +measurement. + ## Other shipped languages | Language | Code | UI strings translated | Status | |---|---|---|---| -| Hungarian | hu | 1,905 entries + 4 context captions (2026-10-25); initial native review | partial | +| Hungarian | hu | 1,906 entries + 4 context captions (2026-10-30); 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 | @@ -128,6 +139,7 @@ German variants have no automatic hyphenation; their German shaping and quotes a | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Integrate current main; retain both branches’ entries/tests; add Hungarian Depth Ruler caption (1,906 entries + four context captions) | | 2026-10-27 | minor | Adopt shared module layout; preserve both parents' tests and all Hungarian catalog entries | | 2026-10-25 | minor | Hungarian Data Merge arrangement caption added; 1,905 entries and four context captions; both merge parents' tests retained | | 2026-10-25 | minor | Georgian File Info and Check for Updates captions added; complete-menu regression retained | From b7f4612aa1d19f8f89488a21a33b1c0b2406c5dc Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:09:00 +0100 Subject: [PATCH 43/75] HU1: preserve the verified roadmap without editor whitespace --- ROADMAP.md | 1 - 1 file changed, 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index cc59b2f01..113cab376 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -767,4 +767,3 @@ The shared craft-installer UI preserves this revision's install options and prov - **2026-10-15** · Windows recovery folders validate the identity of the live locked file without reading it through a second handle. Missing or replaced locks disable writes and cleanup; regression tests cover retained exclusive locking and replacement-file protection. No readiness percentage change. - PDF reader infrastructure: pinned shared parsing/rasterization, transformed page-box geometry and explicit failures for known incomplete layered-PDF exports. Application ownership and parity targets are unchanged. - \ No newline at end of file From aa449aa35822ce01f5aadb152010c7b56e3db2ce Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 06:47:04 +0100 Subject: [PATCH 44/75] Fix intersects_rect missing hits for zero-width/height marquee rects Found while testing crates/geom/src/hit.rs:32-35 (intersects_rect early-out) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/geom/src/hit.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/geom/src/hit.rs b/crates/geom/src/hit.rs index 944ded3e9..096e0e1e3 100644 --- a/crates/geom/src/hit.rs +++ b/crates/geom/src/hit.rs @@ -27,7 +27,8 @@ pub fn stroke_contains(path: &BezPath, width: f64, tol: f64, p: Point) -> bool { /// Does the path intersect (or lie inside) the rect? Used for marquee selection. pub fn intersects_rect(path: &PathData, r: Rect) -> bool { let Some(b) = path.bounds() else { return false }; - if b.intersect(r).area() <= 0.0 && !(b.width() == 0.0 || b.height() == 0.0) { + let flat = |q: Rect| q.width() == 0.0 || q.height() == 0.0; + if b.intersect(r).area() <= 0.0 && !flat(b) && !flat(r) { return false; } // Any anchor inside? @@ -90,4 +91,12 @@ mod tests { let l = shapes::line(Point::new(0.0, 0.0), Point::new(10.0, 10.0)); assert!(intersects_rect(&l, Rect::new(4.0, 0.0, 6.0, 10.0))); } + + #[test] + fn flat_marquee() { + let p = shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)); + assert!(intersects_rect(&p, Rect::new(-5.0, 5.0, 15.0, 5.0))); + assert!(intersects_rect(&p, Rect::new(5.0, -5.0, 5.0, 15.0))); + assert!(!intersects_rect(&p, Rect::new(-5.0, 20.0, 15.0, 20.0))); + } } From 4b9a800899e4ab36144b04d862e1f05dfa2164ec Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 08:41:28 +0100 Subject: [PATCH 45/75] Report script parse error line numbers from the original text Found while testing crates/engine/src/script.rs:30-47 (parse: `t.lines().enumerate()` runs over the trimmed text) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/script.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/script.rs b/crates/engine/src/script.rs index 4a66ff87f..bcc4f8fe9 100644 --- a/crates/engine/src/script.rs +++ b/crates/engine/src/script.rs @@ -26,7 +26,8 @@ pub struct Step { pub fn parse(text: &str) -> Result, String> { // Windows tools save "UTF-8 with BOM"; `trim` keeps U+FEFF, which then became part of the // first command id. - let t = text.trim_start_matches('\u{feff}').trim(); + let body = text.trim_start_matches('\u{feff}'); + let t = body.trim(); if t.starts_with('[') { let v: Value = serde_json::from_str(t).map_err(|e| format!("script: {e}"))?; return v @@ -38,7 +39,8 @@ pub fn parse(text: &str) -> Result, String> { .collect(); } let mut out = Vec::new(); - for (n, line) in t.lines().enumerate() { + // Number lines from the untrimmed text so leading blank lines still count. + for (n, line) in body.lines().enumerate() { let l = line.trim(); if l.is_empty() || l.starts_with('#') || l.starts_with("//") { continue; @@ -185,6 +187,12 @@ impl crate::Session { mod tests { use super::*; + #[test] + fn leading_blank_lines_keep_line_numbers() { + let e = parse("\n\n# c\nfile.new\nframe.create {bad").unwrap_err(); + assert!(e.contains("line 5"), "{e}"); + } + #[test] fn parses_all_forms() { let a = parse(r#"[{"command": "file.new"}, {"command": "frame.create", "params": {"rect": [0, 0, 10, 10]}}]"#).unwrap(); From 3871100e3f81f07d18d739ea5524ac1b341bd190 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:30:41 +0100 Subject: [PATCH 46/75] Fix zero tangent angle in Type on a Path at collapsed handles Found while testing crates/geom/src/warp.rs:~44-52 (PathWarp::at) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/geom/src/warp.rs | 47 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/geom/src/warp.rs b/crates/geom/src/warp.rs index dacdedd06..6dd0cda9b 100644 --- a/crates/geom/src/warp.rs +++ b/crates/geom/src/warp.rs @@ -2,6 +2,20 @@ use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, ParamCurveDeriv, PathSeg, Point}; +/// Direction at a curve end whose derivative vanishes: from the end anchor towards the nearest +/// control point that differs from it (`at_end` selects the last anchor). +fn first_distinct(pts: &[Point], at_end: bool) -> kurbo::Vec2 { + let n = pts.len(); + for k in 1..n { + let (a, b) = if at_end { (pts[n - 1 - k], pts[n - 1]) } else { (pts[0], pts[k]) }; + let v = b - a; + if v.hypot2() > 1e-18 { + return v; + } + } + kurbo::Vec2::ZERO +} + /// A path measured for placing glyphs along it. #[derive(Clone, Debug)] pub struct PathWarp { @@ -29,12 +43,24 @@ impl PathWarp { let n = self.segs.len(); for (i, (seg, len)) in self.segs.iter().enumerate() { if s <= *len || i + 1 == n { - let t = if *len > 1e-9 { seg.inv_arclen(s.min(*len), 1e-4) } else { 0.0 }; + let t = if *len <= 1e-9 || s <= 0.0 { + 0.0 + } else if s >= *len { + 1.0 + } else { + seg.inv_arclen(s, 1e-4) + }; let p = seg.eval(t); let d = match seg { PathSeg::Line(l) => l.p1 - l.p0, - PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(), - PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(), + PathSeg::Quad(q) => { + let d = q.deriv().eval(t).to_vec2(); + if d.hypot2() > 1e-18 { d } else { first_distinct(&[q.p0, q.p1, q.p2], t > 0.5) } + } + PathSeg::Cubic(c) => { + let d = c.deriv().eval(t).to_vec2(); + if d.hypot2() > 1e-18 { d } else { first_distinct(&[c.p0, c.p1, c.p2, c.p3], t > 0.5) } + } }; let mut a = d.y.atan2(d.x); if self.flip { @@ -98,4 +124,19 @@ mod tests { assert!((w.nearest(Point::new(30.0, -8.0)) - 30.0).abs() < 1e-6); assert!((flipped.nearest(Point::new(30.0, -8.0)) - 170.0).abs() < 1e-6); } + + #[test] + fn collapsed_handles_fall_back_to_the_next_control_point() { + let half_pi = std::f64::consts::FRAC_PI_2; + let mut bp = BezPath::new(); + bp.move_to((0.0, 0.0)); + bp.curve_to((0.0, 0.0), (0.0, 10.0), (10.0, 10.0)); + let w = PathWarp::new(&bp, false); + assert!((w.at(0.0).unwrap().1 - half_pi).abs() < 1e-9, "start handle on the anchor"); + let mut bp = BezPath::new(); + bp.move_to((0.0, 0.0)); + bp.curve_to((10.0, 0.0), (10.0, 10.0), (10.0, 10.0)); + let w = PathWarp::new(&bp, false); + assert!((w.at(w.length()).unwrap().1 - half_pi).abs() < 1e-9, "end handle on the anchor"); + } } From c128548111b929d2072eb254bb05fd5a31ddd304 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:52:55 +0100 Subject: [PATCH 47/75] Clamp math.svg size to the same range as math.insert Found while testing crates/engine/src/cmd/mathexpr.rs:~50 (the math.svg query) vs insert at ~64 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/mathexpr.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/mathexpr.rs b/crates/engine/src/cmd/mathexpr.rs index e5342f79c..d6b4a9e95 100644 --- a/crates/engine/src/cmd/mathexpr.rs +++ b/crates/engine/src/cmd/mathexpr.rs @@ -35,6 +35,11 @@ fn math_item(d: &mut designcraft_doc::Document, latex: &str, size: f64, lid: des it } +/// The `size` parameter in points: 12 by default (also for non-finite values), clamped to 2..=400. +fn size_param(p: &Value) -> f64 { + p.get("size").and_then(Value::as_f64).filter(|v| v.is_finite()).unwrap_or(12.0).clamp(2.0, 400.0) +} + pub fn specs() -> Vec { vec![ cmd!( @@ -48,7 +53,7 @@ pub fn specs() -> Vec { ), cmd!(query "math.svg", "Math Expression SVG", [], None, "{latex, size?} → {svg, width, height} — typeset without placing", has_doc, |_s, p| { let latex = str_param(p, "latex").ok_or_else(|| bad("math.svg", "`latex` required"))?; - let (svg, (w, h)) = crate::math::to_svg(latex, p.get("size").and_then(Value::as_f64).unwrap_or(12.0)); + let (svg, (w, h)) = crate::math::to_svg(latex, size_param(p)); Ok(json!({"svg": String::from_utf8_lossy(&svg), "width": w, "height": h})) }), ] @@ -56,7 +61,7 @@ pub fn specs() -> Vec { fn insert(s: &mut Session, p: &Value) -> Result { let latex = str_param(p, "latex").ok_or_else(|| bad("math.insert", "`latex` required"))?.to_string(); - let size = p.get("size").and_then(Value::as_f64).unwrap_or(12.0).clamp(2.0, 400.0); + let size = size_param(p); let lid = s.doc()?.active_layer; // Edit an existing expression in place. if let Some(id) = super::id_param(p, "id") { @@ -102,6 +107,24 @@ mod tests { use crate::Session; + #[test] + fn math_svg_clamps_size_like_insert() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let dims = |s: &mut Session, size: f64| { + let r = s.execute("math.svg", &json!({"latex": "x", "size": size})).unwrap(); + (r["width"].as_f64().unwrap(), r["height"].as_f64().unwrap()) + }; + let (w2, h2) = dims(&mut s, 2.0); + let (w400, h400) = dims(&mut s, 400.0); + for size in [0.0, -5.0, 1e-300] { + assert_eq!(dims(&mut s, size), (w2, h2), "size {size}"); + } + for size in [1e9, 1e300] { + assert_eq!(dims(&mut s, size), (w400, h400), "size {size}"); + } + } + #[test] fn math_places_inline_and_as_frames() { let mut s = Session::new(); From 52232fc2c7d25b802e0f983e0e3dedf775693688 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:47:33 +0100 Subject: [PATCH 48/75] Read PDF/X output intent and Info as parsed objects in validate-pdf Fixes #2050 Co-Authored-By: Claude Opus 5.5 --- apps/designcraft-cli/src/main.rs | 3 +- crates/pdf/src/lib.rs | 2 +- crates/pdf/src/optional_content.rs | 2 +- crates/pdf/src/pdfx.rs | 70 ++++++++++++++++---- crates/pdf/src/pdfx/tests.rs | 103 +++++++++++++++++++++++++++++ crates/pdf/src/pdfx1a.rs | 24 +++---- crates/pdf/src/x1a_tests.rs | 10 +++ 7 files changed, 183 insertions(+), 31 deletions(-) diff --git a/apps/designcraft-cli/src/main.rs b/apps/designcraft-cli/src/main.rs index 9ce15674c..a9839fcd3 100644 --- a/apps/designcraft-cli/src/main.rs +++ b/apps/designcraft-cli/src/main.rs @@ -156,7 +156,6 @@ fn report(r: Result<(), String>) -> ExitCode { fn validate_pdf(args: &[String]) -> Result<(), String> { const USAGE: &str = "usage: designcraft-cli validate-pdf FILE.pdf [--standard x4|x1a]"; - const X1A: &[u8] = b"/GTS_PDFXVersion(PDF/X-1:"; let mut path = None; let mut standard = None; let mut it = args.iter(); @@ -172,7 +171,7 @@ fn validate_pdf(args: &[String]) -> Result<(), String> { let standard = match standard.map(designcraft_pdf::Standard::parse) { Some(Some(s)) => s, Some(None) => return Err(USAGE.into()), - None if bytes.windows(X1A.len()).any(|w| w == X1A) => designcraft_pdf::Standard::PdfX1a, + None if designcraft_pdf::pdfx_version(&bytes).is_some_and(|v| v.starts_with("PDF/X-1:")) => designcraft_pdf::Standard::PdfX1a, None => designcraft_pdf::Standard::PdfX4, }; let (name, issues) = match standard { diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 987e7ea97..37909a1f5 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -51,7 +51,7 @@ mod transitions; pub use export::{ BookletKind, BookletOptions, booklet_pairs, export_booklet, export_pdf, export_pdf_with_report, merge_pdfs, print_color, sheet_spreads, }; -pub use pdfx::{check_pdfx4, has_rgb_groups, make_pdfx4}; +pub use pdfx::{check_pdfx4, has_rgb_groups, make_pdfx4, pdfx_version}; pub use pdfx1a::check_pdfx1a; pub use transitions::{add_blend_space, add_catalog_entries, add_page_entries, add_transitions}; diff --git a/crates/pdf/src/optional_content.rs b/crates/pdf/src/optional_content.rs index 7857d36d7..374c0d1f1 100644 --- a/crates/pdf/src/optional_content.rs +++ b/crates/pdf/src/optional_content.rs @@ -58,7 +58,7 @@ fn edited(dict: &Dict<'_>, replacements: &[(&[u8], &[u8])]) -> Result> { /// The trailer of the cross-reference section at `offset`: a table's `trailer` dictionary or an /// xref stream's own. A table is read entry by entry rather than searched for the keyword, so a /// `trailer` in a comment can't stand in for the real one; reading is bounded either way. -fn trailer_at(bytes: &[u8], offset: usize) -> Option> { +pub(crate) fn trailer_at(bytes: &[u8], offset: usize) -> Option> { const MAX_SECTION_BYTES: usize = 4 * 1024 * 1024; let data = bytes.get(offset..)?; let mut reader = Reader::new(data.get(..data.len().min(MAX_SECTION_BYTES))?); diff --git a/crates/pdf/src/pdfx.rs b/crates/pdf/src/pdfx.rs index e20247813..6d7a3714f 100644 --- a/crates/pdf/src/pdfx.rs +++ b/crates/pdf/src/pdfx.rs @@ -8,6 +8,7 @@ use std::collections::HashSet; use hayro_syntax::content::UntypedIter; use hayro_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; use hayro_syntax::page::Resources; +use hayro_syntax::reader::ReaderExt; use craft_pdf::write::WriteError; use craft_pdf::write::backend as krilla; @@ -115,16 +116,18 @@ pub fn has_rgb_groups(pdf: &[u8]) -> bool { pub fn check_pdfx4(pdf: &[u8]) -> Vec { let mut issues = Vec::new(); let text = String::from_utf8_lossy(pdf); + let file = hayro_syntax::Pdf::new(pdf.to_vec()).ok(); if !pdf.starts_with(b"%PDF-1.") || pdf.get(7).is_none_or(|v| *v > b'6') { issues.push("the PDF version must be 1.6 or lower".into()); } - if !text.contains("/S/GTS_PDFX") { + let intents = file.as_ref().map(pdfx_intents).unwrap_or_default(); + if intents.is_empty() { issues.push("no PDF/X output intent".into()); } - if !text.contains("/DestOutputProfile") { + if !intents.iter().any(|oi| oi.contains_key(b"DestOutputProfile")) { issues.push("the output intent has no destination profile".into()); } - if !text.contains("/GTS_PDFXVersion(PDF/X-4)") { + if file.as_ref().and_then(|f| info_text(pdf, f, b"GTS_PDFXVersion")).as_deref() != Some("PDF/X-4") { issues.push("the document information doesn't name PDF/X-4".into()); } if !text.contains("pdfxid:GTS_PDFXVersion>PDF/X-4<") { @@ -150,22 +153,67 @@ pub fn check_pdfx4(pdf: &[u8]) -> Vec { if text.contains("/Subtype/Type3") { issues.push("Type 3 fonts aren't allowed".into()); } - issues.extend(device_rgb(pdf)); + issues.extend(device_rgb(file.as_ref())); issues.extend(fonts::unembedded_fonts(pdf)); issues } +/// The document information's `GTS_PDFXVersion` (`PDF/X-4`, `PDF/X-1:2003`, …), read as an +/// object; `None` when the file can't be read or doesn't name one. +pub fn pdfx_version(pdf: &[u8]) -> Option { + info_text(pdf, &hayro_syntax::Pdf::new(pdf.to_vec()).ok()?, b"GTS_PDFXVersion") +} + +/// The catalog's `GTS_PDFX` output intents, read as objects (whitespace and object streams +/// don't matter). +pub(crate) fn pdfx_intents<'a>(file: &'a hayro_syntax::Pdf) -> Vec> { + let xref = file.xref(); + let Some(catalog) = xref.get::>(xref.root_id()) else { return Vec::new() }; + let Some(intents) = catalog.get::>(b"OutputIntents") else { return Vec::new() }; + intents.iter::>().filter(|oi| oi.get::>(b"S").is_some_and(|s| s.as_ref() == b"GTS_PDFX")).collect() +} + +/// The text entry `key` of the document information dictionary, trimmed: the `/Info` of the +/// newest trailer that has one (classic or cross-reference stream, following `/Prev` a bounded +/// number of times), else of the last `trailer` keyword in the file. +pub(crate) fn info_text(pdf: &[u8], file: &hayro_syntax::Pdf, key: &[u8]) -> Option { + let lookup = |trailer: &Dict<'_>| -> Option { + let info = match trailer.get_ref(b"Info") { + Some(r) => file.xref().get::>(r.into())?, + None => trailer.get::>(b"Info")?, + }; + let value = info.get::>(key)?; + Some(String::from_utf8_lossy(value.as_bytes()).trim().to_string()) + }; + let tail = pdf.get(rfind(pdf, b"startxref")?..).unwrap_or_default(); + let mut offset = int_after(&String::from_utf8_lossy(tail), "startxref"); + let mut seen = HashSet::new(); + for _ in 0..32 { + let Some(at) = offset.filter(|at| seen.insert(*at)) else { break }; + let Some(trailer) = crate::optional_content::trailer_at(pdf, at) else { break }; + if trailer.contains_key(b"Info") { + return lookup(&trailer); + } + offset = trailer.get::(b"Prev"); + } + // A damaged cross-reference offset: the dictionary after the last `trailer` keyword. + let at = rfind(pdf, b"trailer")? + b"trailer".len(); + let mut reader = hayro_syntax::reader::Reader::new(pdf.get(at..)?); + reader.skip_white_spaces_and_comments(); + lookup(&reader.read_without_context::>()?) +} + /// Device-dependent RGB under a CMYK output intent. PDF/X-4 (ISO 15930-7) lets RGB in only when /// it is colour-managed: ICCBased RGB, or DeviceRGB remapped by a `DefaultRGB` colour space in the /// resources. DeviceCMYK and DeviceGray are the output intent's own device values, so they stay. /// This walks every page's content, the images, forms, patterns, shadings and soft masks it uses, /// and the transparency groups, and reports each kind of device RGB it finds with the pages it is /// on. Nothing is reported when the file has no CMYK PDF/X output intent. -fn device_rgb(pdf: &[u8]) -> Vec { - let Ok(file) = hayro_syntax::Pdf::new(pdf.to_vec()) else { +fn device_rgb(file: Option<&hayro_syntax::Pdf>) -> Vec { + let Some(file) = file else { return vec!["the file couldn't be read to check its colour spaces".into()]; }; - if !cmyk_output_intent(&file) { + if !cmyk_output_intent(file) { return Vec::new(); } let mut found: Vec<(String, Vec)> = Vec::new(); @@ -198,13 +246,7 @@ fn device_rgb(pdf: &[u8]) -> Vec { /// Whether the file's PDF/X output intent has a four-channel (CMYK) destination profile. pub(crate) fn cmyk_output_intent(file: &hayro_syntax::Pdf) -> bool { - let xref = file.xref(); - let Some(catalog) = xref.get::>(xref.root_id()) else { return false }; - let Some(intents) = catalog.get::>(b"OutputIntents") else { return false }; - intents.iter::>().any(|oi| { - oi.get::>(b"S").is_some_and(|s| s.as_ref() == b"GTS_PDFX") - && oi.get::>(b"DestOutputProfile").and_then(|p| p.dict().get::(b"N")) == Some(4) - }) + pdfx_intents(file).iter().any(|oi| oi.get::>(b"DestOutputProfile").and_then(|p| p.dict().get::(b"N")) == Some(4)) } /// Whether a colour space is device RGB: `/DeviceRGB` (`/RGB` in inline images), or an diff --git a/crates/pdf/src/pdfx/tests.rs b/crates/pdf/src/pdfx/tests.rs index edb6aec8f..b35e897c2 100644 --- a/crates/pdf/src/pdfx/tests.rs +++ b/crates/pdf/src/pdfx/tests.rs @@ -462,3 +462,106 @@ fn xmp_states_trapped() { assert!(String::from_utf8_lossy(&r.bytes).contains("/Trapped/False")); assert!(r.warnings.is_empty(), "{:?}", r.warnings); } + +/// `objects` (from 1) as a PDF 1.6 file with a classic cross-reference table and `trailer`. +fn assemble_x4(objects: &[&str], trailer: &str) -> Vec { + let mut out = b"%PDF-1.6\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objects.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f\r\n", objects.len() + 1).as_bytes()); + for off in offsets { + out.extend_from_slice(format!("{off:010} 00000 n\r\n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n{trailer}\nstartxref\n{xref}\n%%EOF\n").as_bytes()); + out +} + +const NO_INTENT: &str = "no PDF/X output intent"; +const NO_INFO: &str = "the document information doesn't name PDF/X-4"; + +/// The output intent and the document information are read as objects: whitespace between +/// tokens (as qpdf and Ghostscript write them) doesn't hide them. +#[test] +fn x4_checks_ignore_whitespace_between_tokens() { + let pdf = assemble_x4( + &[ + "<< /Type /Catalog /Pages 2 0 R /OutputIntents [ << /Type /OutputIntent /S /GTS_PDFX /DestOutputProfile 4 0 R >> ] >>", + "<< /Type /Pages /Kids [ 3 0 R ] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [ 0 0 100 100 ] /TrimBox [ 0 0 100 100 ] >>", + "<< /N 4 /Length 0 >>\nstream\n\nendstream", + "<< /GTS_PDFXVersion (PDF/X-4) /Trapped /False >>", + ], + "<< /Size 6 /Root 1 0 R /Info 5 0 R >>", + ); + let issues = check_pdfx4(&pdf); + assert!(!issues.iter().any(|i| i == NO_INTENT || i == NO_INFO), "{issues:?}"); + // Without them, both are still reported. + let bare = assemble_x4( + &[ + "<< /Type /Catalog /Pages 2 0 R /OutputIntents [ << /Type /OutputIntent /S /GTS_PDFA1 >> ] >>", + "<< /Type /Pages /Kids [ 3 0 R ] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [ 0 0 100 100 ] /TrimBox [ 0 0 100 100 ] >>", + "<< /N 4 /Length 0 >>\nstream\n\nendstream", + "<< /Title (/S/GTS_PDFX /GTS_PDFXVersion(PDF/X-4)) /GTS_PDFXVersion (PDF/X-3:2002) >>", + ], + "<< /Size 6 /Root 1 0 R /Info 5 0 R >>", + ); + let issues = check_pdfx4(&bare); + assert!(issues.iter().any(|i| i == NO_INTENT) && issues.iter().any(|i| i == NO_INFO), "{issues:?}"); +} + +/// The output intent and the document information are found inside a compressed object stream, +/// through a cross-reference stream. +#[test] +fn x4_checks_read_object_streams() { + let plain = [ + "<>", + "<>", + "<>", + "<>\nstream\n\nendstream", + ]; + let (info, intent) = ("<>", "<>"); + let mut out = b"%PDF-1.6\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in plain.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + // Object 5 is free; 6 and 7 live in object stream 8. + let header = format!("6 0 7 {} ", info.len() + 1); + let body = format!("{header}{info} {intent}"); + let objstm = out.len(); + let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast()); + std::io::Write::write_all(&mut z, body.as_bytes()).unwrap(); + let packed = z.finish().unwrap(); + out.extend_from_slice( + format!("8 0 obj\n<>\nstream\n", header.len(), packed.len()).as_bytes(), + ); + out.extend_from_slice(&packed); + out.extend_from_slice(b"\nendstream\nendobj\n"); + let xref = out.len(); + let mut rows = Vec::new(); + let mut row = |kind: u8, field: usize, idx: u16| { + rows.push(kind); + rows.extend_from_slice(&u32::try_from(field).unwrap().to_be_bytes()); + rows.extend_from_slice(&idx.to_be_bytes()); + }; + row(0, 0, 65535); + for off in &offsets { + row(1, *off, 0); + } + row(0, 0, 0); + row(2, 8, 0); + row(2, 8, 1); + row(1, objstm, 0); + row(1, xref, 0); + out.extend_from_slice(format!("9 0 obj\n<>\nstream\n", rows.len()).as_bytes()); + out.extend_from_slice(&rows); + out.extend_from_slice(format!("\nendstream\nendobj\nstartxref\n{xref}\n%%EOF\n").as_bytes()); + let issues = check_pdfx4(&out); + assert!(!issues.iter().any(|i| i == NO_INTENT || i == NO_INFO), "{issues:?}"); +} diff --git a/crates/pdf/src/pdfx1a.rs b/crates/pdf/src/pdfx1a.rs index 88759b071..495a389d8 100644 --- a/crates/pdf/src/pdfx1a.rs +++ b/crates/pdf/src/pdfx1a.rs @@ -116,16 +116,18 @@ pub fn check_pdfx1a(pdf: &[u8]) -> Vec { fn checks(pdf: &[u8]) -> (Vec, Vec) { let mut issues = Vec::new(); let text = String::from_utf8_lossy(pdf); + let file = hayro_syntax::Pdf::new(pdf.to_vec()).ok(); if !pdf.starts_with(b"%PDF-1.") || pdf.get(7).is_none_or(|v| *v > b'4') { issues.push("the PDF version must be 1.4 or lower".into()); } - if !text.contains("/S/GTS_PDFX") { + if file.as_ref().is_none_or(|f| crate::pdfx::pdfx_intents(f).is_empty()) { issues.push("no PDF/X output intent".into()); } - if !text.contains(&format!("/GTS_PDFXVersion({VERSION})")) { + let info = |key: &[u8]| file.as_ref().and_then(|f| crate::pdfx::info_text(pdf, f, key)); + if info(b"GTS_PDFXVersion").as_deref() != Some(VERSION) { issues.push(format!("the document information doesn't name {VERSION}")); } - if !text.contains(&format!("/GTS_PDFXConformance({CONFORMANCE})")) { + if info(b"GTS_PDFXConformance").as_deref() != Some(CONFORMANCE) { issues.push(format!("the document information doesn't name the conformance level {CONFORMANCE}")); } if !text.contains("/Trapped/False") && !text.contains("/Trapped/True") && !text.contains("/Trapped /False") && !text.contains("/Trapped /True") { @@ -142,18 +144,18 @@ fn checks(pdf: &[u8]) -> (Vec, Vec) { if pages > 0 && trims < pages { issues.push("every page needs a trim box".into()); } - let (content, fonts) = content(pdf); + let (content, fonts) = content(file.as_ref()); issues.extend(fonts); (content, issues) } /// Transparency and color spaces, then the output intent and fonts, walked through every page's /// content and what it uses. -fn content(pdf: &[u8]) -> (Vec, Vec) { - let Ok(file) = hayro_syntax::Pdf::new(pdf.to_vec()) else { +fn content(file: Option<&hayro_syntax::Pdf>) -> (Vec, Vec) { + let Some(file) = file else { return (vec!["the file couldn't be read to check its transparency and color spaces".into()], Vec::new()); }; - let mut fonts = intent_issues(&file); + let mut fonts = intent_issues(file); let mut found: Vec<(String, Vec)> = Vec::new(); let mut font_found: Vec<(String, Vec)> = Vec::new(); for (i, page) in file.pages().iter().enumerate() { @@ -214,13 +216,9 @@ impl Intent { /// The catalog's `GTS_PDFX` output intents. fn pdfx_intents(file: &hayro_syntax::Pdf) -> Vec { - let xref = file.xref(); - let Some(catalog) = xref.get::>(xref.root_id()) else { return Vec::new() }; - let Some(intents) = catalog.get::>(b"OutputIntents") else { return Vec::new() }; let number = |n: i32| usize::try_from(n).ok(); - intents - .iter::>() - .filter(|oi| oi.get::>(b"S").is_some_and(|s| s.as_ref() == b"GTS_PDFX")) + crate::pdfx::pdfx_intents(file) + .into_iter() .map(|oi| { let text = |key: &[u8]| { oi.get::>(key) diff --git a/crates/pdf/src/x1a_tests.rs b/crates/pdf/src/x1a_tests.rs index 777d1e2a6..c79aaa172 100644 --- a/crates/pdf/src/x1a_tests.rs +++ b/crates/pdf/src/x1a_tests.rs @@ -318,3 +318,13 @@ fn x1a_dropped_inks_warning_says_they_wont_print() { assert!(warnings.iter().any(|w| w.contains("3 tint(s)") && w.contains("dropped and won't print")), "{warnings:?}"); assert!(warnings.iter().all(|w| !w.contains("converted to process")), "{warnings:?}"); } + +/// The output intent and the identification are read as objects: whitespace between tokens (as +/// qpdf and Ghostscript write them) doesn't hide them. +#[test] +fn x1a_checks_ignore_whitespace_between_tokens() { + let mut objects = tiny_objects("/Resources<<>>", "0 0 0 1 k", "/OutputConditionIdentifier(Custom)/DestOutputProfile 4 0 R", &icc_header(2)); + objects[5] = b"<< /GTS_PDFXVersion (PDF/X-1:2003) /GTS_PDFXConformance (PDF/X-1a:2003) /Trapped /False >>".to_vec(); + objects[6] = b"<< /Type /OutputIntent /S /GTS_PDFX /OutputConditionIdentifier (Custom) /DestOutputProfile 4 0 R >>".to_vec(); + assert_eq!(check_pdfx1a(&assemble(&objects)), Vec::::new()); +} From 8832b0cc91d64bf83485b46e09af072b299c794d Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 12:25:44 +0000 Subject: [PATCH 49/75] Keep Color::to_hsb hue below 360 Found while testing crates/color/src/lib.rs:115 (Color::to_hsb, the `max == r` branch) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/color/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index d085c5c48..c83d93f0c 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -118,6 +118,8 @@ impl Color { } else { 60.0 * ((r - g) / d + 4.0) }; + // A tiny negative `rem_euclid` input can round up to exactly 6.0 (hue 360). + let h = if h >= 360.0 { 0.0 } else { h }; let s = if max <= 0.0 { 0.0 } else { d / max }; [h, s, max] } @@ -251,6 +253,12 @@ impl Paint { mod tests { use super::*; + #[test] + fn hsb_hue_stays_below_360() { + let h = Color::rgb(1.0, 0.0, 1e-8).to_hsb()[0]; + assert!((0.0..360.0).contains(&h), "hue {h}"); + } + #[test] fn hex_roundtrip() { let c = Color::from_hex("#ff8000").unwrap(); From 9caae9c542ada7656986ecb87d32a89e8b51e6bd Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 14:07:33 +0100 Subject: [PATCH 50/75] Drop XML-forbidden control characters from math SVG text Found while testing crates/engine/src/math.rs:909 (to_svg, the `esc` closure used for each glyph) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/math.rs | 4 ++++ crates/engine/src/math/tests.rs | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/crates/engine/src/math.rs b/crates/engine/src/math.rs index 468ce1c5a..6cfd5e2e9 100644 --- a/crates/engine/src/math.rs +++ b/crates/engine/src/math.rs @@ -916,6 +916,10 @@ pub fn to_svg(latex: &str, size: f64) -> (Vec, (f64, f64)) { let face = f.face(c, *italic); let fam = if std::ptr::eq(face, &*f.fallback) { FALLBACK } else { FAMILY }; let it = *italic && !std::ptr::eq(face, &*f.fallback); + // XML 1.0 forbids most control characters and U+FFFE/U+FFFF. + if !matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}') { + continue; + } let _ = write!( svg, "{}", diff --git a/crates/engine/src/math/tests.rs b/crates/engine/src/math/tests.rs index 9d1e75884..bd35c74bd 100644 --- a/crates/engine/src/math/tests.rs +++ b/crates/engine/src/math/tests.rs @@ -474,3 +474,10 @@ fn deep_nodes_drop_when_replaced_or_owned_by_unwinding_continuations() { }); assert!(result.is_err(), "the intentional panic must actually unwind the owned frames"); } + +#[test] +fn svg_drops_control_characters_forbidden_by_xml() { + let (svg, _) = to_svg("a\u{1}b\u{c}", 12.0); + let svg = String::from_utf8(svg).unwrap(); + assert!(!svg.chars().any(|c| c < ' ' && !matches!(c, '\t' | '\n' | '\r')), "control character in SVG: {svg:?}"); +} From f0c86e77e042f1c22b25952bb85614ee9428926f Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sat, 31 Oct 2026 11:26:05 -0400 Subject: [PATCH 51/75] file.openBytes: title a .designcraft file without its extension file.openBytes used the name it was given as the document's title as is. The IDML path strips .idml/.indd/.indt, but a native document opened as "Brochure.designcraft" kept the extension, and everything that adds it back doubled it: Save in the browser downloaded Brochure.designcraft.designcraft, Save As suggested that name, and the File Name variable showed it. The app's own drop and open paths strip the extension before calling, so only hosts and scripts that pass a file name hit it. The title now drops one trailing .designcraft (any case). Other names are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ApvpyStzzHbKyDBNxpzn2Y --- ROADMAP.md | 1 + crates/engine/src/cmd/file/bytes.rs | 10 +++++++++ crates/engine/src/cmd/file/open.rs | 34 +++++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 796ff7306..8443ae9e6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -388,6 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-31** · A `.designcraft` file opened from bytes (`file.openBytes` with its file name, as a browser or host passes it) is titled without the extension, as an IDML file opened that way is. Saving it in the browser downloaded `Brochure.designcraft.designcraft`, and the File Name variable showed the doubled name. No readiness percentage change. - **2026-10-30** · Import warnings about a story have a Show button: it closes the warnings (the status line keeps them) and selects the story's first frame, fitted in the window. `file.open` returns `warningItems`, the frame each IDML or InDesign warning is about (or null), matching the converter's INDD story numbers (`story 30892`, written to IDML as `u78ac`) and IDML `Self` names to the imported stories. No readiness percentage change. - **2026-10-30** · Parent pages: a running-header variable shows its name in angle brackets on the parent itself (it was blank), as InDesign shows it; on the document pages, and in text with no page, it shows its text as before. No readiness percentage change. - **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. diff --git a/crates/engine/src/cmd/file/bytes.rs b/crates/engine/src/cmd/file/bytes.rs index 0946205a8..f42925034 100644 --- a/crates/engine/src/cmd/file/bytes.rs +++ b/crates/engine/src/cmd/file/bytes.rs @@ -27,6 +27,16 @@ pub fn with_document_extension(path: &str) -> String { format!("{path}.{}", designcraft_format::EXTENSION) } +/// `name` without a trailing `.designcraft` (any case): the title of a document opened from bytes +/// under its file name. Save, Save As and the File Name variable add the extension back, so a title +/// that kept it would name the file `Brochure.designcraft.designcraft`. A bare `.designcraft` stays. +pub(super) fn without_native_extension(name: &str) -> &str { + match name.rsplit_once('.') { + Some((stem, ext)) if !stem.is_empty() && ext.eq_ignore_ascii_case(designcraft_format::EXTENSION) => stem, + _ => name, + } +} + /// Whether `bytes` are a document `file.open` reads: a DesignCraft package or an IDML package. /// The first [`DOCUMENT_SNIFF_BYTES`] of a file are enough for packages that start with their /// `mimetype` entry, as both formats write them. diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 75a7ce627..01d268635 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -1,6 +1,6 @@ #[cfg(test)] use super::bytes::{PREVIEW_PX, to_bytes}; -use super::bytes::{base64_decode, load_bytes}; +use super::bytes::{base64_decode, load_bytes, without_native_extension}; use super::{Arc, DocState, Document, EngineError, Result, Session, Value, bad, json, str_param}; pub(super) fn has_recent(s: &Session) -> std::result::Result<(), String> { @@ -150,7 +150,8 @@ pub(super) fn file_open_bytes(s: &mut Session, p: &Value) -> Result { let (mut d, warnings) = load_bytes(&b)?; crate::cmd::datamerge::resolve_sources_on_open(&mut d, None); if let Some(n) = str_param(p, "name") { - d.title = n.to_string(); + // Titled as an IDML file opened from bytes is: without its extension. + d.title = without_native_extension(n).to_string(); } let i = s.add_document(DocState::new(d, None)); Ok(json!({"index": i, "warnings": warnings})) @@ -413,6 +414,35 @@ mod document_fonts_tests { } } +#[cfg(test)] +mod open_bytes_tests { + use serde_json::json; + + use crate::Session; + + /// A `.designcraft` file opened from bytes under its file name (a browser or host passes the + /// name it has) is titled without the extension, as an IDML file is, so saving it gives + /// `Brochure.designcraft` and not `Brochure.designcraft.designcraft`. + #[test] + fn open_bytes_titles_a_native_document_without_its_extension() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let native = s.execute("file.serialize", &json!({})).unwrap(); + for (name, title) in [ + ("Brochure.designcraft", "Brochure"), + ("Brochure.DesignCraft", "Brochure"), + ("Brochure", "Brochure"), + ("Brochure.v2", "Brochure.v2"), + ("Brochure.designcraft.designcraft", "Brochure.designcraft"), + (".designcraft", ".designcraft"), + ] { + let r = s.execute("file.openBytes", &json!({"name": name, "base64": native["base64"]})).unwrap(); + let st = &s.documents()[r["index"].as_u64().unwrap() as usize]; + assert_eq!((st.doc.title.as_str(), st.title()), (title, title.to_string()), "{name}"); + } + } +} + #[cfg(test)] mod recent_tests { use serde_json::json; From 41a64bc10f2a98a2888f310454c1beca499e6a5a Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:35:41 +0000 Subject: [PATCH 52/75] Match placed SVG font-family against legacy family and full names Fixes #2085 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + crates/images/Cargo.toml | 1 + crates/images/src/lib.rs | 78 +++++++++++++++++++++++++++++++------- crates/images/src/tests.rs | 28 ++++++++++++++ 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c89a20002..f63305bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1504,6 +1504,7 @@ dependencies = [ "log", "quick-xml 0.41.0", "resvg", + "skrifa 0.47.0", "tiff", "zune-core", "zune-jpeg", diff --git a/crates/images/Cargo.toml b/crates/images/Cargo.toml index 48dc5955d..685c7d947 100644 --- a/crates/images/Cargo.toml +++ b/crates/images/Cargo.toml @@ -14,6 +14,7 @@ designcraft-fonts = { workspace = true } image = { workspace = true } tiff = { workspace = true } resvg = { workspace = true } +skrifa = { workspace = true } quick-xml = { workspace = true } flate2 = "1" log = { workspace = true } diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 97067ac41..3f9a23d63 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -104,7 +104,10 @@ fn svg_options() -> &'static usvg::Options<'static> { /// family's but a face's PostScript name (as Illustrator's SVG export writes them: /// `font-family="'MyriadPro-Regular'"`) sets the text in that face, whatever font-weight and /// font-style say: the name already picks the weight and style, and a family's other faces would -/// contradict it. Families listed before it come first. +/// contradict it. A name that is a face's legacy family name (name ID 1) or full name (ID 4), as +/// Windows names faces outside the regular/bold/italic quartet (`'Segoe UI Semibold'`, whose +/// typographic family is `Segoe UI`), sets the text in that face (the closest to font-weight and +/// font-style when the legacy family has several). Families listed before it come first. fn select_font() -> usvg::FontSelectionFn<'static> { use usvg::fontdb::{Family, Query, Stretch, Style, Weight}; Box::new(move |font, db| { @@ -112,27 +115,67 @@ fn select_font() -> usvg::FontSelectionFn<'static> { let spelled = |db: &usvg::fontdb::Database, name: &str| { db.faces().flat_map(|f| &f.families).find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(n, _)| n.clone()) }; - for family in font.families() { + // (index in the font-family list, face) of the faces a name names by its legacy names. + let mut legacy: Vec<(usize, usvg::fontdb::ID)> = Vec::new(); + for (i, family) in font.families().iter().enumerate() { if let usvg::FontFamily::Named(name) = family && spelled(db, name).is_none() { if fonts.has_family(name) { for style in fonts.styles(name) { - add_face(db, fonts.face(name, &style)); + let face = fonts.face(name, &style); + let named = has_legacy_name(&face, name); + if let Some(id) = add_face(db, face) + && named + { + legacy.push((i, id)); + } + } + } else if postscript_face(db, name).is_none() { + if let Some((family, style)) = fonts.postscript_face(name) { + add_face(db, fonts.face(&family, &style)); + } else { + // A full name: the family is the name without its last words ("Bold"). + let mut base = name.as_str(); + for _ in 0..3 { + let Some((head, _)) = base.trim_end().rsplit_once(' ') else { break }; + base = head; + if fonts.has_family(base) { + for style in fonts.styles(base) { + let face = fonts.face(base, &style); + if has_legacy_name(&face, name) + && let Some(id) = add_face(db, face) + { + legacy.push((i, id)); + } + } + break; + } + } } - } else if postscript_face(db, name).is_none() - && let Some((family, style)) = fonts.postscript_face(name) - { - add_face(db, fonts.face(&family, &style)); } } } - for family in font.families() { + let want_style = match font.style() { + usvg::FontStyle::Normal => Style::Normal, + usvg::FontStyle::Italic => Style::Italic, + usvg::FontStyle::Oblique => Style::Oblique, + }; + for (i, family) in font.families().iter().enumerate() { match family { usvg::FontFamily::Named(name) if spelled(db, name).is_none() => { if let Some(id) = postscript_face(db, name) { return Some(id); } + let closest = legacy + .iter() + .filter(|(at, _)| *at == i) + .filter_map(|(_, id)| db.face(*id)) + .min_by_key(|f| (f.style != want_style, f.weight.0.abs_diff(font.weight()))) + .map(|f| f.id); + if closest.is_some() { + return closest; + } } // A family (or generic family) that matches: the query below. _ => break, @@ -173,15 +216,22 @@ fn select_font() -> usvg::FontSelectionFn<'static> { usvg::FontStretch::ExtraExpanded => Stretch::ExtraExpanded, usvg::FontStretch::UltraExpanded => Stretch::UltraExpanded, }; - let style = match font.style() { - usvg::FontStyle::Normal => Style::Normal, - usvg::FontStyle::Italic => Style::Italic, - usvg::FontStyle::Oblique => Style::Oblique, - }; - db.query(&Query { families: &families, weight: Weight(font.weight()), stretch, style }) + db.query(&Query { families: &families, weight: Weight(font.weight()), stretch, style: want_style }) }) } +/// Is `name` (ignoring case) a legacy family name (name ID 1) or full name (ID 4) of `face`? +fn has_legacy_name(face: &designcraft_fonts::FontFace, name: &str) -> bool { + use skrifa::MetadataProvider; + use skrifa::string::StringId; + let Some(f) = face.skrifa() else { return false }; + [StringId::FAMILY_NAME, StringId::FULL_NAME] + .into_iter() + .flat_map(|id| f.localized_strings(id)) + .take(256) + .any(|s| s.chars().map(|c| c.to_ascii_lowercase()).eq(name.chars().map(|c| c.to_ascii_lowercase()))) +} + /// The face in the SVG's font database with PostScript name `name` (ignoring case). fn postscript_face(db: &usvg::fontdb::Database, name: &str) -> Option { db.faces().find(|f| f.post_script_name.eq_ignore_ascii_case(name)).map(|f| f.id) diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index dd09f2577..d69a6f504 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -248,6 +248,34 @@ fn svg_text_finds_fonts_by_postscript_name() { assert_eq!(text_face(r#" font-family="NoSuch-Font""#), "SourceSerif4-Regular"); } +#[test] +fn svg_text_matches_legacy_family_names() { + use designcraft_fonts::testing::{font_with_names, with_table_u16}; + // Laid out as Windows names faces outside the regular/bold/italic quartet (seguisb.ttf): the + // weight is in the legacy family (ID 1) and full name (ID 4), the typographic family (ID 16) + // is the base name. + let face = |names: &[(u16, &str)], weight: u16| with_table_u16(font_with_names(names, &['a', 'b']).unwrap(), b"OS/2", 4, weight).unwrap(); + let fonts = designcraft_fonts::FontDb::global(); + fonts.add_font(face(&[(1, "DC Test Legacy"), (2, "Regular"), (4, "DC Test Legacy Regular"), (6, "DCTestLegacy-Regular")], 400)); + fonts.add_font(face(&[(1, "DC Test Legacy"), (2, "Bold"), (4, "DC Test Legacy Bold"), (6, "DCTestLegacy-Bold")], 700)); + for (name, weight) in [("Semibold", 600), ("Light", 300)] { + let legacy = format!("DC Test Legacy {name}"); + let ps = format!("DCTestLegacy-{name}"); + fonts.add_font(face(&[(1, &legacy), (2, "Regular"), (4, &legacy), (6, &ps), (16, "DC Test Legacy"), (17, name)], weight)); + } + assert_eq!(text_face(r#" font-family="DC Test Legacy Semibold""#), "DCTestLegacy-Semibold"); + assert_eq!(text_face(r#" font-family="dc test legacy light""#), "DCTestLegacy-Light"); + assert_eq!(text_face(r#" font-family="'DC Test Legacy Semibold', sans-serif""#), "DCTestLegacy-Semibold"); + assert_eq!(text_face(r#" font-family="NoSuch Font, 'DC Test Legacy Light', serif""#), "DCTestLegacy-Light"); + // The full name (ID 4) of a face of the quartet. + assert_eq!(text_face(r#" font-family="DC Test Legacy Bold""#), "DCTestLegacy-Bold"); + // The typographic family still picks by weight. + assert_eq!(text_face(r#" font-family="DC Test Legacy" font-weight="600""#), "DCTestLegacy-Semibold"); + assert_eq!(text_face(r#" font-family="DC Test Legacy""#), "DCTestLegacy-Regular"); + // A name nobody has still falls to the serif. + assert_eq!(text_face(r#" font-family="DC Test Legacy Nothing""#), "SourceSerif4-Regular"); +} + /// A minimal Photoshop file: 2×1 RGB, no layers, raw merged image. fn tiny_psd() -> Vec { let mut b = Vec::new(); From 01f291297a09621dd34d895bb55e58a5fad8b4f3 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:37:17 +0100 Subject: [PATCH 53/75] Skip children of hidden groups in EPUB/HTML export Found while testing crates/epub/src/html.rs:47-54 (reading_order: the `it.hidden` check comes after the group's children are pushed) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/epub/src/html.rs | 6 +++--- crates/epub/src/tests.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/epub/src/html.rs b/crates/epub/src/html.rs index ce9d7176c..7f7fbe26f 100644 --- a/crates/epub/src/html.rs +++ b/crates/epub/src/html.rs @@ -45,13 +45,13 @@ pub fn reading_order(doc: &Document) -> Vec { continue; }; let xf = *parent * it.xf; + if it.hidden { + continue; + } let children = it.children(); if !children.is_empty() { stack.push((children.iter(), xf)); } - if it.hidden { - continue; - } let block = match &it.content { Content::Text(tf) => { let Some(story) = doc.story(tf.story) else { continue }; diff --git a/crates/epub/src/tests.rs b/crates/epub/src/tests.rs index 4b0531967..b3865dfc1 100644 --- a/crates/epub/src/tests.rs +++ b/crates/epub/src/tests.rs @@ -977,3 +977,21 @@ fn override_keeps_style_decorations_and_resets_tracking_and_caps() { assert_eq!(css::override_css(&d, "Base", &small), "text-transform: none; font-variant: normal; font-variant: small-caps;"); assert_eq!(css::override_css(&d, "Small", &all), "text-transform: none; font-variant: normal; text-transform: uppercase;"); } + +fn grouped_text(hidden: bool) -> Document { + let mut d = Document::new(&NewDocument::default()); + let lid = d.default_layer(); + d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 300.0, 100.0), lid, "Grouped text", ParaFormat::default()).unwrap(); + let child = Arc::make_mut(&mut d.spreads[0]).items.pop().unwrap(); + let mut g = Item::new(ItemId(d.alloc()), lid, Shape::Group, Default::default()); + g.content = Content::Group { items: vec![child] }; + g.hidden = hidden; + Arc::make_mut(&mut d.spreads[0]).items.push(Arc::new(g)); + d +} + +#[test] +fn hidden_group_children_are_not_exported() { + assert!(html::reading_order(&grouped_text(true)).is_empty()); + assert_eq!(html::reading_order(&grouped_text(false)).len(), 1); +} From 00717534cbf2bb5814e73a5238f7d30906e683a2 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:55:44 +0100 Subject: [PATCH 54/75] Drop text variable markers in Text Only and RTF story export Found while testing crates/textimport/src/export.rs:18 (plain_char) and :177 (rtf) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/textimport/src/export.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/textimport/src/export.rs b/crates/textimport/src/export.rs index 8262e4691..7d614f640 100644 --- a/crates/textimport/src/export.rs +++ b/crates/textimport/src/export.rs @@ -15,7 +15,7 @@ fn plain_char(c: char) -> Option<&'static str> { match c { PAGE_NUMBER | NEXT_PAGE_NUMBER | PREV_PAGE_NUMBER => Some("#"), FORCED_LINE_BREAK => Some("\u{2028}"), - '\u{E000}'..='\u{E0FF}' => None, + '\u{E000}'..='\u{E1FF}' => None, _ => Some(""), } } @@ -174,7 +174,7 @@ pub fn rtf(doc: &Document, story: &Story) -> String { } } PAGE_NUMBER | NEXT_PAGE_NUMBER | PREV_PAGE_NUMBER => out.push('#'), - '\u{E000}'..='\u{E0FF}' => {} + '\u{E000}'..='\u{E1FF}' => {} c => rtf_escape(&mut out, c), } } @@ -201,6 +201,14 @@ mod tests { st } + #[test] + fn text_variable_markers_are_dropped() { + let st = Story::with_text(StoryId(1), "Page \u{E100} of", Default::default()); + assert_eq!(plain_text(&st), "Page of"); + let r = rtf(&Document::new(&NewDocument::default()), &st); + assert!(!r.contains("\\u-7936") && r.contains("Page of"), "{r}"); + } + #[test] fn text_only() { assert_eq!(plain_text(&story()), "Plain bold {x}\r\nCafé\ntwo"); From 98d80160014a11f3455d7bb7e7e429c894f0ebe6 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 31 Oct 2026 18:53:17 -0400 Subject: [PATCH 55/75] packaging(flatpak): add OCI image build and GHCR publishing helper (#2099) --- packaging/linux/flatpak-bundle.sh | 8 ++++ packaging/linux/flatpak-oci.sh | 64 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100755 packaging/linux/flatpak-oci.sh diff --git a/packaging/linux/flatpak-bundle.sh b/packaging/linux/flatpak-bundle.sh index dbb9b699e..40301a1fb 100755 --- a/packaging/linux/flatpak-bundle.sh +++ b/packaging/linux/flatpak-bundle.sh @@ -20,10 +20,12 @@ APP_ID=ai.storyteller.designcraft FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo TEST=1 +OCI=0 TARBALL="" while [[ $# -gt 0 ]]; do case "$1" in --no-test) TEST=0; shift ;; + --oci) OCI=1; shift ;; -h | --help) sed -n '2,14p' "$0"; exit 0 ;; -*) echo "unknown argument: $1" >&2; exit 2 ;; *) TARBALL="$1"; shift ;; @@ -61,6 +63,12 @@ OUT="$DIST/$BASENAME.flatpak" # --runtime-repo lets `flatpak install` fetch the freedesktop runtime from Flathub if missing. flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable echo "wrote $OUT" +if [[ "$OCI" = 1 ]]; then + OUT_OCI="$DIST/$BASENAME-oci" + rm -rf "$OUT_OCI" + flatpak build-bundle --oci "$WORK/repo" "$OUT_OCI" "$APP_ID" stable + echo "wrote OCI layout to $OUT_OCI" +fi if [[ "$TEST" = 1 ]]; then flatpak install --user -y --noninteractive --reinstall "$OUT" diff --git a/packaging/linux/flatpak-oci.sh b/packaging/linux/flatpak-oci.sh new file mode 100755 index 000000000..4a500a2b8 --- /dev/null +++ b/packaging/linux/flatpak-oci.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Build designcraft as an OCI container layout and push it to GitHub Container Registry (GHCR) +# for consumption by Flatpak OCI clients (flatpak-oci-specs). +# +# Usage: +# packaging/linux/flatpak-oci.sh [--push] [TARBALL] +# +# Environment variables: +# REGISTRY Target registry image (default: ghcr.io/storytold/designcraft) +# TAG Target image tag (default: $VERSION-$ARCH, also tags stable-$ARCH) +# GITHUB_TOKEN Token for GHCR authentication when --push is used +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" + +PUSH=0 +TARBALL="" +while [[ $# -gt 0 ]]; do + case "$1" in + --push) PUSH=1; shift ;; + -h | --help) sed -n '2,12p' "$0"; exit 0 ;; + -*) echo "unknown argument: $1" >&2; exit 2 ;; + *) TARBALL="$1"; shift ;; + esac +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) ;; + aarch64 | arm64) ARCH=aarch64 ;; + *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; +esac + +BASENAME="designcraft-$VERSION-linux-$ARCH" +OUT_OCI="$DIST/$BASENAME-oci" + +echo "==> Building Flatpak OCI layout for $BASENAME" +"$HERE/flatpak-bundle.sh" --oci --no-test "${TARBALL:-}" + +[[ -d "$OUT_OCI" ]] || { echo "error: $OUT_OCI layout not generated" >&2; exit 1; } + +REGISTRY="${REGISTRY:-ghcr.io/storytold/designcraft}" +TAG="${TAG:-$VERSION-$ARCH}" + +echo "==> OCI layout ready at $OUT_OCI (Target: $REGISTRY:$TAG)" + +if [[ "$PUSH" = 1 ]]; then + command -v skopeo >/dev/null || { echo "error: skopeo is required to push OCI images (apt install skopeo)" >&2; exit 1; } + + AUTH_ARGS=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + AUTH_ARGS=(--dest-creds "${GITHUB_ACTOR:-storytold}:$GITHUB_TOKEN") + fi + + echo "==> Pushing to $REGISTRY:$TAG" + skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:$TAG" + + # Also update architecture-specific stable tag + echo "==> Pushing to $REGISTRY:stable-$ARCH" + skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:stable-$ARCH" + + echo "==> Push complete" +fi From 008f519c46ae82c281bea921e297b3f2e37d99f2 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 31 Oct 2026 23:43:00 -0400 Subject: [PATCH 56/75] ci: grant packages write permission to release workflow --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 622ace75c..29f36a0d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,6 +63,7 @@ jobs: needs: git-sources permissions: contents: write + packages: write # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/release.yml@main with: From 81d5641252ae3d0caf6d03e69a8ed55238329abe Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 01:55:46 -0500 Subject: [PATCH 57/75] Contributors: credit Tom-R-Main by their GitHub profile name --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index cd1cb022a..2b4a134e0 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -71,3 +71,6 @@ display_name = "skylartaylor" [people.dylanmtaylor] sync_github_name = true + +[people.Tom-R-Main] +sync_github_name = true From becb09900bf01764823315ac5286c984a5f63135 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:26:14 +0100 Subject: [PATCH 58/75] HU1: keep oversized tool flyouts scrollable --- crates/ui-egui/src/toolbar.rs | 92 +++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 09194e1b6..0331074e7 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -193,49 +193,57 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { .x; width.max(label_width + shortcut_width + 46.0) }); - 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); + let max_width = (ui.ctx().content_rect().width() + - egui::Frame::popup(ui.style()).total_margin().sum().x) + .max(0.0); + egui::ScrollArea::horizontal().id_salt(("tool-flyout-scroll", gi)).max_width(max_width).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); + } + }); } - 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); - } - }); - } + }, + ); }); }, ); From 7c42fd74e37ca8eb067eaaa5db9620d24f5c52f9 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:26:50 +0100 Subject: [PATCH 59/75] HU1: cover Hungarian flyouts in narrow viewports --- crates/ui-egui/src/toolbar/tests.rs | 95 ++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/toolbar/tests.rs b/crates/ui-egui/src/toolbar/tests.rs index a7692ae24..3598cf301 100644 --- a/crates/ui-egui/src/toolbar/tests.rs +++ b/crates/ui-egui/src/toolbar/tests.rs @@ -24,21 +24,21 @@ fn rect(h: &egui_kittest::Harness<'static, test_window::Window>, label: &str) -> 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}; - /// 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)), - _ => {} - } - } - for double in [false, true] { let mut app = app(double); app.ui.language = "hu".into(); @@ -113,6 +113,83 @@ fn hungarian_tool_buttons_translate_every_tool_and_keep_shortcuts() { } } +/// Keeps long translated flyout rows reachable by scrolling in a narrow viewport, +/// preserving the custom shortcut and selection through actual pointer input. +#[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"); + } 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] From e33d870d62d1467f7ee8b152855c4753ee5dd395 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:27:23 +0100 Subject: [PATCH 60/75] HU1: verify Hungarian glyphs in bundled UI fonts Removed specific assertions for Georgian translations and added a new test for Hungarian catalog glyphs in bundled UI fonts. --- crates/ui-egui/src/i18n/tests.rs | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index c8dc8578f..a954fd376 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -61,9 +61,6 @@ fn cached_lookup_preserves_every_translation_across_language_switches() { assert_eq!(tr("", key), *key); } } - // Keep the repaired Georgian menu captions present with their exact translations. - assert_eq!(tr("ka", "File Info…"), "ფაილის ინფორმაცია…"); - assert_eq!(tr("ka", "Check for Updates…"), "განახლებების შემოწმება…"); // Strings no catalog has stay as they are, in every language. let unknown = String::from("A user-defined untranslated label"); for lang in LANGUAGES.iter().map(|(code, _)| *code).chain(["en", "unknown"]) { @@ -118,6 +115,81 @@ fn hungarian_catalog_preserves_placeholders_context_and_counts() { 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] From 1f347dac44ba1a6836c0d337e04e39e38f4d6f16 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:28:01 +0100 Subject: [PATCH 61/75] HU1: remove PR-only Georgian menu checks Removed the test for Georgian menu label translations. --- crates/ui-egui/src/menus/tests.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index cd46a6930..60c19806a 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -686,30 +686,6 @@ fn show_document_grid_toggles_from_the_grids_and_guides_menu() { assert!(!app.ui.document_grid); } -/// Checks every Georgian menu title, item and submenu, allowing language-neutral labels. -fn assert_georgian_menu_labels_are_translated() { - // Every menu title, item and submenu is translated into Georgian. - let mut untranslated = Vec::new(); - for (title, entries) in menu_tree() { - let mut labels = vec![title.to_string()]; - let mut stack: Vec<&Item> = entries.iter().collect(); - while let Some(entry) = stack.pop() { - match entry { - Item::Sub(label, children) => { - labels.push(label.clone()); - stack.extend(children.iter()); - } - Item::Cmd { label, id, .. } if id != "app.language" => labels.push(label.clone()), - _ => {} - } - } - // Labels without letters ("180°") or point sizes ("72 pt") read the same in every language. - let size = |l: &str| l.strip_suffix(" pt").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit())); - untranslated.extend(labels.into_iter().filter(|l| l.chars().any(char::is_alphabetic) && !size(l) && crate::i18n::tr("ka", l) == l.as_str())); - } - assert!(untranslated.is_empty(), "untranslated Georgian menu labels: {untranslated:?}"); -} - /// Checks language selection and rejection of unknown language codes, Preferences dispatch, /// translated menu labels and preservation of document data when switching to Arabic. #[test] @@ -770,11 +746,6 @@ fn interface_language_switches_and_validates() { assert_ne!(crate::i18n::tr("it", title), title, "{title}"); } } - - run_ui(&mut app, "app.language", &json!({"lang": "ka"})).unwrap().unwrap(); - assert_eq!(app.ui.language, "ka"); - assert_eq!(checked(&app, "app.language", &json!({"lang": "ka"})), Some(true)); - assert_georgian_menu_labels_are_translated(); } /// Checks Hungarian selection, menu captions and saved UI preferences without changing From 218c71af5c0eacba47bff96c2dec163f5eda7928 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:28:37 +0100 Subject: [PATCH 62/75] HU1: summarize Hungarian progress in one entry Removed outdated Hungarian localization entries and integrated new ones while retaining existing tests and assertions. --- ROADMAP.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 2caa87565..e74fdbb4b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -393,28 +393,18 @@ Newest first. - **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. - **2026-10-30** · File > Package opens a dialog, as the reference app's: a Summary of fonts, links and images and inks, pages for each, a Printing Instructions step, and a PDF made with the options of a preset (#1514, #1515). - **2026-10-30** · IDML import reads TOC separators and entry styles written with the `$ID/` prefix or as `Properties` children, and the TOC separator notation (`^t`, `^y`, `^m`, `^>`) is shared by the dialog and IDML, with a special-character menu on the separator field (#1249, #1255). -- **2026-10-30** · HU1: resolve the progress-log conflict against current main while retaining both branches’ entries, all Hungarian catalog contents and localization/menu/toolbar regression tests. Adopt upstream’s current test harness and toolbar proxy refactor; translate Show/Hide Depth Ruler (1,906 Hungarian entries and four context captions). Deduplicate the two identical Georgian menu captions now supplied by main; retain their assertions. No readiness percentage change. - **2026-10-29** · Inserting text with many returns no longer takes time that grows with their square (part of #1588): each return copied its paragraph's format with its own insert, moving every format after it. Splitting 20,000 returns into paragraphs took 15.8 s; placing a text file of them now takes 0.2 s, and one of a million returns 5.5 s. No readiness percentage change. - **2026-10-28** · A placed image whose pixels would decode past 1 GiB of RGBA (256 Mpx) is refused from its header, before decoding (#1586). A 288 KB 23,000 px gray PNG used to hang the first draw for 9 s and take the app to 4.5 GB; it now draws as an empty frame, and PDF export skips it with a warning. A 300 ppi A0 scan still fits. No readiness percentage change. - **2026-10-28** · Discretionary hyphens (U+00AD) break in the single-line composer with automatic hyphenation on or off, matching the paragraph composer. Unbroken soft hyphens remain invisible and zero-width, and words containing them keep only their discretionary break points (#1955). No readiness percentage change. - **2026-10-28** · Autoflow no longer sizes the rest of a story by a frame that a Start Paragraph option (next column, frame, page, or odd or even page) ends early (#1209). With a title before a chapter that starts on the next odd page, it estimated a page per few characters, added up to 2000 pages (the autoflow limit) and removed them again: placing 2000 words took 36 s in the app, now 0.4 s, with the same pages. It looks past tracked-deleted paragraphs for the option, as composing does, and asks for every frame that start options and breaks force in one round: 200 one-line chapters on their own pages take 9 ms (58 ms before). A break and the start option it meets count once, and breaks that tracked changes delete or a hidden condition hides don't count. No readiness percentage change. - **2026-10-28** · IDML export writes the document's default `StoryPreference` in `Resources/Preferences.xml`, as InDesign does. Only each story carried one, and IDML readers that take the spread reading order from the document's (Okapi's IDML filter) failed on DesignCraft's files. No readiness percentage change. - **2026-10-27** · An iconic panel's flyout stays open while you work in the canvas or a text field: Auto-Collapse Iconic Panels is off by default, and preferences saved with it on start with it off. Preferences › Interface › Panels holds the setting next to the column bar's menu item. No readiness percentage change. -- **2026-10-27** · HU1: integrate Hungarian localization with the shared module layout; retain both parents' localization, menu, toolbar and panel-docking tests and all Hungarian catalog entries. Adapt toolbar tests and flyout sizing to platform-formatted and user-assigned shortcuts; add direct lookup assertions for the repaired Georgian menu captions. No readiness percentage change. - **2026-10-26** · Inner Shadow, Inner Glow (from the edges) and Bevel and Emboss on a frame holding an image follow what the image paints: a transparent part is outside the object, so the effect lines the image's opaque edge, not the frame (#1622). Satin and a centred Inner Glow still follow the frame. No readiness percentage change. -- **2026-10-25** · HU1: extend Hungarian toolbar regression coverage through real right-click flyouts in both column layouts, checking translated row labels, painted shortcuts, clipping and overlap. Existing assertions and runtime behavior are unchanged. No readiness percentage change. -- **2026-10-25** · HU1: retain both branches' localization and toolbar regression tests while adopting the updated egui button role; translate Data Merge Arrangement as Elrendezés (1,905 Hungarian entries and four context captions). No readiness percentage change. -- **2026-10-25** · HU1: add the two missing Georgian File Info and Check for Updates menu captions while retaining the complete-menu regression unchanged. No readiness percentage change. - **2026-10-24** · Column rules run from the top line's ascent to the bottom line's descent, as InDesign draws them, instead of from the top to the bottom of the columns, on screen, in PDF and in PowerPoint export. The top and bottom insets shorten the rule from those lines. Existing documents draw their column rules at the new length. No readiness percentage change. -- **2026-10-24** · HU1: document localization lookup, Tools-panel rendering and language, menu-path and tooltip regression tests. Documentation only; runtime behavior, translation catalogs and test assertions are unchanged. No readiness percentage change. -- 2026-10-23 (HU1): repair localization test integration after merging main: restore separate all-catalog integrity, context-caption and numeral/count tests, retaining assertions from both branches; register Hungarian in the shared language and context catalogs. Retain 17 live labels missing from the known-label test list and remove 13 unused catalog entries (1,904 entries + four context captions remain). - -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-20** · HU1: Hungarian review corrections distinguish numbers/counts and text/grid columns (four context captions), consistently use formal address, and align menu paths with visible labels. Added context and menu-path regression coverage; corrected localization documentation. 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: 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-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. From 3e8fbbfee8122865dc4f0a58b8d4e6959a1b220b Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:29:21 +0100 Subject: [PATCH 63/75] HU1: document native review and UI font coverage Updated Hungarian interface catalog with new entries and context-specific captions, reflecting recent reviews and regressions. --- docs/localization-parity.md | 71 +++++++++++-------------------------- 1 file changed, 21 insertions(+), 50 deletions(-) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 697220dce..361aa27eb 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-30 · **Change:** minor (current-main integration with retained catalogs and regression tests; older catalog counts retain their dated 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), @@ -54,55 +54,32 @@ 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. -Review corrections (2026-10-20) distinguish numbers from counts and text columns from grid -columns, use formal Hungarian address consistently, and align translated menu paths with -their visible labels. These corrections do not add new catalog keys or change coverage. -Test integration (2026-10-23) registers Hungarian in the shared catalog checks and restores -assertions from both merge parents. That snapshot has **1,904 entries and four context -captions** after removing 11 untranslated interface-language names (the selector displays their -native names directly) and two unused internal tint keys. Seventeen live UI labels are retained -and added to the integrity test's known-label list. No used Hungarian translation was removed. +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. -Data Merge follow-up (2026-10-25): `Arrangement` is translated as `Elrendezés`, bringing the -Hungarian catalog to **1,905 entries and four context captions**. The shared all-language -arrangement check and the Hungarian catalog and toolbar tests are retained alongside the -main-branch egui API update. This is a targeted addition, not a new coverage measurement. +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. -Georgian menu follow-up (2026-10-25): `File Info…` and `Check for Updates…` now have -Georgian catalog entries. The existing complete-menu translation assertion is unchanged. -This targeted addition retains the dated coverage figures below; the two new captions have -not had a separate native-speaker review. - -Module-layout integration (2026-10-27): catalog lookup stays in `i18n.rs`, while the shared -table and localization tests live in `i18n/table.rs` and `i18n/tests.rs`. Menu and toolbar -regressions from both branches are retained in `menus/tests.rs` and `toolbar/tests.rs`. -Hungarian catalog contents and the dated coverage measurements are unchanged. Toolbar -coverage follows the current platform-specific keymap captions and checks a user-assigned -chord alongside translated flyout text; row sizing measures the displayed shortcut. - -Main integration (2026-10-30): retain every prior Hungarian entry and both branches’ -localization, menu and toolbar regressions while adopting upstream’s current file-dialog test -harness and toolbar proxy refactor. Add `Show/Hide Depth Ruler` as `Mélységmérő -megjelenítése/elrejtése`, bringing the catalog to **1,906 entries and four context captions**. -The upstream all-language depth-ruler assertion is retained. The unchanged complete Georgian -menu assertions run through a helper so the existing language-switch test meets upstream’s -new complexity limit. The two Georgian menu captions now also exist in main; retain each -identical translation once, keeping the direct lookup and complete-menu assertions. The -progress log keeps both branches’ dated entries; this targeted addition is not a new coverage -measurement. +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-10-30); initial catalog native-reviewed | partial | +| 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 | @@ -139,13 +116,7 @@ German variants have no automatic hyphenation; their German shaping and quotes a | Date | Change | Summary | |---|---|---| -| 2026-10-30 | minor | Integrate current main; retain both branches’ entries/tests; add Hungarian Depth Ruler caption (1,906 entries + four context captions) | -| 2026-10-27 | minor | Adopt shared module layout; preserve both parents' tests and all Hungarian catalog entries | -| 2026-10-25 | minor | Hungarian Data Merge arrangement caption added; 1,905 entries and four context captions; both merge parents' tests retained | -| 2026-10-25 | minor | Georgian File Info and Check for Updates captions added; complete-menu regression retained | -| 2026-10-20 | minor | Hungarian review: four context captions, formal address, consistent menu paths; corrected other-language count to six | -| 2026-10-17 | minor | Hungarian (`hu`) interface added: 1,917 entries and two context captions; initial native-speaker review | -| 2026-10-23 | minor | Hungarian test integration restored; 1,904 active catalog entries and four context captions after removing unused keys | +| 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 | From 406e2bf7b3c5d2f3a836589c3df7556a33a15993 Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:24:23 +0100 Subject: [PATCH 64/75] HU1: reset tool flyout scrolling when reopened --- crates/ui-egui/src/toolbar.rs | 92 ++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 0331074e7..d909d6a04 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -196,54 +196,56 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { let max_width = (ui.ctx().content_rect().width() - egui::Frame::popup(ui.style()).total_margin().sum().x) .max(0.0); - egui::ScrollArea::horizontal().id_salt(("tool-flyout-scroll", gi)).max_width(max_width).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); - } + 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.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, + r.right_center() - vec2(6.0, 0.0), + egui::Align2::RIGHT_CENTER, + sc, + egui::FontId::proportional(11.5), + t.text_dim, ); - 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); - } + } + 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); + } + }); + } + }); }); }, ); From 667ccc078361c37fddd43ccba2a97f72ade2b6fd Mon Sep 17 00:00:00 2001 From: macadminHU <51995175+macrobi@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:25:33 +0100 Subject: [PATCH 65/75] HU1: cover flyout scroll reset after reopening Updated test for Hungarian flyout to ensure it resets correctly on reopening while preserving scroll position, shortcut, and selection. --- crates/ui-egui/src/toolbar/tests.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/toolbar/tests.rs b/crates/ui-egui/src/toolbar/tests.rs index 3598cf301..df06bd020 100644 --- a/crates/ui-egui/src/toolbar/tests.rs +++ b/crates/ui-egui/src/toolbar/tests.rs @@ -114,7 +114,7 @@ fn hungarian_tool_buttons_translate_every_tool_and_keep_shortcuts() { } /// Keeps long translated flyout rows reachable by scrolling in a narrow viewport, -/// preserving the custom shortcut and selection through actual pointer input. +/// 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}; @@ -166,6 +166,32 @@ fn hungarian_flyout_scrolls_horizontally_in_narrow_windows() { 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)); } From 7b8858d08ce5b3e6428f27e0725090cf9bee1c21 Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Sun, 1 Nov 2026 16:53:00 +0100 Subject: [PATCH 66/75] Contributors: key y-az as the pulls and commits APIs give the login --- contributors/people.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors/people.toml b/contributors/people.toml index ec7a75f01..3828d9b1d 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -72,6 +72,6 @@ real_name = "Paul Gemignani" [people.skylartaylor] display_name = "skylartaylor" -[people.Y-az] +[people.y-az] real_name = "Youssef Azriouil" display_name = "youssef" From f3f23718dc31249b413db52abd746951e310db7c Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Sun, 1 Nov 2026 17:26:45 +0100 Subject: [PATCH 67/75] Dutch hyphenation: test long compound words --- crates/compose/src/hyphen.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/compose/src/hyphen.rs b/crates/compose/src/hyphen.rs index 37f7ae598..5caad033b 100644 --- a/crates/compose/src/hyphen.rs +++ b/crates/compose/src/hyphen.rs @@ -451,6 +451,11 @@ mod tests { assert_eq!(h("leesvaardigheid"), "lees-vaar-dig-heid"); assert_eq!(h("zelfvertrouwen"), "zelf-ver-trou-wen"); assert_eq!(h("Ontwikkelen"), "Ont-wik-ke-len"); + // Long compounds break at their parts and at the syllables inside them. + assert_eq!(h("arbeidsongeschiktheidsverzekering"), "ar-beids-on-ge-schikt-heids-ver-ze-ke-ring"); + assert_eq!(h("ziekenhuisopname"), "zie-ken-huis-op-na-me"); + assert_eq!(h("gemeenteraadsverkiezingen"), "ge-meen-te-raads-ver-kie-zin-gen"); + assert_eq!(h("kinderdagverblijf"), "kin-der-dag-ver-blijf"); } #[test] From 021b3fcaa44633f4f65d5d3324294c69ac153318 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 1 Nov 2026 12:29:57 -0500 Subject: [PATCH 68/75] docs: sync Craft App Suite list with ArtCraft Launcher --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2f065153..37e67cfa4 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,16 @@ stand on its own. | | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) | | | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) | | | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) | -| | **DesignCraft** | **Page layout and publishing · you are here** | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) | +| | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) | +| | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) | +| | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) | +| | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) | +| | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) | +| | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) | +| | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) | +| | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) | +| | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) | +| | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | | And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control. From 700c4bb344af3ba3e9fa0bd663778c61e8779329 Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 13:31:07 -0500 Subject: [PATCH 69/75] file.openBytes: test titles that aren't ASCII Names with characters outside ASCII, one shorter than the extension, lose a trailing .designcraft or keep their names like the others. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ApvpyStzzHbKyDBNxpzn2Y --- crates/engine/src/cmd/file/open.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 01d268635..4e90f30fb 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -435,6 +435,12 @@ mod open_bytes_tests { ("Brochure.v2", "Brochure.v2"), ("Brochure.designcraft.designcraft", "Brochure.designcraft"), (".designcraft", ".designcraft"), + // Names that aren't ASCII: cutting the extension's length off the end of the last one + // would land inside its `é`. + ("Präsentation.designcraft", "Präsentation"), + ("日本.DESIGNCRAFT", "日本"), + ("é", "é"), + ("édesigncraft", "édesigncraft"), ] { let r = s.execute("file.openBytes", &json!({"name": name, "base64": native["base64"]})).unwrap(); let st = &s.documents()[r["index"].as_u64().unwrap() as usize]; From 1fe8885bda7451263879cfbb256b9a3d46c411bf Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 1 Nov 2026 13:58:16 -0500 Subject: [PATCH 70/75] Release 0.16.0 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 42 +++++++++++++++++++++--------------------- Cargo.toml | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c89a20002..fb4b2627c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "designcraft" -version = "0.15.2" +version = "0.16.0" dependencies = [ "arboard", "craft-update", @@ -1330,7 +1330,7 @@ dependencies = [ [[package]] name = "designcraft-cli" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1346,7 +1346,7 @@ dependencies = [ [[package]] name = "designcraft-color" -version = "0.15.2" +version = "0.16.0" dependencies = [ "craft-cms", "kurbo", @@ -1358,7 +1358,7 @@ dependencies = [ [[package]] name = "designcraft-compose" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-color", "designcraft-doc", @@ -1378,7 +1378,7 @@ dependencies = [ [[package]] name = "designcraft-doc" -version = "0.15.2" +version = "0.16.0" dependencies = [ "chrono", "designcraft-color", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "designcraft-engine" -version = "0.15.2" +version = "0.16.0" dependencies = [ "caseless", "craft-atomic", @@ -1431,7 +1431,7 @@ dependencies = [ [[package]] name = "designcraft-epub" -version = "0.15.2" +version = "0.16.0" dependencies = [ "chrono", "designcraft-color", @@ -1441,7 +1441,7 @@ dependencies = [ [[package]] name = "designcraft-fonts" -version = "0.15.2" +version = "0.16.0" dependencies = [ "core-text", "harfrust 0.13.3", @@ -1454,7 +1454,7 @@ dependencies = [ [[package]] name = "designcraft-format" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-doc", "designcraft-images", @@ -1466,7 +1466,7 @@ dependencies = [ [[package]] name = "designcraft-geom" -version = "0.15.2" +version = "0.16.0" dependencies = [ "flo_curves", "kurbo", @@ -1479,7 +1479,7 @@ dependencies = [ [[package]] name = "designcraft-idml" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1494,7 +1494,7 @@ dependencies = [ [[package]] name = "designcraft-images" -version = "0.15.2" +version = "0.16.0" dependencies = [ "craft-cms", "designcraft-color", @@ -1511,7 +1511,7 @@ dependencies = [ [[package]] name = "designcraft-mcp" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-engine", "designcraft-geom", @@ -1524,7 +1524,7 @@ dependencies = [ [[package]] name = "designcraft-pdf" -version = "0.15.2" +version = "0.16.0" dependencies = [ "chrono", "craft-cms", @@ -1550,7 +1550,7 @@ dependencies = [ [[package]] name = "designcraft-pptx" -version = "0.15.2" +version = "0.16.0" dependencies = [ "chrono", "designcraft-color", @@ -1568,7 +1568,7 @@ dependencies = [ [[package]] name = "designcraft-render" -version = "0.15.2" +version = "0.16.0" dependencies = [ "craft-pdf", "designcraft-color", @@ -1587,7 +1587,7 @@ dependencies = [ [[package]] name = "designcraft-textimport" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-doc", "quick-xml 0.41.0", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "designcraft-tools" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1608,7 +1608,7 @@ dependencies = [ [[package]] name = "designcraft-ui-egui" -version = "0.15.2" +version = "0.16.0" dependencies = [ "craft-dialogs", "craft-keymap", @@ -1640,7 +1640,7 @@ dependencies = [ [[package]] name = "designcraft-web" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-engine", "designcraft-format", @@ -7604,7 +7604,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" [[package]] name = "xtask" -version = "0.15.2" +version = "0.16.0" dependencies = [ "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 726874c06..138a07985 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/designcraft", "apps/designcraft-cli", "xtask"] [workspace.package] -version = "0.15.2" +version = "0.16.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" From 79408dd54fd25eefeedc56d061a5facb8ff8d812 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:05:37 +0100 Subject: [PATCH 71/75] Zoom In/Out shortcuts zoom around the pointer over the canvas (#2075) --- crates/ui-egui/src/menus/invoke.rs | 15 ++++++ crates/ui-egui/src/menus/keys.rs | 14 ++++-- crates/ui-egui/src/menus/run.rs | 6 ++- crates/ui-egui/src/menus/tests.rs | 67 +++++++++++++++++++++++++ crates/ui-egui/src/menus/ui_commands.rs | 4 +- 5 files changed, 98 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index 16c7bc191..b9d35539d 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -151,9 +151,24 @@ pub fn activate_native(app: &mut DesignApp, ctx: &egui::Context, id: &str, param if params.is_null() && app.session.wants_text() && matches!(id, "edit.copy" | "edit.cut" | "edit.paste") && forward_to_text_edit(app, ctx, id) { return; } + // Zoom In/Out by key equivalent: the pointer is still in the window. A click in the menu bar + // takes it out, so that zooms around the centre. + if params.is_null() + && let Some(p) = pointer_zoom_params(ctx, id) + { + activate(app, id, &p); + return; + } activate(app, id, params); } +/// Zoom In/Out from a key: the pointer's position (`at`), so the zoom keeps the point under it +/// when it is over the canvas. `None` for other commands or with no pointer in the window. +pub(super) fn pointer_zoom_params(ctx: &egui::Context, id: &str) -> Option { + let at = ctx.pointer_hover_pos()?; + matches!(id, "view.zoomIn" | "view.zoomOut").then(|| json!({"at": [at.x, at.y]})) +} + /// The editing commands a focused text field takes from the native menu bar. pub(super) const TEXT_FIELD_COMMANDS: [&str; 6] = ["edit.selectAll", "edit.copy", "edit.cut", "edit.paste", "edit.undo", "edit.redo"]; diff --git a/crates/ui-egui/src/menus/keys.rs b/crates/ui-egui/src/menus/keys.rs index b7e1fa539..8b2179469 100644 --- a/crates/ui-egui/src/menus/keys.rs +++ b/crates/ui-egui/src/menus/keys.rs @@ -1,4 +1,4 @@ -use super::invoke::activate; +use super::invoke::{activate, pointer_zoom_params}; use super::{DesignApp, ScreenMode, Value, json}; /// The command a paste event stands for besides plain Paste. egui-winit turns every ⌘+V chord into @@ -39,7 +39,7 @@ pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) { } let typing = app.session.wants_text(); let keys = ctx.input_mut(|i| take_keys(app, i, typing)); - run_keys(app, keys, typing); + run_keys(app, ctx, keys, typing); } /// Keys that a floating panel in its own OS window left over (craft-ui's @@ -59,7 +59,7 @@ pub fn forwarded_shortcuts(app: &mut DesignApp, ctx: &egui::Context, events: Vec input.events = events; input.modifiers = modifiers; let keys = take_keys(app, &mut input, typing); - run_keys(app, keys, typing); + run_keys(app, ctx, keys, typing); crate::canvas::forwarded_keys(app, input.events, modifiers); } @@ -112,7 +112,7 @@ fn take_keys(app: &mut DesignApp, input: &mut egui::InputState, typing: bool) -> } /// Runs what [`take_keys`] took, then Esc's return to the Selection tool. -fn run_keys(app: &mut DesignApp, keys: Keys, typing: bool) { +fn run_keys(app: &mut DesignApp, ctx: &egui::Context, keys: Keys, typing: bool) { for step in keys.presentation { match step { None => { @@ -138,7 +138,11 @@ fn run_keys(app: &mut DesignApp, keys: Keys, typing: bool) { app.select_tool(&tool); } else { // Like choosing the menu item: "…" commands open their dialog. - activate(app, &hit.command.id, &hit.command.params); + // Zoom In/Out by key zoom around the pointer while it is over the canvas. + match pointer_zoom_params(ctx, &hit.command.id) { + Some(p) => activate(app, &hit.command.id, &p), + None => activate(app, &hit.command.id, &hit.command.params), + } } } // Esc with no shortcut of its own goes back to the Selection tool. diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index ee7fdbb5e..f2d871c54 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -575,7 +575,11 @@ fn run_ui_part3(app: &mut DesignApp, id: &str, p: &Value) -> Option { if let Some(r) = rect { - crate::canvas::zoom_at(app, r.center(), if id == "view.zoomIn" { 1.5 } else { 1.0 / 1.5 }); + // A keyboard zoom names the pointer ("at"); it counts only over the canvas. + let xy = p.get("at").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect::>()); + let at = xy.filter(|v| v.len() == 2).map(|v| egui::pos2(v[0] as f32, v[1] as f32)); + let at = at.filter(|at| r.contains(*at)).unwrap_or_else(|| r.center()); + crate::canvas::zoom_at(app, at, if id == "view.zoomIn" { 1.5 } else { 1.0 / 1.5 }); } Ok(Value::Null) } diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index efe978dc5..3fa416d39 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -3152,3 +3152,70 @@ fn file_menu_ends_with_quit_but_the_macos_menu_bar_leaves_it_to_the_app_menu() { let file_items = native.iter().find(|(t, _)| *t == "File").map(|(_, e)| e); assert!(!matches!(file_items.and_then(|e| e.last()), Some(Item::Sep)), "no separator left at the end of File"); } + +#[test] +fn zoom_commands_zoom_around_the_pointer_when_it_is_over_the_canvas() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.session.execute("file.new", &json!({})).unwrap(); + let ctx = egui::Context::default(); + let frame = |app: &mut crate::DesignApp, events: Vec| { + let input = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))), + events, + ..Default::default() + }; + let mut out = ctx.run_ui(input, |ui| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }); + out.textures_delta.clear(); + }; + frame(&mut app, vec![]); + frame(&mut app, vec![]); + let rect = app.canvas_rect.unwrap(); + let pointer = rect.center() + egui::vec2(200.0, 120.0); + let under = |app: &crate::DesignApp, at: egui::Pos2| crate::canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_canvas(at); + let before = under(&app, pointer); + let zoom = app.view().unwrap().zoom; + + // The command with a pointer position keeps the point under it fixed. + app.run("view.zoomIn", json!({"at": [pointer.x, pointer.y]})).unwrap(); + assert!(app.view().unwrap().zoom > zoom); + let after = under(&app, pointer); + assert!((after.x - before.x).abs() < 1e-3 && (after.y - before.y).abs() < 1e-3); + app.run("view.zoomOut", json!({"at": [pointer.x, pointer.y]})).unwrap(); + let after = under(&app, pointer); + assert!((after.x - before.x).abs() < 1e-3 && (after.y - before.y).abs() < 1e-3); + + // A position outside the canvas (or none: the menu) zooms around the centre. + let centre = under(&app, rect.center()); + app.run("view.zoomIn", json!({"at": [-50.0, -50.0]})).unwrap(); + let kept = under(&app, rect.center()); + assert!((kept.x - centre.x).abs() < 1e-3 && (kept.y - centre.y).abs() < 1e-3); + app.run("view.zoomIn", json!({})).unwrap(); + let kept = under(&app, rect.center()); + assert!((kept.x - centre.x).abs() < 1e-3 && (kept.y - centre.y).abs() < 1e-3); + + // The keyboard shortcut uses the pointer. + app.run("view.zoom", json!({"zoom": 1.0})).unwrap(); + let before = under(&app, pointer); + frame(&mut app, vec![egui::Event::PointerMoved(pointer)]); + let key = egui::Event::Key { key: egui::Key::Equals, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND }; + frame(&mut app, vec![key]); + assert!(app.view().unwrap().zoom > 1.0); + let after = under(&app, pointer); + assert!((after.x - before.x).abs() < 1e-3 && (after.y - before.y).abs() < 1e-3); + + // The native menu bar's key equivalent (macOS) leaves the pointer in the window and uses it. + app.run("view.zoom", json!({"zoom": 1.0})).unwrap(); + let before = under(&app, pointer); + native_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pointer)], Some("view.zoomIn")); + assert!(app.view().unwrap().zoom > 1.0); + let after = under(&app, pointer); + assert!((after.x - before.x).abs() < 1e-3 && (after.y - before.y).abs() < 1e-3); + // A click in the menu bar takes the pointer out of the window: the centre stays. + let centre = under(&app, rect.center()); + native_frame(&mut app, &ctx, vec![egui::Event::PointerGone], Some("view.zoomOut")); + let kept = under(&app, rect.center()); + assert!((kept.x - centre.x).abs() < 1e-3 && (kept.y - centre.y).abs() < 1e-3); +} diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 4a75a994e..4453c009d 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -178,8 +178,8 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("app.paragraphStylesPanel", "Paragraph Styles", Some("Cmd+F11"), "{} — the Paragraph Styles panel"), ("app.insertCurrentPageNumber", "Current Page Number", Some("Cmd+Alt+Shift+N"), "{} — the Current Page Number marker at the insertion point"), ("app.storyEditor", "Edit in Story Editor", Some("Cmd+Y"), "{story?}"), - ("view.zoomIn", "Zoom In", Some("Cmd+="), "{}"), - ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "{}"), + ("view.zoomIn", "Zoom In", Some("Cmd+="), "{at?: [x, y] window pixels, kept in place when over the canvas}"), + ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "{at?: [x, y] window pixels, kept in place when over the canvas}"), ("view.fitPage", "Fit Page in Window", Some("Cmd+0"), "{}"), ("view.fitSelection", "Fit Selection in Window", Some("Cmd+Alt+="), "{}"), ("view.fitSpread", "Fit Spread in Window", Some("Cmd+Alt+0"), "{}"), From bddef171cdca50b3410d2bec607b1a0670d7ca47 Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 14:30:23 -0500 Subject: [PATCH 72/75] #2108: preserve full filenames at the browser import boundary Pass the original filename to file.openBytes so the engine removes one native extension. Cover Open and Drop, repeated suffixes, Unicode, bare and missing extensions, and IDML through the import inbox. Keep the full filename in import warnings. Co-Authored-By: OpenAI Codex --- ROADMAP.md | 2 +- crates/ui-egui/src/app/files.rs | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8443ae9e6..054d0f28a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -388,7 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. -- **2026-10-31** · A `.designcraft` file opened from bytes (`file.openBytes` with its file name, as a browser or host passes it) is titled without the extension, as an IDML file opened that way is. Saving it in the browser downloaded `Brochure.designcraft.designcraft`, and the File Name variable showed the doubled name. No readiness percentage change. +- **2026-10-31** · A `.designcraft` file opened from bytes (`file.openBytes` with its file name, as a browser or host passes it) is titled without the extension, as an IDML file opened that way is. Saving it in the browser downloaded `Brochure.designcraft.designcraft`, and the File Name variable showed the doubled name. Browser Open and Drop pass the full file name, so only the engine removes the extension and a repeated suffix is kept in the title. No readiness percentage change. - **2026-10-30** · Import warnings about a story have a Show button: it closes the warnings (the status line keeps them) and selects the story's first frame, fitted in the window. `file.open` returns `warningItems`, the frame each IDML or InDesign warning is about (or null), matching the converter's INDD story numbers (`story 30892`, written to IDML as `u78ac`) and IDML `Self` names to the imported stories. No readiness percentage change. - **2026-10-30** · Parent pages: a running-header variable shows its name in angle brackets on the parent itself (it was blank), as InDesign shows it; on the document pages, and in text with no page, it shows its text as before. No readiness percentage change. - **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index b5de53ab1..68155d884 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -161,8 +161,7 @@ impl DesignApp { } else if place { self.open_file("file.place", json!({"name": file.name, "base64": b64})) } else { - let title = file.name.rsplit_once('.').map_or(file.name.as_str(), |(stem, _)| stem); - self.open_file("file.openBytes", json!({"name": title, "base64": b64})) + self.open_file("file.openBytes", json!({"name": file.name, "base64": b64})) } } @@ -377,7 +376,7 @@ mod import_warning_tests { let request = app.import_request("open"); app.services.inbox.as_ref().unwrap().lock().unwrap().push(ImportedFile { request, name: "partial.idml".into(), bytes: idml(true) }); app.drain_inbox(); - assert_partial_import_warning(&mut app, "partial"); + assert_partial_import_warning(&mut app, "partial.idml"); } #[test] @@ -424,6 +423,34 @@ pub(crate) mod browser_import_tests { app.services.inbox.as_ref().unwrap().lock().unwrap().push(ImportedFile { request, name: name.into(), bytes }); app.drain_inbox(); } + + /// A document chosen or dropped in the browser is titled by its file name less one extension, + /// as on the desktop: the extension comes off once, in the engine. + #[test] + fn browser_opens_title_a_document_by_its_name_without_one_extension() { + let mut app = app(); + let native = designcraft_engine::cmd::base64_decode(app.run("file.serialize", json!({})).unwrap()["base64"].as_str().unwrap()); + for purpose in ["open", "drop"] { + for (name, title) in [ + ("Brochure.designcraft", "Brochure"), + ("Brochure.designcraft.designcraft", "Brochure.designcraft"), + ("Brochure.v2.designcraft", "Brochure.v2"), + ("日本.DESIGNCRAFT", "日本"), + (".designcraft", ".designcraft"), + ("Brochure", "Brochure"), + ] { + let request = app.import_request(purpose); + deliver(&mut app, request, name, native.clone()); + assert_eq!(app.session.doc().unwrap().doc.title, title, "{purpose}: {name}"); + } + } + let idml = designcraft_idml::export_idml(&app.session.doc().unwrap().doc); + for purpose in ["open", "drop"] { + let request = app.import_request(purpose); + deliver(&mut app, request, "Brochure.idml", idml.clone()); + assert_eq!(app.session.doc().unwrap().doc.title, "Brochure", "{purpose}: IDML"); + } + } fn image() -> Vec { br##""##.to_vec() } From 9e12d58887355b84c862b3ce253ce4c55f7218e1 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 1 Nov 2026 14:37:27 -0500 Subject: [PATCH 73/75] Math SVG: drop XML-forbidden characters before layout so they take no width Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/math.rs | 12 +++++++----- crates/engine/src/math/tests.rs | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/math.rs b/crates/engine/src/math.rs index 6cfd5e2e9..e0d520648 100644 --- a/crates/engine/src/math.rs +++ b/crates/engine/src/math.rs @@ -900,7 +900,13 @@ fn layout(f: &Fonts, n: &Node, size: f64) -> MBox { pub fn to_svg(latex: &str, size: f64) -> (Vec, (f64, f64)) { let db = FontDb::global(); let f = Fonts { upright: db.face(FAMILY, "Regular"), italic: db.face(FAMILY, "Italic"), fallback: db.face(FALLBACK, "Regular") }; - let b = layout(&f, &parse(latex), size); + // XML 1.0 forbids most control characters and U+FFFE/U+FFFF; drop them before layout so + // they take no width either. + let latex: String = latex + .chars() + .filter(|c| matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}')) + .collect(); + let b = layout(&f, &parse(&latex), size); let pad = size * 0.1; let (w, h) = (b.w + 2.0 * pad, b.asc + b.desc + 2.0 * pad); let base = pad + b.asc; @@ -916,10 +922,6 @@ pub fn to_svg(latex: &str, size: f64) -> (Vec, (f64, f64)) { let face = f.face(c, *italic); let fam = if std::ptr::eq(face, &*f.fallback) { FALLBACK } else { FAMILY }; let it = *italic && !std::ptr::eq(face, &*f.fallback); - // XML 1.0 forbids most control characters and U+FFFE/U+FFFF. - if !matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}') { - continue; - } let _ = write!( svg, "{}", diff --git a/crates/engine/src/math/tests.rs b/crates/engine/src/math/tests.rs index bd35c74bd..ddc006be6 100644 --- a/crates/engine/src/math/tests.rs +++ b/crates/engine/src/math/tests.rs @@ -480,4 +480,6 @@ fn svg_drops_control_characters_forbidden_by_xml() { let (svg, _) = to_svg("a\u{1}b\u{c}", 12.0); let svg = String::from_utf8(svg).unwrap(); assert!(!svg.chars().any(|c| c < ' ' && !matches!(c, '\t' | '\n' | '\r')), "control character in SVG: {svg:?}"); + // The dropped characters take no width either. + assert_eq!(to_svg("a\u{1}b\u{c}", 12.0).1, to_svg("ab", 12.0).1); } From aa609e4692211952bd0c89dd62c02603124c4a14 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 1 Nov 2026 14:37:42 -0500 Subject: [PATCH 74/75] macOS: list Hungarian in CFBundleLocalizations Co-Authored-By: Claude Opus 5.5 --- packaging/macos/Info.plist.in | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in index 03d7cb8e4..c7c6f7c12 100644 --- a/packaging/macos/Info.plist.in +++ b/packaging/macos/Info.plist.in @@ -16,6 +16,7 @@ de es fr + hu it ja ka From 34b7317cf4bcaf35ffd1602a6d61e77829029ee9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 1 Nov 2026 14:38:23 -0500 Subject: [PATCH 75/75] Zoom at pointer: reject an "at" with non-number coordinates Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/menus/run.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index f2d871c54..4df325e8a 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -576,8 +576,11 @@ fn run_ui_part3(app: &mut DesignApp, id: &str, p: &Value) -> Option { if let Some(r) = rect { // A keyboard zoom names the pointer ("at"); it counts only over the canvas. - let xy = p.get("at").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect::>()); - let at = xy.filter(|v| v.len() == 2).map(|v| egui::pos2(v[0] as f32, v[1] as f32)); + let at = p + .get("at") + .and_then(Value::as_array) + .filter(|a| a.len() == 2) + .and_then(|a| Some(egui::pos2(a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32))); let at = at.filter(|at| r.contains(*at)).unwrap_or_else(|| r.center()); crate::canvas::zoom_at(app, at, if id == "view.zoomIn" { 1.5 } else { 1.0 / 1.5 }); }