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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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/78] 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 1d9e577e3655f7d4c615feb953b948526a2168b7 Mon Sep 17 00:00:00 2001 From: Tom Main Date: Fri, 30 Oct 2026 19:21:41 -0400 Subject: [PATCH 51/78] font.list: report each face's fsType embedding bits font.list now gives, for the face that sets the text, its raw OS/2 fsType and what the bits mean: restrictedLicence, bitmapOnly, noSubsetting, editable and previewPrint (null when the face is missing). A host that makes print PDFs can then refuse a face whose licence doesn't allow the export, including system fonts and the bundled fonts, without reading the font files itself. PDF export subsets every font, so a caller needs noSubsetting as well as the restricted and bitmap-only bits. FontFace gains fs_type(); restricted_licence() and bitmap_only() read it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ApvpyStzzHbKyDBNxpzn2Y --- ROADMAP.md | 1 + crates/engine/src/cmd/fonts.rs | 65 ++++++++++++++++++++++++++++++++- crates/fonts/src/fontdb/face.rs | 11 ++++-- docs/target-app-parity.md | 2 +- 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 796ff7306..9d2955ca0 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** · `font.list` reports the embedding licence of the face that sets each font's text: its OS/2 `fsType` and what the bits mean (`restrictedLicence`, `bitmapOnly`, `noSubsetting`, `editable`, `previewPrint`; null when the font is missing). A script or host that makes PDFs can check every face, bundled and installed ones included, without reading font files. Export behaviour is unchanged. 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/fonts.rs b/crates/engine/src/cmd/fonts.rs index ee80eb914..52ddf4239 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -14,7 +14,7 @@ use crate::{Result, Session}; pub fn specs() -> Vec { vec![ - cmd!(query "font.list", "Fonts in Document", [], None, "{} → [{family, style, characters, missing, styleMissing, source: bundled|installed|document|added (null when missing), matchStatus: exact|styleSubstitute|missing, resolvedFamily, resolvedStyle, replacementFamily, replacementStyle}] (missing first)", has_doc, |s, _| { + cmd!(query "font.list", "Fonts in Document", [], None, "{} → [{family, style, characters, missing, styleMissing, source: bundled|installed|document|added (null when missing), matchStatus: exact|styleSubstitute|missing, resolvedFamily, resolvedStyle, replacementFamily, replacementStyle, fsType, restrictedLicence, bitmapOnly, noSubsetting, editable, previewPrint (the OS/2 fsType embedding bits of the face that sets the text and what they mean; null when missing)}] (missing first)", has_doc, |s, _| { Ok(Value::Array(list(&s.doc()?.doc))) }), cmd!( @@ -75,6 +75,18 @@ pub(super) struct UsedFont { pub resolved_style: String, pub replacement_family: String, pub replacement_style: String, + /// The OS/2 `fsType` of the face that sets the text (`resolved_*`); None when missing. + pub fs_type: Option, + /// Restricted License embedding (bit 1 without bits 2–3). + pub restricted_licence: Option, + /// Bitmap embedding only (bit 9). + pub bitmap_only: Option, + /// No subsetting (bit 8): the font may only be embedded whole. + pub no_subsetting: Option, + /// Editable embedding (bit 3). + pub editable: Option, + /// Preview & Print embedding (bit 2). + pub preview_print: Option, } fn list(d: &Document) -> Vec { @@ -124,6 +136,8 @@ pub(super) fn used_fonts(d: &Document) -> Vec { let resolved = db.face(&family, &style); let characters: Vec = characters.into_iter().filter(|c| !('\u{E000}'..='\u{E1FF}').contains(c)).collect(); let replacement = db.replacement_face(&family, &style, &characters); + let fs_type = (!missing).then(|| resolved.fs_type()); + let bit = |mask: u16| fs_type.map(|t| t & mask != 0); let source = (!missing).then(|| match db.face(&family, &style).source { FontSource::Bundled => "bundled", FontSource::Installed(_) => "installed", @@ -148,6 +162,12 @@ pub(super) fn used_fonts(d: &Document) -> Vec { resolved_style: resolved.style.clone(), replacement_family: replacement.family.clone(), replacement_style: replacement.style.clone(), + fs_type, + restricted_licence: (!missing).then(|| resolved.restricted_licence()), + bitmap_only: bit(0x0200), + no_subsetting: bit(0x0100), + editable: bit(0x0008), + preview_print: bit(0x0004), } }) .collect(); @@ -297,6 +317,49 @@ mod tests { assert_eq!(before, s.execute("font.list", &json!({})).unwrap()); } + /// `font.list` reports each face's OS/2 fsType and what its bits allow, so a caller can + /// refuse a face whose licence doesn't allow the export it is about to make. + #[test] + fn font_list_reports_the_embedding_licence_bits() { + use designcraft_fonts::testing::{font_with, with_fs_type}; + const FAMILY: &str = "DocFont List FsType"; + const FS_TYPES: [u16; 3] = [0x0002, 0x0108, 0x0204]; + for fs_type in FS_TYPES { + designcraft_fonts::FontDb::global() + .add_font(with_fs_type(font_with(&format!("{FAMILY} {fs_type:04x}"), &['W']).unwrap(), fs_type).unwrap()); + } + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + // "W W W Plain Gone": each W in one of the fonts above, Plain in the default (bundled) + // font, Gone in a family that isn't available. + let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "W W W Plain Gone"})).unwrap(); + let sid = s.doc().unwrap().doc.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story; + let set = |s: &mut Session, at: usize, end: usize, family: &str| { + s.execute("text.select", &json!({"story": sid.0, "anchor": at, "focus": end})).unwrap(); + s.execute("type.char", &json!({"fontFamily": family, "fontStyle": "Regular"})).unwrap(); + }; + for (i, fs_type) in FS_TYPES.into_iter().enumerate() { + set(&mut s, i * 2, i * 2 + 1, &format!("{FAMILY} {fs_type:04x}")); + } + set(&mut s, 12, 16, "Nonexistent Sans"); + let l = s.execute("font.list", &json!({})).unwrap(); + let rows = l.as_array().unwrap(); + let entry = |family: &str| rows.iter().find(|f| f["family"] == family).unwrap_or_else(|| panic!("{family}: {l}")); + let bits = |e: &Value| ["fsType", "restrictedLicence", "bitmapOnly", "noSubsetting", "editable", "previewPrint"].map(|k| e[k].clone()); + let (t, f) = (json!(true), json!(false)); + assert_eq!(entry(&format!("{FAMILY} 0002"))["source"], "added", "{l}"); + assert_eq!(bits(entry(&format!("{FAMILY} 0002"))), [json!(0x0002), t.clone(), f.clone(), f.clone(), f.clone(), f.clone()], "{l}"); + assert_eq!(bits(entry(&format!("{FAMILY} 0108"))), [json!(0x0108), f.clone(), f.clone(), t.clone(), t.clone(), f.clone()], "{l}"); + assert_eq!(bits(entry(&format!("{FAMILY} 0204"))), [json!(0x0204), f.clone(), t.clone(), f.clone(), f.clone(), t.clone()], "{l}"); + // The bundled fonts are OFL: installable embedding, every bit clear. + let bundled = rows.iter().find(|r| r["source"] == "bundled").unwrap_or_else(|| panic!("no bundled face: {l}")); + assert_eq!(bits(bundled), [json!(0), f.clone(), f.clone(), f.clone(), f.clone(), f], "{l}"); + // A missing face has no file to read. + let missing = entry("Nonexistent Sans"); + assert_eq!(missing["missing"], true, "{l}"); + assert_eq!(bits(missing), [(); 6].map(|()| Value::Null), "{l}"); + } + #[test] fn find_and_replace_font() { let mut s = Session::new(); diff --git a/crates/fonts/src/fontdb/face.rs b/crates/fonts/src/fontdb/face.rs index 058be0c0e..2283682dd 100644 --- a/crates/fonts/src/fontdb/face.rs +++ b/crates/fonts/src/fontdb/face.rs @@ -297,16 +297,21 @@ impl FontFace { /// (bit 1) without a less restrictive bit (Preview & Print, Editable). Fonts without an OS/2 /// table aren't restricted. pub fn restricted_licence(&self) -> bool { - use skrifa::raw::TableProvider; - let fs_type = self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type()); + let fs_type = self.fs_type(); fs_type & 0x0002 != 0 && fs_type & 0x000C == 0 } /// Does the font's licence allow embedding bitmaps only? OS/2 `fsType` bit 9: the outlines /// may not be passed on, so the font file can't be packaged whole. pub fn bitmap_only(&self) -> bool { + self.fs_type() & 0x0200 != 0 + } + + /// The font's OS/2 `fsType` (embedding licence bits), 0 (installable) when the font has no + /// OS/2 table. + pub fn fs_type(&self) -> u16 { use skrifa::raw::TableProvider; - self.skrifa().and_then(|f| f.os2().ok()).is_some_and(|t| t.fs_type() & 0x0200 != 0) + self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type()) } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9c3bff483..2ac146e1f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -293,7 +293,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Column guides: with the Selection tool, drag an interior column guide to move its gutter (both guides together, keeping its width, stopped by the neighbouring guides and the margins), snapping to the document grid, guides and objects, or with Shift to the ruler's tick marks; Escape cancels; one undo step. Refused while Lock Column Guides is on (the default). A document page takes the result as its own columns; on a parent page it reaches the pages that follow it. Objects stay where they are (there is no persistent Layout Adjustment setting yet). `layout.moveColumnGuide`. - Links panel: every placed graphic with status (OK / Modified / Missing / Embedded, checked against the file on disk), page and effective PPI (low resolution flagged); Relink…, Go To, Update, Embed; `links.*` commands. - Linked text files: `file.place {link: true}` or Preferences › File Handling › Create Links When Placing Text Files keeps a placed .docx/.rtf/.txt story linked to its file (path, size and hash, and the style map and conflict option used). The Links panel shows it OK / Modified / Missing; Update imports the file again into the same frames and thread with the saved style map, replacing edits made in the layout (the Links panel and menu ask first; the `links.update` command updates and returns a warning; undoable); Relink, Go To and Unlink. Saved in the native format; IDML exports the text unlinked. -- Missing fonts: highlighted pink on screen (never in output), listed by Preflight; Type › Find/Replace Font (fonts in the document, missing first; replace in text, cells, footnotes and styles); `font.list` / `font.replace`. Type › Reload Fonts (`font.reload`) scans the font folders again and reads again the installed font files edited or removed since they were read, then recomposes the open documents: a font installed or edited while DesignCraft runs needs no restart (#328, #547). InDesign notices such changes by itself. +- Missing fonts: highlighted pink on screen (never in output), listed by Preflight; Type › Find/Replace Font (fonts in the document, missing first; replace in text, cells, footnotes and styles); `font.list` (with each face's OS/2 `fsType` embedding bits) / `font.replace`. Type › Reload Fonts (`font.reload`) scans the font folders again and reads again the installed font files edited or removed since they were read, then recomposes the open documents: a font installed or edited while DesignCraft runs needs no restart (#328, #547). InDesign notices such changes by itself. - Missing glyphs, as in InDesign: a character the applied font lacks is drawn as that font's missing-glyph box (its .notdef; a drawn box when the font's is empty), on screen and in PDF (as a path, so PDF/A and PDF/UA exports take it), and Preflight lists the characters by font. Preferences › Composition › Draw Missing Glyphs from Fallback Fonts (`document.preferences {glyphFallback}`, stored with the document) draws them from other fonts instead; it is off in new documents and IDML imports, on in documents saved before it existed. A missing font's substitute always uses fallback fonts. - Damage-region repaint: after an edit the canvas re-renders only the frames whose composed text (or the items that) changed and patches its texture — pixel-identical to a full render; typing in a frame repaints in under a millisecond instead of a full-viewport render (22 ms measured on the sample). - Native engine saves, Save a Copy, snippets, books, libraries, swatches and engine/MCP exports use same-folder atomic replacement through craft-libs/craft-atomic, as do desktop Services writes and CLI raster exports. Failed writes before replacement retain the original, and already-open readers retain the old bytes. On Unix a new file gets the permissions ordinary file creation gives it (the umask applies) and a replaced file keeps its permissions. Parent-folder flushing is best effort, so this is not a power-loss durability guarantee. From f0c86e77e042f1c22b25952bb85614ee9428926f Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sat, 31 Oct 2026 11:26:05 -0400 Subject: [PATCH 52/78] 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 53/78] 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 54/78] 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 55/78] 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 81d5641252ae3d0caf6d03e69a8ed55238329abe Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 01:55:46 -0500 Subject: [PATCH 56/78] 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 57/78] 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 58/78] 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 59/78] 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 60/78] 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 61/78] 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 62/78] 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 63/78] 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 64/78] 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 100186def85017540e738a831b133d29b24720b3 Mon Sep 17 00:00:00 2001 From: Patrik Lermon Date: Sun, 1 Nov 2026 14:43:13 +0100 Subject: [PATCH 65/78] Swedish interface translation (sv) Adds Swedish (Svenska) as an interface language: 1850 strings, written from the English source. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 6 +- crates/ui-egui/src/i18n.rs | 66 +- crates/ui-egui/src/i18n/sv.rs | 2957 +++++++++++++++++++++++++++++++++ crates/ui-egui/src/menus.rs | 34 + docs/localization-parity.md | 8 +- 5 files changed, 3066 insertions(+), 5 deletions(-) create mode 100644 crates/ui-egui/src/i18n/sv.rs diff --git a/ROADMAP.md b/ROADMAP.md index 796ff7306..7dfdb8f32 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -354,7 +354,7 @@ A string without a translation shows in English and fails no check; `cargo xtask | Vietnamese | vi | none | 0% | Also shipped: Czech (99%), Italian (85%), Ukrainian (83%), Polish (99%). -Also shipped: Italian (85%), Ukrainian (83%), Georgian (100%). +Also shipped: Italian (85%), Ukrainian (83%), Georgian (100%), Swedish (99%). ## Upcoming @@ -537,6 +537,10 @@ Newest first. - **2026-10-16** · Layout: autoflow, inserted pages and new parents follow dragged column guides (#1096). Autoflow sets one frame per dragged column and counts the pages it adds from them; Insert Pages and New Parent keep the custom positions with the column count and gutter; `document.inspect` reports each page's gutter, column rects and whether its columns are custom. No readiness percentage change. - **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change. +- **2026-10-15** · Swedish interface (`sv`, Preferences ▸ General ▸ Interface Language ▸ Svenska): menus, panels, + dialogs and messages, 2,857 of the 2,891 interface strings (99%) in the missing-translations report; the other 34 + show in English. AI-drafted with Claude Code from the English source; spot-checked by a native speaker, not every + string reviewed. 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/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 3c2565052..5e27a6366 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -18,6 +18,7 @@ mod ka; mod pl; mod pt_br; mod ru; +mod sv; mod uk; /// Supported interface languages: (code, name in that language). @@ -36,6 +37,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[ ("cs", "Čeština"), ("pl", "Polski"), ("ka", "ქართული"), + ("sv", "Svenska"), ]; /// The interface languages in the order the Preferences picker lists them: English first (the @@ -90,6 +92,7 @@ fn lookup<'a>(lang: &str, s: &'a str) -> &'a str { static CZECH: OnceLock> = OnceLock::new(); static FRENCH: OnceLock> = OnceLock::new(); static GEORGIAN: OnceLock> = OnceLock::new(); + static SWEDISH: OnceLock> = OnceLock::new(); if lang == "ar" { return ARABIC.get_or_init(|| ar::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s); } @@ -123,6 +126,9 @@ fn lookup<'a>(lang: &str, s: &'a str) -> &'a str { if lang == "ka" { return GEORGIAN.get_or_init(|| ka::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s); } + if lang == "sv" { + return SWEDISH.get_or_init(|| sv::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s); + } if lang == "uk" { return UKRAINIAN.get_or_init(|| uk::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s); } @@ -151,6 +157,7 @@ pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str { "pl" => pl::CONTEXT, "ja" => ja::CONTEXT, "ar" => ar::CONTEXT, + "sv" => sv::CONTEXT, _ => &[], }; let written = |text: &&str| !text.trim().is_empty(); @@ -176,6 +183,12 @@ pub(crate) fn russian_catalog() -> Vec<&'static str> { ru::TABLE.iter().map(|(_, text)| *text).chain(ru::CONTEXT.iter().map(|(_, _, text)| *text)).collect() } +/// Whether the Swedish catalog has an entry for `s` (many Swedish labels read like the English ones). +#[cfg(test)] +pub(crate) fn swedish_has(s: &str) -> bool { + sv::TABLE.iter().any(|(key, _)| *key == s) +} + /// Ukrainian, Russian, Polish and Georgian put the category before the value in count captions /// ("сторінок: 5", "страниц: 5", "Strony: 5", "გვერდი: 5"): every number stays grammatical in the /// languages with several plural forms, and Georgian nouns stay singular after numerals. Other @@ -184,12 +197,29 @@ pub fn category_first(lang: &str) -> bool { matches!(lang, "uk" | "ru" | "pl" | "ka") } +/// Swedish singular for a count of one, where it differs from the catalog's plural ("1 sida", +/// "5 sidor"); "fel", "objekt" and "lager" read the same either way. +fn swedish_singular(noun: &str) -> Option<&'static str> { + match noun { + "pages" => Some("sida"), + "stories" => Some("artikel"), + "warnings" => Some("varning"), + _ => None, + } +} + /// Caption for a counted category, e.g. `count_label("ru", "pages", 5)` is "страниц: 5". Czech -/// picks the plural form for the count (1, 2–4, other). +/// picks the plural form for the count (1, 2–4, other); Swedish the singular for one. pub fn count_label(lang: &str, noun: &str, count: usize) -> String { if category_first(lang) { return format!("{}: {count}", tr(lang, noun)); } + if lang == "sv" + && count == 1 + && let Some(one) = swedish_singular(noun) + { + return format!("{count} {one}"); + } if lang == "cs" && let Some((_, [one, few, other])) = cs::PLURALS.iter().find(|(key, _)| *key == noun) { @@ -231,6 +261,40 @@ pub fn workspace_name<'a>(lang: &str, name: &'a str) -> &'a str { #[cfg(test)] mod tests; +#[cfg(test)] +mod swedish_tests { + use super::*; + + #[test] + fn swedish_catalog_preserves_placeholders_and_ellipses() { + let catalog: HashMap<_, _> = sv::TABLE.iter().copied().collect(); + assert_eq!(catalog.len(), sv::TABLE.len(), "duplicate Swedish keys"); + let placeholders = |s: &str| { + s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name.to_owned())).collect::>() + }; + for (key, translation) in sv::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_eq!(tr("sv", key), *translation, "sv: {key}"); + } + let contexts: std::collections::HashSet<_> = sv::CONTEXT.iter().map(|(key, scope, _)| (*key, *scope)).collect(); + assert_eq!(contexts.len(), sv::CONTEXT.len(), "duplicate Swedish context keys"); + for (key, context, expected) in sv::CONTEXT { + assert_eq!(placeholders(key), placeholders(expected), "placeholders: {key} ({context})"); + assert_eq!(tr_context("sv", key, context), *expected, "sv {context}: {key}"); + } + assert_eq!(tr("sv", "File"), "Arkiv"); + assert_eq!(tr("sv", "Save As…"), "Spara som…"); + assert_eq!(tr("sv", "Unknown label"), "Unknown label"); + assert!(!is_rtl("sv")); + assert_eq!(count_label("sv", "pages", 5), "5 sidor"); + assert_eq!(count_label("sv", "pages", 1), "1 sida"); + assert_eq!(count_label("sv", "warnings", 1), "1 varning"); + assert_eq!(count_label("sv", "errors", 1), "1 fel"); + } +} + /// `name` lowercased with the Latin accents that occur in language names folded away, so a /// byte-wise sort reads alphabetically. Other scripts are left as they are. fn name_sort_key(name: &str) -> String { diff --git a/crates/ui-egui/src/i18n/sv.rs b/crates/ui-egui/src/i18n/sv.rs new file mode 100644 index 000000000..1a7550a35 --- /dev/null +++ b/crates/ui-egui/src/i18n/sv.rs @@ -0,0 +1,2957 @@ +//! Swedish (sv) interface translations for DesignCraft, written from the English source with ordinary +//! Swedish software vocabulary; no other catalog and no proprietary localisation was used. +//! AI-drafted with Claude Code; spot-checked by a native Swedish speaker, not every string reviewed. +pub(super) const TABLE: &[(&str, &str)] = &[ + ("File", "Arkiv"), + ("Edit", "Redigera"), + ("Layout", "Layout"), + ("Type", "Text"), + ("Object", "Objekt"), + ("Table", "Tabell"), + ("View", "Visa"), + ("Window", "Fönster"), + ("Help", "Hjälp"), + ("New", "Nytt"), + ("New Document…", "Nytt dokument…"), + ("New Document", "Nytt dokument"), + ("Open…", "Öppna…"), + ("Close", "Stäng"), + ("Save", "Spara"), + ("Save As…", "Spara som…"), + ("Save a Copy…", "Spara en kopia…"), + ("Revert", "Återgå"), + ("Place…", "Placera…"), + ("Export PDF…", "Exportera PDF…"), + ("Print…", "Skriv ut…"), + ("Print", "Skriv ut"), + ("Document Setup…", "Dokumentinställningar…"), + ("Document Setup", "Dokumentinställningar"), + ("Package…", "Paketera…"), + ("Undo", "Ångra"), + ("Redo", "Gör om"), + ("Cut", "Klipp ut"), + ("Copy", "Kopiera"), + ("Paste", "Klistra in"), + ("Bulleted & Numbered Lists", "Punktlistor och numrerade listor"), + ("Current Page Number", "Aktuellt sidnummer"), + ("Apply Color (,)", "Använd färg (,)"), + ("Apply Gradient (.)", "Använd gradient (.)"), + ("View Options", "Visningsalternativ"), + ("Frame Edges", "Ramkanter"), + ("Hidden Characters", "Dolda tecken"), + ("Create Flex Layout", "Skapa flexlayout"), + ("Add to Flex Layout", "Lägg till i flexlayout"), + ("Convert to Flex Layout", "Konvertera till flexlayout"), + ("Remove Flex Layout", "Ta bort flexlayout"), + ("Show Flex Controls", "Visa flexkontroller"), + ("Flex Layout Options", "Alternativ för flex-layout"), + ("Reorder in Flex Layout", "Ändra ordning i flexlayout"), + ("Flex Start", "Flex-start"), + ("Flex End", "Flex-slut"), + ("Space Between", "Avstånd mellan"), + ("Space Around", "Avstånd runt"), + ("Space Evenly", "Jämnt avstånd"), + ("Reverse Row", "Omvänd rad"), + ("Reverse Column", "Omvänd kolumn"), + ("Wrap Content", "Ombryt innehåll"), + ("Spacing", "Avstånd"), + ("Horizontal Spacing", "Horisontellt avstånd"), + ("Vertical Spacing", "Vertikalt avstånd"), + ("Constrain proportions", "Behåll proportioner"), + ("Auto", "Auto"), + ("W:", "B:"), + ("H:", "H:"), + ("Number:", "Antal:"), + ("Gutter:", "Spaltmellanrum:"), + ("Fit Guides to:", "Anpassa stödlinjer till:"), + ("Top:", "Överkant:"), + ("Bottom:", "Nederkant:"), + ("Inside:", "Inre:"), + ("Outside:", "Yttre:"), + ("Left:", "Vänster:"), + ("Right:", "Höger:"), + ("Paths:", "Banor:"), + ("Pathfinder:", "Banfinnare:"), + ("Convert Shape:", "Konvertera form:"), + ("Convert Point:", "Konvertera punkt:"), + ("Join Path", "Foga samman bana"), + ("Open Path", "Öppen bana"), + ("Plain", "Normal"), + ("Symmetrical", "Symmetrisk"), + ("The stroke isn't a gradient.", "Konturen är inte en gradient."), + ("Reverse Path", "Vänd banans riktning"), + ("Set Anchor", "Ange ankare"), + ("Clear", "Rensa"), + ("Delete", "Radera"), + ("Duplicate", "Duplicera"), + ("Select All", "Markera allt"), + ("Deselect All", "Avmarkera alla"), + ("Find/Change…", "Sök/ändra…"), + ("Find/Change", "Sök/ändra"), + ("Check Spelling…", "Kontrollera stavning…"), + ("Preferences…", "Inställningar…"), + ("Preferences", "Inställningar"), + ("Keyboard Shortcuts…", "Kortkommandon…"), + ("Keyboard Shortcuts", "Kortkommandon"), + ("Interface Language", "Gränssnittsspråk"), + ("Pages", "Sidor"), + ("Panels", "Paneler"), + ("Page", "Sida"), + ("Margins and Columns…", "Marginaler och spalter…"), + ("Create Guides…", "Skapa stödlinjer…"), + ("Numbering & Section Options…", "Numrerings- och avsnittsalternativ…"), + ("Go to Page…", "Gå till sida…"), + ("Go to Page", "Gå till sida"), + ("Next Page", "Nästa sida"), + ("Previous Page", "Föregående sida"), + ("First Page", "Första sidan"), + ("Last Page", "Sista sidan"), + ("Font", "Typsnitt"), + ("Size", "Storlek"), + ("Character", "Tecken"), + ("Paragraph", "Stycke"), + ("Paragraph Styles", "Styckestilar"), + ("Character Styles", "Teckenstilar"), + ("Paragraph Style Options", "Alternativ för styckestil"), + ("Object Styles", "Objektstilar"), + ("Glyphs", "Glyfer"), + ("Find/Replace Font…", "Sök/ersätt typsnitt…"), + ("Find/Replace Font", "Sök/ersätt typsnitt"), + ("Story Editor", "Artikelredigerare"), + ("Edit in Story Editor", "Redigera i textredigeraren"), + ("Insert Special Character", "Infoga specialtecken"), + ("Insert White Space", "Infoga blanktecken"), + ("Insert Break Character", "Infoga brytningstecken"), + ("Change Case", "Ändra skiftläge"), + ("Text Frame Options…", "Alternativ för textram…"), + ("Text Frame Options", "Alternativ för textram"), + ("Footnote Options", "Fotnotsalternativ"), + ("New Cross-Reference", "Ny korsreferens"), + ("Arrange", "Ordna"), + ("Transform", "Transformera"), + ("Group", "Gruppera"), + ("Ungroup", "Dela upp grupp"), + ("Lock", "Lås"), + ("Unlock All on Spread", "Lås upp alla på uppslaget"), + ("Bring to Front", "Flytta längst fram"), + ("Send to Back", "Flytta längst bak"), + ("Bring Forward", "Flytta framåt"), + ("Send Backward", "Flytta bakåt"), + ("Fitting", "Anpassning"), + ("Fit Frame to Content", "Anpassa ram till innehåll"), + ("Fit Content to Frame", "Anpassa innehåll till ram"), + ("Fill Frame Proportionally", "Fyll ram proportionerligt"), + ("Fit Content Proportionally", "Anpassa innehåll proportionerligt"), + ("Center Content", "Centrera innehåll"), + ("Frame Fitting Options", "Alternativ för ramanpassning"), + ("Generate QR Code", "Generera QR-kod"), + ("Create Table", "Skapa tabell"), + ("Insert Table…", "Infoga tabell…"), + ("Insert", "Infoga"), + ("Rows", "Rader"), + ("Columns", "Kolumner"), + ("Row", "Rad"), + ("Column", "Kolumn"), + ("Merge Cells", "Sammanfoga celler"), + ("Split Cell Horizontally", "Dela cell horisontellt"), + ("Split Cell Vertically", "Dela cell vertikalt"), + ("Table Options", "Tabellalternativ"), + ("Cell Options", "Cellalternativ"), + ("Header Rows", "Rubrikrader"), + ("Footer Rows", "Fotrader"), + ("Zoom In", "Zooma in"), + ("Zoom Out", "Zooma ut"), + ("Fit Page in Window", "Anpassa sidan till fönstret"), + ("Fit Spread in Window", "Anpassa uppslaget till fönstret"), + ("Actual Size", "Verklig storlek"), + ("Screen Mode", "Skärmläge"), + ("Normal", "Normal"), + ("Preview", "Förhandsgranskning"), + ("Rulers", "Linjaler"), + ("Grids & Guides", "Rutnät och stödlinjer"), + ("Show Guides", "Visa stödlinjer"), + ("Hide Guides", "Dölj stödlinjer"), + ("Show Rulers", "Visa linjaler"), + ("Hide Rulers", "Dölj linjaler"), + ("Extras", "Extrafunktioner"), + ("Workspace", "Arbetslayout"), + ("Editorial", "Redaktionell"), + ("Object & Layout", "Objekt och layout"), + ("Styles", "Stilar"), + ("Essentials Classic", "Grundläggande klassisk"), + ("New Workspace", "Ny arbetslayout"), + ("Properties", "Egenskaper"), + ("Layers", "Lager"), + ("Swatches", "Färgrutor"), + ("Colour", "Färg"), + ("Color", "Färg"), + ("Stroke", "Kontur"), + ("Fill", "Fyllning"), + ("Gradient", "Gradient"), + ("Transparency", "Transparens"), + ("Effects", "Effekter"), + ("Links", "Länkar"), + ("Notes", "Anteckningar"), + ("Conditions", "Villkor"), + ("Library", "Bibliotek"), + ("Text Wrap", "Textombrytning"), + ("Align", "Justera"), + ("Pathfinder", "Banfinnare"), + ("Transform Again", "Transformera igen"), + ("Interactive", "Interaktiv"), + ("Buttons and Forms", "Knappar och formulär"), + ("Hyperlinks", "Hyperlänkar"), + ("Bookmarks", "Bokmärken"), + ("Show All Menu Items", "Visa alla menyalternativ"), + ("About DesignCraft", "Om DesignCraft"), + ("Quit DesignCraft", "Avsluta DesignCraft"), + ("Welcome to DesignCraft", "Välkommen till DesignCraft"), + ("Page layout for print and screen — fast, open and scriptable.", "Sidlayout för tryck och skärm – snabb, öppen och skriptbar."), + (" New file ", " Ny fil "), + (" Open ", " Öppna "), + (" Open sample magazine ", " Öppna exempeltidning "), + ("Start a new document", "Skapa ett nytt dokument"), + ("No document open", "Inget dokument är öppet"), + ("Join the ArtCraft community", "Gå med i ArtCraft-gemenskapen"), + ("Get help, share your layouts and shape what we build next.", "Få hjälp, dela dina layouter och påverka vad vi bygger härnäst."), + ("Join our Discord", "Gå med i vår Discord"), + ("Community", "Gemenskap"), + ("Join the community", "Gå med i gemenskapen"), + ("discord.gg/artcraft — help, feedback and show-and-tell", "discord.gg/artcraft – hjälp, återkoppling och visa upp vad du gjort"), + (" OK ", " OK "), + ("Float panel", "Gör panelen flytande"), + ("Dock panel", "Docka panel"), + ("OK", "OK"), + ("Cancel", "Avbryt"), + ("Can't Open the File", "Det går inte att öppna filen"), + ("Can't Place the File", "Det går inte att placera filen"), + ("Apply", "Använd"), + ("Done", "Klart"), + ("Reset", "Återställ"), + ("Add", "Lägg till"), + ("Remove", "Ta bort"), + ("Name", "Namn"), + ("Width", "Bredd"), + ("Height", "Höjd"), + ("Portrait", "Stående"), + ("Landscape", "Liggande"), + ("Orientation", "Orientering"), + ("Units", "Enheter"), + ("Preset", "Förinställning"), + ("Facing Pages", "Motstående sidor"), + ("Primary Text Frame", "Primär textram"), + ("Number of Pages", "Antal sidor"), + ("Start Page #", "Startsida nr"), + ("Page Size", "Sidstorlek"), + ("Margins", "Marginaler"), + ("Bleed", "Utfall"), + ("Slug", "Informationsområde"), + ("Top", "Överkant"), + ("Bottom", "Nederkant"), + ("Left", "Vänster"), + ("Right", "Höger"), + ("Inside", "Inuti"), + ("Outside", "Utanför"), + ("Gutter", "Spaltmellanrum"), + ("Binding", "Bindning"), + ("Left to Right", "Vänster till höger"), + ("Right to Left", "Höger till vänster"), + ("No Selection", "Ingen markering"), + ("Rectangle", "Rektangel"), + ("Ellipse", "Ellips"), + ("Polygon", "Polygon"), + ("Polygon Settings", "Polygoninställningar"), + ("Text", "Text"), + ("Image", "Bild"), + ("Line", "Linje"), + ("Position", "Position"), + ("Appearance", "Utseende"), + ("Opacity", "Opacitet"), + ("Corner", "Hörn"), + ("Rotation", "Rotation"), + ("Rulers & Grids", "Linjaler och rutnät"), + ("Guides", "Stödlinjer"), + ("Flex Layout", "Flex-layout"), + ("Auto Size", "Automatisk storlek"), + ("Tracking", "Spärrning"), + ("Leading", "Radavstånd"), + ("Kerning", "Kerning"), + ("Language", "Språk"), + ("Hyphenation", "Avstavning"), + ("Alignment", "Justering"), + ("Indent", "Indrag"), + ("Space Before", "Avstånd före"), + ("Space After", "Avstånd efter"), + ("New Layer", "Nytt lager"), + ("New Swatch", "Ny färgruta"), + ("Object Layer Options", "Lageralternativ för objekt"), + ("Colour Picker", "Färgväljare"), + ("Color Picker", "Färgväljare"), + ("Color Settings", "Färginställningar"), + ("User Dictionary", "Användarordlista"), + ("Menu Customization", "Anpassning av menyer"), + ("Import Options", "Importalternativ"), + ("Place PDF", "Placera PDF"), + ("Find", "Sök"), + ("Change", "Ändra"), + ("Find What", "Sök efter"), + ("Change To", "Ändra till"), + ("Find Next", "Sök nästa"), + ("Change All", "Ändra alla"), + ("Search", "Sök"), + ("Document", "Dokument"), + ("Selection", "Markering"), + ("All Documents", "Alla dokument"), + ("Direction", "Riktning"), + ("Horizontal", "Horisontell"), + ("Vertical", "Vertikal"), + ("On", "På"), + ("Off", "Av"), + ("None", "Ingen"), + ("[None]", "[Ingen]"), + ("Place with Import Options…", "Placera med importalternativ…"), + ("Print Booklet…", "Skriv ut häfte…"), + ("Export", "Exportera"), + ("Export Page as PNG…", "Exportera sidan som PNG…"), + ("Export IDML…", "Exportera IDML…"), + ("Export EPUB…", "Exportera EPUB…"), + ("Export EPUB (Fixed Layout)…", "Exportera EPUB (fast layout)…"), + ("Export PDF (Interactive)…", "Exportera PDF (interaktiv)…"), + ("Export HTML…", "Exportera HTML…"), + ("Export XML…", "Exportera XML…"), + ("Export Text…", "Exportera text…"), + ("Quick Actions", "Snabbåtgärder"), + ("Import File", "Importera fil"), + ("Insert Page", "Infoga sida"), + ("Delete selected pages", "Radera markerade sidor"), + ("Create new page", "Skapa ny sida"), + ("Edit page size", "Redigera sidstorlek"), + ("Custom Page Size…", "Anpassad sidstorlek…"), + ("Edit Page", "Redigera sida"), + ("Adjust Layout", "Anpassa layout"), + ("Smart Guides", "Smarta stödlinjer"), + ("Lock Guides", "Lås stödlinjer"), + ("Show Document Grid", "Visa dokumentrutnät"), + ("Show Baseline Grid", "Visa baslinjerutnät"), + ("Options", "Alternativ"), + ("More Options", "Fler alternativ"), + ("Dialog", "Dialogruta"), + ("General", "Allmänt"), + ("Basic Character Formats", "Grundläggande teckenformat"), + ("Indents and Spacing", "Indrag och avstånd"), + ("Justification", "Justering"), + ("Drop Caps and Nested Styles", "Anfanger och kapslade stilar"), + ("GREP Style", "GREP-format"), + ("Character Color", "Teckenfärg"), + ("Export Tagging", "Exporttaggning"), + ("Numbering and Formatting", "Numrering och formatering"), + ("Spacing Options", "Avståndsalternativ"), + ("First Baseline", "Första baslinje"), + ("Rule Above", "Linje ovanför"), + ("Numbering", "Numrering"), + ("Footnote Reference Number in Text", "Fotnotsreferensnummer i texten"), + ("Footnote Formatting", "Fotnotsformatering"), + ("Span Footnotes Across Columns", "Låt fotnoter sträcka sig över spalter"), + ("Rule On", "Linje på"), + ("Ascent", "Uppstapelhöjd"), + ("Cap Height", "Versalhöjd"), + ("x Height", "x-höjd"), + ("Em Box Height", "Fyrkantshöjd"), + ("Fixed", "Fast"), + ("Never (continuous)", "Aldrig (kontinuerlig)"), + ("Spread", "Uppslag"), + ("Section", "Avsnitt"), + ("Footnote Reference", "Fotnotsreferens"), + ("Footnote Text", "Fotnotstext"), + ("Both", "Båda"), + ("Apply Superscript", "Använd upphöjd"), + ("Apply Subscript", "Använd nedsänkt"), + ("Apply Normal", "Använd normal"), + ("OpenType Superscript", "OpenType upphöjd"), + ("through", "till"), + ("up to", "upp till"), + ("line", "rad"), + ("lines", "rader"), + ("sentences", "meningar"), + ("words", "ord"), + ("characters", "tecken"), + ("letters", "bokstäver"), + ("digits", "siffror"), + ("tab", "tabb"), + ("forcedLineBreak", "forcedLineBreak"), + ("emSpace", "helfyrkant"), + ("enSpace", "halvfyrkant"), + ("dropcap", "anfang"), + ("Drop Caps", "Anfanger"), + ("Lines:", "Rader:"), + ("Characters:", "Tecken:"), + ("Drop Lines", "Anfangsrader"), + ("Drop Chars", "Anfangtecken"), + ("Align Left Edge", "Justera vänsterkant"), + ("Scale for Descenders", "Skala för nedstaplar"), + ("chars", "tkn"), + ("[Automatic]", "[Automatisk]"), + ("Size:", "Storlek:"), + ("Tracking:", "Spärrning:"), + ("Left Indent:", "Vänsterindrag:"), + ("Right Indent:", "Högerindrag:"), + ("First Line Indent:", "Indrag första raden:"), + ("Space Before:", "Avstånd före:"), + ("Space After:", "Avstånd efter:"), + ("Words with at Least:", "Ord med minst:"), + ("After First:", "Efter första:"), + ("Before Last:", "Före sista:"), + ("Hyphen Limit:", "Gräns för avstavningar:"), + ("Word Spacing:", "Ordavstånd:"), + ("Letter Spacing:", "Teckenavstånd:"), + ("Glyph Scaling:", "Glyfskalning:"), + ("Insert Kashidas:", "Infoga kashidor:"), + ("In justified Arabic text", "I marginaljusterad arabisk text"), + ("Korean Line Breaks:", "Koreanska radbrytningar:"), + ("Between syllables (not only at spaces)", "Mellan stavelser (inte bara vid blanksteg)"), + ("Minimum Space Before First Footnote:", "Minsta avstånd före första fotnoten:"), + ("Space Between Footnotes:", "Avstånd mellan fotnoter:"), + ("Offset:", "Förskjutning:"), + ("Min:", "Min:"), + ("Weight:", "Tjocklek:"), + ("Color:", "Färg:"), + ("Width:", "Bredd:"), + ("Style:", "Stil:"), + ("Start at:", "Börja på:"), + ("Restart Numbering Every:", "Börja om numreringen varje:"), + ("Show Prefix/Suffix in:", "Visa prefix/suffix i:"), + ("Prefix:", "Prefix:"), + ("Suffix:", "Suffix:"), + ("Position:", "Position:"), + ("Character Style:", "Teckenstil:"), + ("Paragraph Style:", "Styckestil:"), + ("Separator:", "Avgränsare:"), + ("EPUB and HTML", "EPUB och HTML"), + ("Tag:", "Tagg:"), + ("Class:", "Klass:"), + ("Nested Styles", "Kapslade stilar"), + ("Nested Line Styles", "Kapslade radstilar"), + ("New Nested Style", "Ny kapslad stil"), + ("New Line Style", "Ny radstil"), + ("GREP Styles", "GREP-format"), + ("Apply Style:", "Använd stil:"), + ("To Text:", "Till text:"), + ("New GREP Style", "Ny GREP-stil"), + ("Font Family:", "Typsnittsfamilj:"), + ("Font Style:", "Teckenstil:"), + ("Leading:", "Radavstånd:"), + ("Alignment:", "Justering:"), + ("Hyphenate", "Avstava"), + ("Minimum", "Minimum"), + ("Desired", "Önskad"), + ("Maximum", "Maximum"), + ("Composer:", "Sättningsmotor:"), + ("Paragraph Composer", "Styckesättare"), + ("Single-line Composer", "Enradssättare"), + ("Style Name:", "Stilnamn:"), + ("Based On:", "Baserat på:"), + ("for", "för"), + ("Hyphenation Exceptions", "Avstavningsundantag"), + ("Binding:", "Bindning:"), + ("Dynamic Spelling", "Dynamisk stavning"), + ("UI Scaling", "Gränssnittsskalning"), + ("Ruby:", "Ruby:"), + ("Set over the selected text; empty removes it.", "Sätts över den markerade texten; tomt tar bort den."), + ("Document Footnote Options…", "Alternativ för dokumentets fotnoter…"), + ("Insert Footnote", "Infoga fotnot"), + ("Go to Footnote Reference", "Gå till fotnotsreferens"), + ("Footnotes", "Fotnoter"), + ("Paragraph Style Options…", "Alternativ för styckestil…"), + ("New Paragraph Style…", "Ny styckestil…"), + ("New Character Style…", "Ny teckenstil…"), + ("[Basic Paragraph]", "[Grundläggande stycke]"), + ("[No Paragraph Style]", "[Ingen styckestil]"), + ("Align left", "Justera vänster"), + ("Align center", "Centrera"), + ("Align right", "Justera höger"), + ("Justify with last line aligned left", "Marginaljustera med sista raden vänsterjusterad"), + ("Justify with last line aligned center", "Marginaljustera med sista raden centrerad"), + ("Justify with last line aligned right", "Marginaljustera med sista raden högerjusterad"), + ("Justify all lines", "Marginaljustera alla rader"), + ("Align towards spine", "Justera mot bokryggen"), + ("Align away from spine", "Justera bort från bokryggen"), + ("Create New Style from Selection", "Skapa ny stil från markering"), + ("No text selected", "Ingen text är markerad"), + ("Essentials", "Grundläggande"), + ("Advanced", "Avancerat"), + ("Book", "Bok"), + ("Digital Publishing", "Digital publicering"), + ("Interactive for PDF", "Interaktiv för PDF"), + ("Printing and Proofing", "Utskrift och korrektur"), + ("Typography", "Typografi"), + ("No errors", "Inga fel"), + ("error", "fel"), + ("errors", "fel"), + ("Display All Blacks as Rich Black", "Visa alla svarta som djupsvart"), + ("Display All Blacks Accurately", "Visa alla svarta korrekt"), + ("Output All Blacks as Rich Black", "Återge all svart som djupsvart"), + ("Output All Blacks Accurately", "Återge all svart korrekt"), + ("Top Margin", "Övre marginal"), + ("Top of Page", "Överst på sidan"), + ("Paragraph Styles panel", "Panelen Styckestilar"), + ("Character Styles panel", "Panel för teckenstilar"), + ("Create new style", "Skapa ny stil"), + ("Clear overrides", "Rensa lokala ändringar"), + ("Accept", "Acceptera"), + ("Reject", "Avvisa"), + ("Add Selection", "Lägg till markering"), + ("Add the selection", "Lägg till markeringen"), + ("Add to Swatches", "Lägg till i färgrutor"), + ("Align Stroke:", "Justera kontur:"), + ("All Caps", "Versaler"), + ("Angle", "Vinkel"), + ("Angle:", "Vinkel:"), + ("Baseline", "Baslinje"), + ("Cap:", "Ände:"), + ("Cell Text", "Celltext"), + ("Cell Type", "Celltyp"), + ("Column Width", "Kolumnbredd"), + ("Column Rules", "Spaltlinjer"), + ("Insert Column Rule", "Infoga spaltlinje"), + ("Tint %", "Färgton %"), + ("Top Inset", "Övre infällning"), + ("Bottom Inset", "Infällning nederkant"), + ("Horizontal Offset", "Horisontell förskjutning"), + ("Composer", "Sättningsmotor"), + ("Convert Selection to Multi-State Object", "Konvertera markering till objekt med flera lägen"), + ("Convert to Text", "Konvertera till text"), + ("Create New Layer", "Skapa nytt lager"), + ("Default", "Standard"), + ("Delete DTD", "Radera DTD"), + ("Delete Layer", "Radera lager"), + ("Delete Page", "Radera sida"), + ("Digits", "Siffror"), + ("Distance", "Avstånd"), + ("Distribute ↔", "Fördela ↔"), + ("Distribute ↕", "Fördela ↕"), + ("Drop Shadow", "Slagskugga"), + ("Effects: Drop Shadow", "Effekter: Slagskugga"), + ("Favorites", "Favoriter"), + ("First Spread", "Första uppslaget"), + ("Frame Fitting", "Ramanpassning"), + ("Gap Color:", "Mellanrumsfärg:"), + ("Go To", "Gå till"), + ("Graphic…", "Grafik…"), + ("Home", "Start"), + ("Hyperlink from the selected text or frames", "Hyperlänk från markerad text eller markerade ramar"), + ("Include when exporting", "Inkludera vid export"), + ("Join:", "Hörn:"), + ("Last Spread", "Sista uppslaget"), + ("Linked Stories", "Länkade artiklar"), + ("Load DTD…", "Läs in DTD…"), + ("Location:", "Plats:"), + ("Miter Limit:", "Spetsgräns:"), + ("Move", "Flytta"), + ("New Article from Selection", "Ny artikel från markering"), + ("New Note at Cursor", "Ny anteckning vid textmarkören"), + ("New condition name", "Namn på nytt villkor"), + ("New group…", "Ny grupp…"), + ("New tag", "Ny tagg"), + ("Next Spread", "Nästa uppslag"), + ("Note text", "Anteckningstext"), + ("Offset", "Förskjutning"), + ("Offsets", "Förskjutningar"), + ("Open", "Öppna"), + ("Paragraph alignment", "Styckejustering"), + ("Pick a file in the folder holding the moved graphics", "Välj en fil i mappen som innehåller den flyttade grafiken"), + ("Place", "Placera"), + ("Previous Spread", "Föregående uppslag"), + ("Release to Objects", "Släpp till objekt"), + ("Reverse the gradient", "Vänd gradienten"), + ("Rotate 90° Counterclockwise", "Rotera 90° moturs"), + ("Rotation Angle", "Rotationsvinkel"), + ("Row Height", "Radhöjd"), + ("Run", "Kör"), + ("Scale X Percentage", "Skala X i procent"), + ("Scale Y Percentage", "Skala Y i procent"), + ("Search commands", "Sök kommandon"), + ("Search fonts", "Sök typsnitt"), + ("Search styles and commands…", "Sök format och kommandon…"), + ("Search: character or U+code", "Sök: tecken eller U+kod"), + ("Separated by commas", "Avgränsade med kommatecken"), + ("Share", "Dela"), + ("Shear X Angle", "Skjuvningsvinkel X"), + ("Show / hide text with this condition", "Visa/dölj text med det här villkoret"), + ("Show Favorites Only", "Visa endast favoriter"), + ("Show in the text", "Visa i texten"), + ("Spreads of up to 10 pages", "Uppslag på upp till 10 sidor"), + ("Stop Color:", "Stoppfärg:"), + ("Strikethrough", "Genomstruken"), + ("Structure", "Struktur"), + ("Style", "Stil"), + ("Style source", "Stilkälla"), + ("Styles panel", "Panelen Stilar"), + ("Superscript", "Upphöjd"), + ("Tag the selection", "Tagga markeringen"), + ("The structure is valid.", "Strukturen är giltig."), + ("Toggle Fill and Stroke", "Växla fyllning och kontur"), + ("Toggle double column", "Växla dubbel kolumn"), + ("Type:", "Typ:"), + ("Underline", "Understrykning"), + ("Untag Selection", "Ta bort tagg från markering"), + ("Update", "Uppdatera"), + ("Validate", "Validera"), + ("Weight", "Tjocklek"), + ("Subtract", "Subtrahera"), + ("Intersect", "Skärning"), + ("Exclude Overlap", "Uteslut överlappning"), + ("Minus Back", "Minus bakom"), + ("Make Compound", "Skapa sammansatt"), + ("Release", "Släpp"), + ("Paths", "Banor"), + ("Align left edges", "Justera vänsterkanter"), + ("Align horizontal centers", "Justera horisontella mittpunkter"), + ("Align right edges", "Justera högerkanter"), + ("Align top edges", "Justera överkanter"), + ("Align vertical centers", "Justera vertikala mittpunkter"), + ("Align bottom edges", "Justera nederkanter"), + ("Align to Selection", "Justera mot markering"), + ("Align to Key Object", "Justera mot nyckelobjekt"), + ("Align to Margins", "Justera mot marginaler"), + ("Align to Page", "Justera mot sida"), + ("Align to Spread", "Justera mot uppslag"), + ("No Text Wrap", "Ingen textombrytning"), + ("Wrap Around Bounding Box", "Ombryt runt avgränsningsramen"), + ("Wrap Around Object Shape", "Ombryt runt objektets form"), + ("Jump Object", "Hoppa över objekt"), + ("Jump to Next Column", "Hoppa till nästa spalt"), + ("Bulleted List", "Punktlista"), + ("Numbered List", "Numrerad lista"), + ("UPPERCASE", "VERSALER"), + ("Small Caps", "Kapitäler"), + ("Butt", "Rak"), + ("Round", "Rund"), + ("Projecting", "Utskjutande"), + ("Miter", "Spets"), + ("Bevel", "Fasning"), + ("Center", "Center"), + ("Start/End:", "Start/slut:"), + ("First", "Första"), + ("Last", "Senaste"), + ("Before", "Före"), + ("After", "Efter"), + ("Shading", "Skuggning"), + ("Border", "Kant"), + ("Overprint Fill", "Övertryck fyllning"), + ("Overprint Stroke", "Övertryck kontur"), + ("Overprint Gap", "Övertryck mellanrum"), + ("Nonprinting", "Skrivs inte ut"), + ("At Least", "Minst"), + ("Exactly", "Exakt"), + ("Justify", "Marginaljustera"), + ("Underline Options", "Alternativ för understrykning"), + ("Strikethrough Options", "Alternativ för genomstrykning"), + ("Character Style Options", "Alternativ för teckenstil"), + ("Advanced Character Formats", "Avancerade teckenformat"), + ("OpenType Features", "OpenType-funktioner"), + ("Style Settings:", "Stilinställningar:"), + ("Kerning:", "Kerning:"), + ("Case:", "Skiftläge:"), + ("OpenType All Small Caps", "OpenType alla kapitäler"), + ("Subscript", "Nedsänkt"), + ("OpenType Subscript", "OpenType nedsänkt"), + ("OpenType Numerator", "OpenType täljare"), + ("OpenType Denominator", "OpenType nämnare"), + ("Ligatures", "Ligaturer"), + ("No Break", "Ingen radbrytning"), + ("Horizontal Scale:", "Horisontell skala:"), + ("Vertical Scale:", "Vertikal skala:"), + ("Baseline Shift:", "Baslinjeförskjutning:"), + ("Skew:", "Skevning:"), + ("Language:", "Språk:"), + ("Tint:", "Färgton:"), + ("Figure Style:", "Formatmall för figur:"), + ("Underline On", "Understrykning på"), + ("Strikethrough On", "Genomstrykning på"), + ("Edit \"{name}\"…", "Redigera \"{name}\"…"), + ("Discretionary Ligatures", "Valfria ligaturer"), + ("Fractions", "Bråk"), + ("Ordinal", "Ordningstal"), + ("Swash", "Swash"), + ("Titling Alternates", "Rubrikalternativ"), + ("Contextual Alternates", "Kontextuella alternativ"), + ("Slashed Zero", "Nolla med snedstreck"), + ("Tabular Lining", "Tabellsiffror, versala"), + ("Proportional Oldstyle", "Proportionella medievalsiffror"), + ("Proportional Lining", "Proportionella versalsiffror"), + ("Tabular Oldstyle", "Medievalsiffror i tabellbredd"), + ("Default Figure Style", "Standardfigurstil"), + ("Not a button", "Inte en knapp"), + ("Go To Next Page", "Gå till nästa sida"), + ("Go To Previous Page", "Gå till föregående sida"), + ("Go To First Page", "Gå till första sidan"), + ("Go To Last Page", "Gå till sista sidan"), + ("Go To Page", "Gå till sida"), + ("Go To URL", "Gå till URL"), + ("Not a form field", "Inte ett formulärfält"), + ("Text Field", "Textfält"), + ("Check Box", "Kryssruta"), + ("Radio Button", "Alternativknapp"), + ("Combo Box", "Kombinationsruta"), + ("List Box", "Listruta"), + ("Signature", "Signatur"), + ("Resize width", "Ändra bredd"), + ("Resize height", "Ändra höjd"), + ("Pin top", "Fäst överkant"), + ("Pin bottom", "Fäst nederkant"), + ("Pin left", "Fäst vänster"), + ("Pin right", "Fäst höger"), + ("Inward", "Inåt"), + ("Metrics", "Mått"), + ("Optical", "Optisk"), + ("Applies to fill", "Gäller för fyllning"), + ("Applies to text", "Gäller för text"), + ("Gray", "Grå"), + ("Linear", "Linjär"), + ("Radial", "Radiell"), + ("Arabic", "Arabiska"), + ("Beveled Rectangle", "Avfasad rektangel"), + ("Chinese", "Kinesiska"), + ("Constrain proportions for width and height", "Behåll proportioner för bredd och höjd"), + ("Custom", "Anpassad"), + ("Czech", "Tjeckiska"), + ("Danish", "Danska"), + ("Dashed", "Streckad"), + ("Dotted", "Prickad"), + ("Dutch: 2005 Reform", "Nederländska: 2005 års reform"), + ("Embedded", "Inbäddad"), + ("English: Canadian", "Engelska: Kanada"), + ("English: UK", "Engelska: Storbritannien"), + ("English: USA", "Engelska: USA"), + ("Farsi", "Farsi"), + ("Finnish", "Finska"), + ("French", "Franska"), + ("French: Canadian", "Franska: kanadensisk"), + ("German: 2006 Reform", "Tyska: reformen 2006"), + ("German: Swiss 2006 Reform", "Tyska: schweizisk, reformen 2006"), + ("Greek", "Grekiska"), + ("Hebrew", "Hebreiska"), + ("Hindi", "Hindi"), + ("Hungarian", "Ungerska"), + ("Inverse Rounded Rectangle", "Omvänd rundad rektangel"), + ("Italian", "Italienska"), + ("Japanese", "Japanska"), + ("Korean", "Koreanska"), + ("Make all settings the same", "Gör alla inställningar lika"), + ("Native", "Ursprunglig"), + ("Norwegian: Bokmål", "Norska: bokmål"), + ("Orthogonal Line", "Ortogonal linje"), + ("Polish", "Polska"), + ("Portuguese", "Portugisiska"), + ("Portuguese: Brazilian", "Portugisiska: brasiliansk"), + ("Rounded Rectangle", "Rundad rektangel"), + ("Russian", "Ryska"), + ("Solid", "Enfärgad"), + ("Spanish", "Spanska"), + ("Swedish", "Svenska"), + ("Thick - Thick", "Tjock - tjock"), + ("Thick - Thin", "Tjock - tunn"), + ("Thin - Thick", "Tunn - tjock"), + ("Thin - Thin", "Tunn - tunn"), + ("Thin - Thick - Thin", "Tunn - tjock - tunn"), + ("Thick - Thin - Thick", "Tjock - tunn - tjock"), + ("Wavy", "Vågig"), + ("Straight Hash", "Rakt nummertecken"), + ("Stripes", "Ränder"), + ("Triangle", "Triangel"), + ("Turkish", "Turkiska"), + ("Ukrainian", "Ukrainska"), + ("Cell Style", "Cellstil"), + ("Table Style", "Tabellstil"), + ("Col ←", "Kol ←"), + ("Col →", "Kol →"), + ("Row ↑", "Rad ↑"), + ("Row ↓", "Rad ↓"), + ("Hide Others", "Göm övriga"), + ("Lock Others", "Lås övriga"), + ("Show All Layers", "Visa alla lager"), + ("Unlock", "Lås upp"), + ("Unlock All Layers", "Lås upp alla lager"), + ("Added", "Tillagd"), + ("Deleted", "Borttaget"), + ("Guide-based", "Stödlinjebaserad"), + ("Page:", "Sida:"), + ("Content:", "Innehåll:"), + ("Object Constraints:", "Objektbegränsningar:"), + ("Resize with Page:", "Ändra storlek med sidan:"), + ("Pin:", "Fäst:"), + ("Object-Based", "Objektbaserad"), + ("Guide-Based", "Stödlinjebaserad"), + ("Resize Page", "Ändra sidstorlek"), + ("Liquid Page Rule:", "Regel för flytande sida:"), + ("Show Parent Page Overlay", "Visa mallsidesöverlägg"), + ("Controlled by Parent", "Styrs av mallsida"), + ("US Business Card", "Amerikanskt visitkort"), + ("Object is pinned relative to the page edge. Click to release.", "Objektet är fäst relativt sidkanten. Klicka för att släppa."), + ("Click to pin the object relative to the page edge.", "Klicka för att fästa objektet relativt sidkanten."), + ( + "The object changes width as the page changes width. Click to lock the object width.", + "Objektets bredd ändras när sidans bredd ändras. Klicka för att låsa objektets bredd.", + ), + ( + "The object's width is locked. Click to let the object change width as the page changes width.", + "Objektets bredd är låst. Klicka för att låta objektets bredd ändras när sidans bredd ändras.", + ), + ( + "The object changes height as the page changes height. Click to lock the object height.", + "Objektets höjd ändras när sidans höjd ändras. Klicka för att låsa objektets höjd.", + ), + ( + "The object's height is locked. Click to let the object change height as the page changes height.", + "Objektets höjd är låst. Klicka för att låta objektets höjd ändras när sidans höjd ändras.", + ), + ("Loop", "Loopa"), + ("Object-based", "Objektbaserad"), + ("Play on Page Load", "Spela upp när sidan läses in"), + ("Re-center", "Centrera igen"), + ("Show Controls", "Visa kontroller"), + ("Signature Field", "Signaturfält"), + ("Select text to apply a condition", "Markera text för att tillämpa ett villkor"), + ("Blinds", "Persienner"), + ("Box", "Ruta"), + ("Comb", "Kam"), + ("Cover", "Täck"), + ("Dissolve", "Övertoning"), + ("Fade", "Toning"), + ("Push", "Skjut"), + ("Split", "Dela"), + ("Uncover", "Avtäck"), + ("Wipe", "Ridå"), + ("Simple", "Enkel"), + ("Simple - Wide", "Enkel – bred"), + ("Triangle - Wide", "Triangel – bred"), + ("Barbed", "Hullingförsedd"), + ("Curved", "Krökt"), + ("Circle", "Cirkel"), + ("Circle - Solid", "Cirkel – enfärgad"), + ("Square", "Kvadrat"), + ("Square - Solid", "Kvadrat – enfärgad"), + ("Bar", "Lodstreck"), + ("Hide", "Dölj"), + ("Show", "Visa"), + ("Select", "Markera"), + ("Fit Text", "Anpassa text"), + ("Preview (W)", "Förhandsgranskning (W)"), + ("Normal (W)", "Normal (W)"), + ("Conveyor", "Transportband"), + ("objects", "objekt"), + ("pages", "sidor"), + ("stories", "artiklar"), + ("items", "objekt"), + ("warnings", "varningar"), + ("Render", "Rendera"), + ("Set", "Ange"), + ("unused", "oanvänd"), + ("⚠ Missing", "⚠ Saknas"), + ("▲ Modified", "▲ Ändrad"), + ("(Text Color)", "(Textfärg)"), + ("{count} glyphs — click to insert at the text cursor", "{count} glyfer – klicka för att infoga vid textmarkören"), + ("{pages} Pages in {spreads} Spreads", "{pages} sidor i {spreads} uppslag"), + ("Text Frame", "Textram"), + ("Text Style", "Textstil"), + (" (auto)", " (auto)"), + ("Absolute Numbering (instead of Section Numbering)", "Absolut numrering (i stället för avsnittsnumrering)"), + ( + "Add and remove pages as the primary text frame's story grows and shrinks", + "Lägg till och ta bort sidor när texten i den primära textramen växer och krymper", + ), + ("Added Text (highlighted)", "Tillagd text (färgmarkerad)"), + ("Adjust Layout (objects follow the new page size)", "Anpassa layout (objekt följer den nya sidstorleken)"), + ("Auto-Fit", "Autoanpassa"), + ("Case sensitive", "Skiftlägeskänslig"), + ("Custom Tracking/Kerning", "Anpassad spärrning/kerning"), + ("Dimensions Include Stroke Weight", "Dimensioner inkluderar konturtjocklek"), + ("Enable Autocorrect", "Aktivera autokorrigering"), + ("Enable Dynamic Spelling (underline misspelled words)", "Aktivera dynamisk stavning (stryk under felstavade ord)"), + ("Grids in Back", "Rutnät bakom"), + ("H&J Violations", "Avstavnings- och justeringsfel"), + ("Include Bleed", "Inkludera utfall"), + ("Include Stroke Weight", "Inkludera konturtjocklek"), + ("Keep Violations", "Behåll regelbrott"), + ("Overprint [Black] Swatch at 100%", "Övertryck färgrutan [Black] vid 100 %"), + ("Printer's Marks", "Tryckmärken"), + ("Remove Styles and Formatting from Text and Tables", "Ta bort format och formatering från text och tabeller"), + ("Ruby", "Ruby"), + ("Show Note Anchors in Layout View", "Visa anteckningsankare i layoutvyn"), + ("Spreads", "Uppslag"), + ("Export PDF", "Exportera PDF"), + ("Standard", "Standard"), + ("Standard:", "Standard:"), + ("Compress images", "Komprimera bilder"), + ("Flattener preset", "Förinställning för tillplattning"), + ("Include document bleed", "Inkludera dokumentets utfall"), + ("Crop Marks", "Skärmärken"), + ("Bleed Marks", "Utfallsmärken"), + ("Page Information", "Sidinformation"), + ("Page Range", "Sidomfång"), + ("Page Range:", "Sidomfång:"), + ("Tagged PDF", "Taggad PDF"), + ("Desktop Printing", "Skrivbordsutskrift"), + ("Commercial Printing", "Kommersiellt tryck"), + ("Screen and Email", "Skärm och e-post"), + ("PDF/X-4", "PDF/X-4"), + ("PDF/A-2b", "PDF/A-2b"), + ("Compression", "Komprimering"), + ("Marks and Bleeds", "Märken och utfall"), + ("Preset:", "Förinställning:"), + ("Substituted Fonts", "Ersatta typsnitt"), + ("Transformations are Totals", "Transformeringar anges som totalvärden"), + ("Use Black Point Compensation", "Använd svartpunktskompensering"), + ("Use Typographer's Quotes", "Använd typografiska citattecken"), + ("Whole word", "Hela ord"), + (" Close ", " Stäng "), + ("(Top Level)", "(Översta nivån)"), + ("(import)", "(import)"), + ("Accept All", "Acceptera alla"), + ("Add Document…", "Lägg till dokument…"), + ("Add to Previous Spread", "Lägg till i föregående uppslag"), + ("Align From:", "Justera från:"), + ("Align to baseline grid", "Justera mot baslinjerutnät"), + ("All Spots to Process", "Alla dekorfärger till processfärger"), + ("Allow Spread to Shuffle", "Tillåt omflyttning av uppslag"), + ("Alternating Row Fills", "Alternerande radfyllningar"), + ("Apply Gradient", "Använd gradient"), + ("Apply None (/)", "Använd ingen (/)"), + ("Apply Parent", "Använd mallsida"), + ("Apply to All Spreads", "Använd på alla uppslag"), + ("Apply to Stroke", "Använd på kontur"), + ("Art", "Konst"), + ("Baseline Grid", "Baslinjerutnät"), + ("Bleed and Slug", "Utfall och informationsområde"), + ("Body Rows", "Brödtextrader"), + ("Break Link to Style", "Bryt länk till stil"), + ("Bullets and Numbering", "Punkter och numrering"), + ("Business Card", "Visitkort"), + ("Buttons act in interactive PDF export.", "Knappar fungerar vid export till interaktiv PDF."), + ("Change to:", "Ändra till:"), + ("Character Settings", "Teckeninställningar"), + ("Characters", "Tecken"), + ("Checked by default", "Ikryssad som standard"), + ("Choices", "Val"), + ("Choose Image…", "Välj bild…"), + ("Choose a link.", "Välj en länk."), + ("Content Fitting", "Innehållsanpassning"), + ("Convert Shape", "Konvertera form"), + ("Copies:", "Kopior:"), + ("Count", "Antal"), + ("Create", "Skapa"), + ("Create Alternate Layout", "Skapa alternativ layout"), + ("Create Table…", "Skapa tabell…"), + ("Crop", "Beskär"), + ("Crop Amount", "Beskärningsmängd"), + ("Crop to:", "Beskär till:"), + ("Cross-Reference Format", "Korsreferensformat"), + ("Cursor Key:", "Piltangent:"), + ("Default Fill and Stroke", "Standardfyllning och -kontur"), + ("Default Fill and Stroke (D)", "Standardfyllning och -kontur (D)"), + ("Default View:", "Standardvy:"), + ("Delete Column", "Ta bort kolumn"), + ("Delete Condition", "Radera villkor"), + ("Delete Row", "Ta bort rad"), + ("Delete Swatch", "Radera färgruta"), + ("Delete Unused Layers", "Radera oanvända lager"), + ("Delete Workspace", "Radera arbetslayout"), + ("Document Grid", "Dokumentrutnät"), + ("Document Recovery Data", "Återställningsdata för dokument"), + ("Duplicate Spread", "Duplicera uppslag"), + ("Duplicate Spread at End of Document", "Duplicera uppslag i slutet av dokumentet"), + ("Delete Spread", "Radera uppslag"), + ("Parent Pages", "Mallsidor"), + ("Apply Parent to Pages", "Använd mallsida på sidor"), + ("Email", "E-post"), + ("Embed", "Bädda in"), + ("Fast", "Snabb"), + ("Fill (X)", "Fyllning (X)"), + ("Fill with Placeholder Text", "Fyll med platshållartext"), + ("Find All", "Sök alla"), + ("Find what:", "Sök efter:"), + ("Fit frame height to text", "Anpassa ramhöjden till texten"), + ("Fitting:", "Anpassning:"), + ("Flip Horizontal", "Vänd vågrätt"), + ("Flip Vertical", "Vänd lodrätt"), + ("Font Size:", "Teckenstorlek:"), + ("Form Field", "Formulärfält"), + ("Format:", "Format:"), + ("Height:", "Höjd:"), + ("Hide Bar", "Dölj fältet"), + ("High Quality", "Hög kvalitet"), + ("Highlight", "Högdager"), + ("Horizontal Gridline Every:", "Horisontell stödlinje var:"), + ("Horizontal Margins:", "Horisontella marginaler:"), + ("Import XML…", "Importera XML…"), + ("Increment Every:", "Öka var:"), + ("Ink Manager", "Tryckfärgshanterare"), + ("Insert Column", "Infoga kolumn"), + ("Insert Page After", "Infoga sida efter"), + ("Insert Row", "Infoga rad"), + ("Inset spacing", "Infällningsavstånd"), + ("Intent:", "Återgivningsmetod:"), + ("Invert", "Invertera"), + ("Keyboard Increments", "Tangentbordssteg"), + ("Kind:", "Typ:"), + ("Link To:", "Länka till:"), + ("Liquid guides", "Flytande stödlinjer"), + ("Load Profile…", "Läs in profil…"), + ("Load Swatches…", "Läs in färgrutor…"), + ("Load…", "Läs in…"), + ("Media", "Media"), + ("Merge into Active Layer", "Sammanfoga med aktivt lager"), + ("Misspelled word → Correction (one per line):", "Felstavat ord → Korrigering (ett per rad):"), + ("Mobile", "Mobil"), + ("More ArtCraft apps", "Fler ArtCraft-appar"), + ("Move Selection Here", "Flytta markeringen hit"), + ("Move to Color Group", "Flytta till färggrupp"), + ("Move to Group", "Flytta till grupp"), + ("Multiline", "Multilinje"), + ("Multiple Objects", "Flera objekt"), + ("Name:", "Namn:"), + ("New Bookmark for This Page", "Nytt bokmärke för den här sidan"), + ("New Book…", "Ny bok…"), + ("New Color Group", "Ny färggrupp"), + ("New Color Group with Swatch", "Ny färggrupp med färgruta"), + ("New Library…", "Nytt bibliotek…"), + ("New Workspace…", "Ny arbetslayout…"), + ("No articles: exports follow page order.", "Inga artiklar: exporten följer sidordningen."), + ("No book open.", "Ingen bok är öppen."), + ("No bookmarks. They become the PDF outline.", "Inga bokmärken. De blir PDF-filens disposition."), + ("No changes.", "Inga ändringar."), + ( + "No conditions. Create one, then select text and click it to apply.", + "Inga villkor. Skapa ett, markera sedan text och klicka på villkoret för att använda det.", + ), + ("No document.", "Inget dokument."), + ( + "No hyperlinks. Select text or a frame, type a URL and click New.", + "Inga hyperlänkar. Markera text eller en ram, skriv en URL och klicka på Nytt.", + ), + ("No library open.", "Inget bibliotek är öppet."), + ("No notes in this document.", "Det finns inga anteckningar i det här dokumentet."), + ("No placed graphics.", "Ingen placerad grafik."), + ("No scripts. Add one below.", "Inga skript. Lägg till ett nedan."), + ("No spot inks", "Inga dekorfärger"), + ("No text anchors in this document.", "Det finns inga textankare i det här dokumentet."), + ("Number of Pages:", "Antal sidor:"), + ("Number of Sides:", "Antal sidor:"), + ("Number of columns", "Antal spalter"), + ("OK changes all: text and paragraph/character styles.", "OK ändrar allt: text samt stycke- och teckenstilar."), + ("OVERSET", "ÖVERSKJUTANDE"), + ("On Release", "Vid släpp"), + ("On Screen:", "På skärmen:"), + ("Open source. No telemetry.", "Öppen källkod. Ingen telemetri."), + ("Options for Black on RGB and Grayscale Devices", "Alternativ för svart på RGB- och gråskaleenheter"), + ("Orientation:", "Orientering:"), + ("Page Numbering", "Sidnumrering"), + ("Page · PPI", "Sida · PPI"), + ("Pages:", "Sidor:"), + ("Part of the ArtCraft suite of open creative tools.", "Del av ArtCraft-sviten av öppna kreativa verktyg."), + ("Pasteboard Options", "Alternativ för montageyta"), + ("Path", "Bana"), + ("Place the insertion point in a table, or create one.", "Placera insättningspunkten i en tabell eller skapa en."), + ("Plain Text", "Oformaterad text"), + ("Plays in EPUB export; print and PDF show the poster.", "Spelas upp vid EPUB-export; utskrift och PDF visar affischen."), + ("Poster", "Affisch"), + ("Printer:", "Skrivare:"), + ("Printing / Exporting:", "Utskrift/export:"), + ("Properties Panel", "Egenskapspanel"), + ("Recently Used", "Senast använda"), + ("Reject All", "Avvisa alla"), + ("Relative To:", "Relativt till:"), + ("Relink to Folder…", "Länka om till mapp…"), + ("Relink…", "Länka om…"), + ("Remove Conditions from Selection", "Ta bort villkor från markeringen"), + ("Repeat Header Rows", "Upprepa rubrikrader"), + ("Replace With", "Ersätt med"), + ("Required", "Obligatoriskt"), + ("Reset All to Defaults", "Återställ alla till standardvärden"), + ("Reverse", "Omvänd"), + ("Right-click: stroke, groups", "Högerklicka: kontur, grupper"), + ("Rotate 90° Clockwise", "Rotera 90° medurs"), + ("Ruler Units", "Linjalenheter"), + ("Save Swatches for Exchange…", "Spara färgrutor för utbyte…"), + ("Save recovery data every:", "Spara återställningsdata var:"), + ("Saves which bars show and where the panels are.", "Sparar vilka fält som visas och var panelerna finns."), + ("Search:", "Sök:"), + ( + "Select a placed video or sound (File › Place a .mp4, .mov, .mp3, .wav …).", + "Markera en placerad video eller ett placerat ljud (Arkiv › Placera en .mp4, .mov, .mp3, .wav …).", + ), + ("Select an object or text to color its fill or stroke.", "Markera ett objekt eller text för att färglägga fyllning eller kontur."), + ("Select an object to give its fill a gradient.", "Markera ett objekt för att ge dess fyllning en gradient."), + ("Select an object to make it a button.", "Markera ett objekt för att göra det till en knapp."), + ("Select an object.", "Markera ett objekt."), + ("Select text or a text frame.", "Markera text eller en textram."), + ( + "Select two or more objects (or a group) to make a multi-state object.", + "Markera två eller fler objekt (eller en grupp) för att skapa ett objekt med flera lägen.", + ), + ("Show Tag Markers", "Visa taggmarkörer"), + ("Single-line", "Enradig"), + ("Smart Text Reflow", "Smart textomflödning"), + ("Speed", "Hastighet"), + ("Star Inset:", "Stjärninfällning:"), + ("Start Page #:", "Startsida nr:"), + ("Start:", "Start:"), + ("Status", "Status"), + ("Stroke (X)", "Kontur (X)"), + ("Style Mapping", "Stilmappning"), + ("Style Name Conflicts", "Stilnamnskonflikter"), + ("Stylistic Sets", "Stilistiska uppsättningar"), + ("Subdivisions:", "Underindelningar:"), + ("Swap Fill and Stroke", "Byt plats på fyllning och kontur"), + ("Swap Fill and Stroke (Shift+X)", "Byt plats på fyllning och kontur (Shift+X)"), + ("Synchronize", "Synkronisera"), + ("System default", "Systemstandard"), + ("Table Dimensions", "Tabelldimensioner"), + ("Text Display Options", "Alternativ för textvisning"), + ("Text Message", "Textmeddelande"), + ("The fill isn't a gradient.", "Fyllningen är inte en gradient."), + ("The library is empty. Select objects and Add Selection.", "Biblioteket är tomt. Markera objekt och välj Lägg till markering."), + ("This PDF has no layers.", "Den här PDF-filen har inga lager."), + ("Track Changes in All Stories", "Spåra ändringar i alla artiklar"), + ("Transitions play in interactive PDF (full-screen mode).", "Övergångar spelas upp i interaktiv PDF (helskärmsläge)."), + ("Trim", "Trimma"), + ("Type Options", "Textalternativ"), + ("Typical", "Normal"), + ("UI Size:", "Gränssnittsstorlek:"), + ("Ungroup Color Group", "Dela upp färggrupp"), + ("Unmerge", "Dela"), + ("Update Numbering", "Uppdatera numrering"), + ("Use Global Light", "Använd globalt ljus"), + ("User Dictionary (this document)", "Användarordlista (det här dokumentet)"), + ("Variable Font", "Variabelt typsnitt"), + ("Vertical Gridline Every:", "Vertikal stödlinje var:"), + ("Vertical Margins:", "Vertikala marginaler:"), + ("Vertical justification", "Vertikal justering"), + ("View Threshold:", "Visningströskel:"), + ("Web", "Webb"), + ("Web Hyperlink", "Webbhyperlänk"), + ("When Scaling", "Vid skalning"), + ("Words, one per line:", "Ord, ett per rad:"), + ("[Basic] (working) ▾", "[Grundläggande] (arbetar) ▾"), + ("[Black] Overprint", "[Svart] övertryck"), + ("[No Group]", "[Ingen grupp]"), + ("[None] — choose a colour on the ramp.", "[Ingen] — välj en färg på färgrampen."), + ("⌥-click clears overrides", "⌥-klicka för att rensa lokala ändringar"), + ("⚠ out of date", "⚠ inaktuell"), + ("Interface", "Gränssnitt"), + ("Advanced Type", "Avancerad typografi"), + ("Composition", "Komposition"), + ("Show Font Names in English", "Visa typsnittsnamn på engelska"), + ("Missing Glyphs", "Saknade glyfer"), + ("Draw Missing Glyphs from Fallback Fonts", "Rita saknade glyfer från reservtypsnitt"), + ("Units & Increments", "Enheter och steg"), + ("Grids", "Rutnät"), + ("Guides & Pasteboard", "Stödlinjer och montageyta"), + ("Dictionary", "Ordlista"), + ("Spelling", "Stavning"), + ("Autocorrect", "Autokorrigering"), + ("Track Changes", "Spåra ändringar"), + ("Story Editor Display", "Visning i artikelredigeraren"), + ("Display Performance", "Visningsprestanda"), + ("Appearance of Black", "Utseende för svart"), + ("File Handling", "Filhantering"), + ("Formatting affects container (J)", "Formateringen påverkar behållaren (J)"), + ("Formatting affects text (J)", "Formateringen påverkar texten (J)"), + ("Apply Color", "Använd färg"), + ("Repeat", "Upprepa"), + ("Count:", "Antal:"), + ("Create as a grid", "Skapa som rutnät"), + ("Rows:", "Rader:"), + ("Columns:", "Kolumner:"), + ("Vertical:", "Vertikalt:"), + ("Horizontal:", "Horisontellt:"), + ("Invalid value", "Ogiltigt värde"), + ("Isolate Blending", "Isolera blandning"), + ("Knockout Group", "Utsparningsgrupp"), + ("Object:", "Objekt:"), + ("Group:", "Grupp:"), + ("Sample Color (Eyedropper)", "Ta färgprov (pipett)"), + ("Apply None", "Använd ingen"), + ("Height Only", "Endast höjd"), + ("Width Only", "Endast bredd"), + ("Height and Width", "Höjd och bredd"), + ("Height only", "Endast höjd"), + ("Width only", "Endast bredd"), + ("Height and width", "Höjd och bredd"), + ("Version", "Version"), + ("{count} characters don't fit", "{count} tecken får inte plats"), + ("document", "dokument"), + ("story", "artikel"), + ("selection", "markering"), + ("top", "överkant"), + ("center", "center"), + ("bottom", "nederkant"), + ("justify", "marginaljustera"), + ("text", "text"), + ("graphic", "grafik"), + ("unassigned", "ej tilldelad"), + ("Perceptual", "Perceptuell"), + ("Relative Colorimetric", "Relativ kolorimetrisk"), + ("Saturation", "Mättnad"), + ("Absolute Colorimetric", "Absolut kolorimetrisk"), + ("No conflicts", "Inga konflikter"), + ("Page (1–{count}):", "Sida (1–{count}):"), + ("Fonts in Document: {count} Missing: {missing}", "Typsnitt i dokumentet: {count} Saknas: {missing}"), + ("Story Direction", "Artikelriktning"), + ("Tate-Chu-Yoko", "Tate-chu-yoko"), + ("Ruby…", "Ruby…"), + ("Kenten", "Kenten"), + ("Warichu", "Warichu"), + ("Tabs", "Tabbstopp"), + ("Story", "Artikel"), + ("Create Outlines", "Skapa konturbanor"), + ("Show Hidden Characters", "Visa dolda tecken"), + ("Unmerge Cells", "Dela celler"), + ("Snap to Guides", "Fäst mot stödlinjer"), + ("Snap to Document Grid", "Fäst mot dokumentrutnät"), + ("Align to Object Edges", "Justera mot objektens kanter"), + ("Align to Object Centers", "Justera mot objektens mittpunkter"), + ("Smart Dimensions", "Smarta mått"), + ("Smart Spacing", "Smarta avstånd"), + ("Snap to Zone", "Fäst mot zon"), + ("Info", "Info"), + ("Index", "Register"), + ("Articles", "Artiklar"), + ("Tags", "Taggar"), + ("Object States", "Objekttillstånd"), + ("Liquid Layout", "Flytande layout"), + ("Split Window", "Dela fönster"), + ("New Window", "Nytt fönster"), + ("CMYK", "CMYK"), + ("Lab", "Lab"), + ("OpenType", "OpenType"), + ("RGB", "RGB"), + ("Accept All Changes", "Acceptera alla ändringar"), + ("Activate Document", "Aktivera dokument"), + ("Add Anchor Point", "Lägg till ankarpunkt"), + ("Add Document", "Lägg till dokument"), + ("Add Hyphenation Exception", "Lägg till avstavningsundantag"), + ("Add Item", "Lägg till objekt"), + ("Add Page to Spread", "Lägg till sida i uppslag"), + ("Allow Selected Spread to Shuffle", "Tillåt omflyttning av markerat uppslag"), + ("Anchored Object Options…", "Alternativ för förankrat objekt…"), + ("Anchored Objects", "Förankrade objekt"), + ("Apply Attributes (Eyedropper)", "Använd attribut (pipett)"), + ("Apply Cell Style", "Använd cellstil"), + ("Apply Condition", "Använd villkor"), + ("Apply Parent to Pages…", "Använd mallsida på sidor…"), + ("ArtCraft App Page…", "ArtCrafts appsida…"), + ("ArtCraft Website…", "ArtCrafts webbplats…"), + ("Attributes", "Attribut"), + ("Bevel and Emboss", "Fasning och relief"), + ("Buttons", "Knappar"), + ("Cell Style Options…", "Alternativ för cellstil…"), + ("Change All (Object)", "Ändra alla (objekt)"), + ("Changes", "Ändringar"), + ("Character Formatting", "Teckenformatering"), + ("Clear Conveyor", "Rensa transportband"), + ("Clear Transformations", "Rensa transformeringar"), + ("Clipping Path…", "Klippbana…"), + ("Close Book", "Stäng bok"), + ("Close Library", "Stäng bibliotek"), + ("Collect", "Samla"), + ("Color Settings…", "Färginställningar…"), + ("Community & Project Links", "Länkar till gemenskap och projekt"), + ("Container", "Behållare"), + ("Content", "Innehåll"), + ("Contextual Task Bar", "Kontextuellt aktivitetsfält"), + ("Control", "Kontroll"), + ("Convert Cell to Graphic Cell", "Konvertera cell till grafikcell"), + ("Convert Cell to Graphic Cell…", "Konvertera cell till grafikcell…"), + ("Convert Cell to Text Cell", "Konvertera cell till textcell"), + ("Convert Color", "Konvertera färg"), + ("Convert Direction Point", "Konvertera riktningspunkt"), + ("Convert Note to Text", "Konvertera anteckning till text"), + ("Convert Table to Text", "Konvertera tabell till text"), + ("Convert Text to Table", "Konvertera text till tabell"), + ("Convert to Button", "Konvertera till knapp"), + ("Convert to Form Field", "Konvertera till formulärfält"), + ("Copy Link(s) To…", "Kopiera länk(ar) till…"), + ("Corner Options…", "Hörnalternativ…"), + ("Create Alternate Layout…", "Skapa alternativ layout…"), + ("Create Frame", "Skapa ram"), + ("Create Merged Document…", "Skapa sammanfogat dokument…"), + ("Cross-Reference Formats", "Korsreferensformat"), + ("Cross-References", "Korsreferenser"), + ("Data Merge Fields", "Fält för datasammanfogning"), + ("Define Cross-Reference Format…", "Definiera korsreferensformat…"), + ("Define Lists…", "Definiera listor…"), + ("Define Text Variable…", "Definiera textvariabel…"), + ("Delete All Guides on Spread", "Radera alla stödlinjer på uppslaget"), + ("Delete Anchor Point", "Radera ankarpunkt"), + ("Delete Item", "Radera objekt"), + ("Delete Text Variable", "Radera textvariabel"), + ("Delete Workspace…", "Radera arbetslayout…"), + ("DesignCraft Website…", "DesignCrafts webbplats…"), + ("DesignCraft on GitHub…", "DesignCraft på GitHub…"), + ("Detach All Objects from Parent", "Koppla bort alla objekt från mallsidan"), + ("Directional Feather", "Riktad fjädring"), + ("Distribute", "Fördela"), + ("Dock Panel", "Docka panel"), + ("Document Endnote Options…", "Alternativ för dokumentets slutnoter…"), + ("Document Preferences", "Inställningar för dokument"), + ("Document Presets", "Dokumentförinställningar"), + ("Document…", "Dokument…"), + ("Edit Parents", "Redigera mallsidor"), + ("Edit Story", "Redigera text"), + ("Endnotes", "Slutnoter"), + ("Entire Pasteboard", "Hela montageytan"), + ("Erase", "Radera"), + ("Export Book to PDF…", "Exportera bok till PDF…"), + ("Export EPUB (Reflowable)…", "Exportera EPUB (flytande)…"), + ("Export Selection as Snippet", "Exportera markering som fragment"), + ("Extend Text Selection", "Utöka textmarkering"), + ("Fast Display", "Snabb visning"), + ("Find Object", "Sök objekt"), + ("Find by Script Label", "Sök efter skriptetikett"), + ("First Object Above", "Första objektet ovanför"), + ("Fit Selection in Window", "Anpassa markeringen till fönstret"), + ("Flattener Preview", "Förhandsgranskning av tillplattning"), + ("Float Panel", "Gör panelen flytande"), + ("Fonts in Document", "Typsnitt i dokumentet"), + ("Form Fields", "Formulärfält"), + ("Frame Fitting Options…", "Alternativ för ramanpassning…"), + ("Generate Index…", "Generera register…"), + ("Generate QR Code…", "Generera QR-kod…"), + ("Generate Static Caption", "Generera statisk bildtext"), + ("Get Story", "Hämta artikel"), + ("Get Table", "Hämta tabell"), + ("Global Light…", "Globalt ljus…"), + ("Go To Link", "Gå till länk"), + ("Go To Source", "Gå till källa"), + ("Gradient Feather", "Gradientfjädring"), + ("Gridify", "Placera i rutnät"), + ("Group Transparency", "Grupptransparens"), + ("Hide Menu Item", "Dölj menyalternativ"), + ("Hide/Lock Others", "Dölj/lås övriga"), + ("High Quality Display", "Visning med hög kvalitet"), + ("History", "Historik"), + ("Import Options: Styles", "Importalternativ: Stilar"), + ("Index Page References", "Sidreferenser i registret"), + ("Inner Glow", "Inre glöd"), + ("Inner Shadow", "Inre skugga"), + ("Insert Anchored Object…", "Infoga förankrat objekt…"), + ("Insert Cross-Reference…", "Infoga korsreferens…"), + ("Insert Endnote", "Infoga slutnot"), + ("Insert Math Expression…", "Infoga matematiskt uttryck…"), + ("Insert Pages…", "Infoga sidor…"), + ("Insert Variable", "Infoga variabel"), + ("Inspect Document", "Inspektera dokumentet"), + ("Interface Color Theme", "Färgtema för gränssnittet"), + ("Join the ArtCraft Discord…", "Gå med i ArtCraft på Discord…"), + ("Last Object Below", "Sista objektet nedanför"), + ("Library Contents", "Biblioteksinnehåll"), + ("Library Items", "Biblioteksobjekt"), + ("Liquid Page Rule", "Regel för flytande sida"), + ("List Documents", "Lista dokument"), + ("List Styles", "Lista stilar"), + ("List Tools", "Lista verktyg"), + ("Load Place Cursor", "Läs in placeringsmarkör"), + ("Make Compound Path", "Skapa sammansatt bana"), + ("Map Styles to Tags", "Mappa stilar till taggar"), + ("Math Expression SVG", "SVG för matematiska uttryck"), + ("Media Options", "Mediealternativ"), + ("Menus…", "Menyer…"), + ("Merge Layers", "Sammanfoga lager"), + ("Move Caret", "Flytta textmarkören"), + ("Move Gap", "Flytta mellanrum"), + ("Move Row", "Flytta rad"), + ("New Book", "Ny bok"), + ("New Cell Style…", "Ny cellstil…"), + ("New Color Swatch…", "Ny färgruta…"), + ("New Condition…", "Nytt villkor…"), + ("New Guide", "Ny stödlinje"), + ("New Hyperlink Destination…", "Nytt hyperlänkmål…"), + ("New Hyperlink…", "Ny hyperlänk…"), + ("New Library", "Nytt bibliotek"), + ("New Page Reference…", "Ny sidreferens…"), + ("New Stroke Style…", "Ny konturstil…"), + ("New Table Style…", "Ny tabellstil…"), + ("Next Cell", "Nästa cell"), + ("Next Document", "Nästa dokument"), + ("Next Object Above", "Nästa objekt ovanför"), + ("Next Object Below", "Nästa objekt nedanför"), + ("Next Object in Group", "Nästa objekt i gruppen"), + ("Object Export Options", "Exportalternativ för objekt"), + ("Object Export Options…", "Exportalternativ för objekt…"), + ("Object Layer Options…", "Lageralternativ för objekt…"), + ("Object Layers", "Objektlager"), + ("Object Liquid Settings", "Inställningar för flexibel layout"), + ("Open Book", "Öppna bok"), + ("Open Bytes", "Öppna byte"), + ("Open IDML", "Öppna IDML"), + ("Open Library", "Öppna bibliotek"), + ("Outer Glow", "Yttre glöd"), + ("Overprint Preview", "Förhandsgranskning av övertryck"), + ("Override All Parent Page Items", "Ändra alla objekt från mallsidan lokalt"), + ("Page Transition", "Sidövergång"), + ("Page Transitions", "Sidövergångar"), + ("Paragraph Formatting", "Styckeformatering"), + ("Paste Into", "Klistra in i"), + ("Paste without Formatting", "Klistra in utan formatering"), + ("Place Caret", "Placera insättningspunkt"), + ("Place Item", "Placera objekt"), + ("Place Snippet", "Placera fragment"), + ("Place and Link", "Placera och länka"), + ("Polygon Settings…", "Polygoninställningar…"), + ("Preflight Document", "Förhandskontrollera dokument"), + ("Previous Cell", "Föregående cell"), + ("Previous Document", "Föregående dokument"), + ("Previous Object in Group", "Föregående objekt i gruppen"), + ("Printers", "Skrivare"), + ("Proof Colors", "Provtrycksfärger"), + ("Proof Setup", "Provtrycksinställningar"), + ("Quick Apply…", "Snabbanvänd…"), + ("Recover Documents", "Återställ dokument"), + ("Recovery Data", "Återställningsdata"), + ("Reject All Changes", "Avvisa alla ändringar"), + ("Release Compound Path", "Släpp sammansatt bana"), + ("Remove All Local Overrides", "Ta bort alla lokala ändringar"), + ("Remove Document", "Ta bort dokument"), + ("Report an Issue…", "Rapportera ett problem…"), + ("Reset Shortcuts", "Återställ kortkommandon"), + ("Reset Workspace", "Återställ arbetslayout"), + ("Resize", "Ändra storlek"), + ("Rotate Spread", "Rotera uppslag"), + ("Run Script", "Kör skript"), + ("Sample Document", "Exempeldokument"), + ("Satin", "Satin"), + ("Save Recovery Data", "Spara återställningsdata"), + ("Script Label", "Skriptetikett"), + ("Select All on Layer", "Markera allt på lagret"), + ("Select Cells", "Markera celler"), + ("Select Column", "Markera kolumn"), + ("Select Frame", "Markera ram"), + ("Select Row", "Markera rad"), + ("Select Table", "Markera tabell"), + ("Select Text", "Markera text"), + ("Select Tool", "Markeringsverktyg"), + ("Select Word", "Markera ord"), + ("Selection Attributes", "Markeringsattribut"), + ("Separations Preview", "Förhandsgranskning av separationer"), + ("Serialize", "Serialisera"), + ("Set Active Layer", "Ange aktivt lager"), + ("Set Shortcut", "Ange kortkommando"), + ("Set Visibility / Lock", "Ange synlighet/lås"), + ("Show Panel", "Visa panel"), + ("Show/Hide Baseline Grid", "Visa/dölj baslinjerutnät"), + ("Show/Hide Frame Edges", "Visa/dölj ramkanter"), + ("Show/Hide Guides", "Visa/dölj stödlinjer"), + ("Show/Hide Hidden Characters", "Visa/dölj dolda tecken"), + ("Show/Hide Panels", "Visa/dölj paneler"), + ("Show/Hide Panels Except Tools", "Visa/dölj paneler utom verktyg"), + ("Show/Hide Rulers", "Visa/dölj linjaler"), + ("Show/Hide Tagged Frames", "Visa/dölj taggade ramar"), + ("Show/Hide Text Threads", "Visa/dölj textflöden"), + ("Smooth", "Mjuk"), + ("Sort", "Sortera"), + ("Split Path", "Dela bana"), + ("Step and Repeat…", "Steg och upprepa…"), + ("Stroke Styles", "Konturstilar"), + ("Style Source", "Stilkälla"), + ("Synchronize Book", "Synkronisera bok"), + ("Table Panel", "Tabellpanel"), + ("Table Style Options…", "Alternativ för tabellstil…"), + ("Table of Contents Entries", "Poster i innehållsförteckningen"), + ("Table of Contents…", "Innehållsförteckning…"), + ("Text Anchors", "Textankare"), + ("Text Variables", "Textvariabler"), + ("Toggle Normal/Preview", "Växla normal/förhandsgranskning"), + ("Toggle Selection", "Växla markering"), + ("Tools: Double Column", "Verktyg: dubbel kolumn"), + ("Transform Panel", "Transformeringspanel"), + ("Transform Values", "Transformeringsvärden"), + ("Transparency Blend Space", "Blandningsrymd för transparens"), + ("Transparency Flattener Presets", "Förinställningar för transparensutjämning"), + ("Type on a Path", "Text på bana"), + ("Type on a Path Options…", "Alternativ för text på bana…"), + ("Typical Display", "Normal visning"), + ("Update Link", "Uppdatera länk"), + ("User Dictionary Words", "Ord i användarordlista"), + ("User Dictionary…", "Användarordlista…"), + ("Zoom To", "Zooma till"), + ("⇧", "⇧"), + ("⌃", "⌃"), + ("⌘", "⌘"), + ("⌥", "⌥"), + ("Accept Change", "Acceptera ändring"), + ("Add Anchor", "Lägg till ankare"), + ("Add Selection to Article", "Lägg till markering i artikel"), + ("Add Unnamed Colors", "Lägg till namnlösa färger"), + ("Add to Dictionary", "Lägg till i ordlistan"), + ("Align Center", "Centrera"), + ("Align Left", "Justera vänster"), + ("Align Right", "Justera höger"), + ("Apply Character Style", "Använd teckenstil"), + ("Apply Object Style", "Använd objektstil"), + ("Apply Paragraph Style", "Använd styckestil"), + ("Apply Table Style", "Använd tabellstil"), + ("Article Options", "Artikelalternativ"), + ("Basic Feather", "Grundläggande fjädring"), + ("Bold", "Fet"), + ("Character Style Options…", "Alternativ för teckenstil…"), + ("Close Path", "Stäng bana"), + ("Color Group Options", "Alternativ för färggrupp"), + ("Condition Options…", "Villkorsalternativ…"), + ("Convert to Liquid Guide", "Konvertera till flytande stödlinje"), + ("Convert to Object", "Konvertera till objekt"), + ("Create Line", "Skapa linje"), + ("Create Path", "Skapa bana"), + ("Decrease Point Size", "Minska punktstorlek"), + ("Delete Article", "Radera artikel"), + ("Delete Bookmark", "Ta bort bokmärke"), + ("Delete Endnote", "Radera slutnot"), + ("Delete Footnote", "Radera fotnot"), + ("Delete Guide", "Radera stödlinje"), + ("Delete Hyperlink", "Radera hyperlänk"), + ("Delete List", "Radera lista"), + ("Delete Note", "Radera anteckning"), + ("Delete Pages", "Radera sidor"), + ("Delete Paragraph Style", "Radera styckestil"), + ("Delete Stroke Style", "Radera konturstil"), + ("Delete Table", "Ta bort tabell"), + ("Delete Tag", "Radera tagg"), + ("Delete Text", "Radera text"), + ("Distribute Columns Evenly", "Fördela kolumner jämnt"), + ("Edit Endnote", "Redigera slutnot"), + ("Edit Note", "Redigera anteckning"), + ("Embed Link", "Bädda in länk"), + ("Flip", "Vänd"), + ("Hyperlink Options…", "Hyperlänkalternativ…"), + ("Increase Point Size", "Öka punktstorlek"), + ("Insert Column Left", "Infoga kolumn till vänster"), + ("Insert Column Right", "Infoga kolumn till höger"), + ("Insert Row Above", "Infoga rad ovanför"), + ("Insert Row Below", "Infoga rad nedanför"), + ("Italic", "Kursiv"), + ("Justify Left", "Marginaljustera vänster"), + ("Layer Options…", "Lageralternativ…"), + ("Move Anchors", "Flytta ankarpunkter"), + ("Move Column", "Flytta kolumn"), + ("Move Guide", "Flytta stödlinje"), + ("Move Layer", "Flytta lager"), + ("Move Pages…", "Flytta sidor…"), + ("Move to Layer", "Flytta till lager"), + ("New Article", "Ny artikel"), + ("New Bookmark", "Nytt bokmärke"), + ("New Layer…", "Nytt lager…"), + ("New Note", "Ny anteckning"), + ("New Object Style…", "Ny objektstil…"), + ("New Parent…", "Ny mallsida…"), + ("New Tag", "Ny tagg"), + ("Paste in Place", "Klistra in på plats"), + ("Place Loaded Graphic", "Placera inläst grafik"), + ("Reject Change", "Avvisa ändring"), + ("Release State to Objects", "Släpp tillstånd till objekt"), + ("Remove Hyphenation Exception", "Ta bort avstavningsundantag"), + ("Remove from Article", "Ta bort från artikel"), + ("Rename", "Byt namn"), + ("Rename Bookmark", "Byt namn på bokmärke"), + ("Rotate", "Rotera"), + ("Scale", "Skala"), + ("Set Cross-Reference Format", "Ange korsreferensformat"), + ("Set Footnote Text", "Ange fotnotstext"), + ("Set Story Text", "Ange artikeltext"), + ("Shear", "Skjuva"), + ("Show All on Spread", "Visa allt på uppslaget"), + ("Show State", "Visa tillstånd"), + ("State Options", "Tillståndsalternativ"), + ("Tag Frame", "Tagga ram"), + ("Tag Text", "Tagga text"), + ("Unlink", "Ta bort länk"), + ("Update Index", "Uppdatera register"), + ("Update Table of Contents", "Uppdatera innehållsförteckning"), + ("Bulleted and Numbered Lists", "Punktlistor och numrerade listor"), + ("Hyperlinks Cross-References", "Hyperlänkar, korsreferenser"), + ("Document Footnote Options...", "Alternativ för dokumentets fotnoter..."), + ("Anchored Object", "Förankrat objekt"), + ("Apply Attributes (Eyedropper", "Använd attribut (pipett"), + ("Captions", "Bildtexter"), + ("Change All (Object", "Ändra alla (objekt"), + ("Clipping Path", "Klippbana"), + ( + "Collect objects with the Content Collector (B); place them with the Content Placer.", + "Samla objekt med innehållsinsamlaren (B) och placera dem med innehållsplaceraren.", + ), + ("Convert Cell Type", "Konvertera celltyp"), + ("Copy Link(s", "Kopiera länk(ar"), + ("DesignCraft", "DesignCraft"), + ("Export EPUB (Fixed Layout", "Exportera EPUB (fast layout"), + ("Export EPUB (Reflowable", "Exportera EPUB (flytande"), + ("Hyperlinks & Cross-References", "Hyperlänkar och korsreferenser"), + ("Hyphens and Dashes", "Bindestreck och tankstreck"), + ("Markers", "Markörer"), + ("Other", "Annat"), + ("Output", "Utdata"), + ("Quotation Marks", "Citattecken"), + ("Symbols", "Symboler"), + ("Type & Tables", "Text och tabeller"), + ( + "Type a word with ~ at each allowed break (ex~am~ple); without ~ the word is never hyphenated.", + "Skriv ett ord med ~ vid varje tillåten avstavning (ex~em~pel); utan ~ avstavas ordet aldrig.", + ), + ("Utilities", "Verktyg"), + ("Paragraph Rules…", "Styckelinjer…"), + ("Paragraph Rules", "Styckelinjer"), + ("Rule Below", "Linje nedanför"), + ("90° CCW", "90° moturs"), + ("90° CW", "90° medurs"), + ("A, B, C, D...", "A, B, C, D..."), + ("All records", "Alla poster"), + ("Alpha Channel", "Alfakanal"), + ("Anchored Object Options", "Alternativ för förankrat objekt"), + ("ArtCraft App Page", "ArtCrafts appsida"), + ("ArtCraft Website", "ArtCrafts webbplats"), + ("Bind to frame", "Koppla till ramen"), + ("Black", "Svart"), + ("Bleed:", "Utfall:"), + ("Bullet Character", "Punkttecken"), + ("CMYK:", "CMYK:"), + ("Center in frame", "Centrera i ramen"), + ("Check Spelling", "Kontrollera stavning"), + ("Clear Rotation", "Rensa rotation"), + ("Closed Path", "Sluten bana"), + ("Color Blindness – Deuteranopia", "Färgblindhet – deuteranopi"), + ("Color Blindness – Protanopia", "Färgblindhet – protanopi"), + ("Column Break", "Kolumnbrytning"), + ("Column spacing", "Kolumnavstånd"), + ("Columns first", "Kolumner först"), + ("Conditional Text", "Villkorlig text"), + ("Copyright Symbol", "Upphovsrättssymbol"), + ("Corner Options", "Hörnalternativ"), + ("Create Guides", "Skapa stödlinjer"), + ("Cyan", "Cyan"), + ("Dark", "Mörk"), + ("Data Merge", "Datasammanfogning"), + ("Define Cross-Reference Format", "Definiera korsreferensformat"), + ("Define Lists", "Definiera listor"), + ("Define Text Variable", "Definiera textvariabel"), + ("Delete Type from Path", "Radera text från bana"), + ("DesignCraft Website", "DesignCrafts webbplats"), + ("DesignCraft on GitHub", "DesignCraft på GitHub"), + ("Detect Edges", "Identifiera kanter"), + ("Discretionary Hyphen", "Mjukt bindestreck"), + ("Document CMYK", "Dokument-CMYK"), + ("Document Endnote Options", "Alternativ för dokumentets slutnoter"), + ("Document Footnote Options", "Alternativ för dokumentets fotnoter"), + ("Document RGB", "Dokument-RGB"), + ("Double Left Quotation Marks", "Dubbla vänstercitattecken"), + ("Double Right Quotation Marks", "Dubbla högercitattecken"), + ("Edit/Interface Language", "Redigera/Gränssnittsspråk"), + ("Edit/Spelling", "Redigera/Stavning"), + ("Edit/Transparency Blend Space", "Redigera/Färgrymd för transparensblandning"), + ("Edit/Transparency Flattener Presets", "Redigera/Förinställningar för transparensutjämning"), + ("Ellipsis", "Ellips"), + ("Em Dash", "Långt tankstreck"), + ("Em Space", "Helfyrkant"), + ("En Dash", "Tankstreck"), + ("En Space", "Halvfyrkant"), + ("Export EPUB", "Exportera EPUB"), + ("Export EPUB (Fixed Layout)", "Exportera EPUB (fast layout)"), + ("Export EPUB (Reflowable)", "Exportera EPUB (flytande)"), + ("Export HTML", "Exportera HTML"), + ("Export IDML", "Exportera IDML"), + ("Export PDF (Interactive)", "Exportera PDF (interaktiv)"), + ("Export Page as PNG", "Exportera sidan som PNG"), + ("Export Text", "Exportera text"), + ("Export XML", "Exportera XML"), + ("Figure Space", "Siffermellanrum"), + ("File/Export", "Arkiv/Exportera"), + ("Fill proportionally", "Fyll proportionerligt"), + ("Fit content to frame", "Anpassa innehåll till ram"), + ("Fit proportionally", "Anpassa proportionellt"), + ("Flush Space", "Utfyllnadsblanksteg"), + ("Forced Line Break", "Tvingad radbrytning"), + ("Frame Break", "Rambrytning"), + ("GREP", "GREP"), + ("Generate Index", "Generera register"), + ("Generate Live Caption", "Generera dynamisk bildtext"), + ("Global Light", "Globalt ljus"), + ("Graphic", "Grafik"), + ("Hair Space", "Hårfint blanksteg"), + ("High Contrast", "Hög kontrast"), + ("High Resolution", "Hög upplösning"), + ("I, II, III, IV...", "I, II, III, IV..."), + ("Import XML", "Importera XML"), + ("Indent to Here", "Indrag hit"), + ("Ink Limit 300%", "Total färgmängd 300 %"), + ("Insert Anchored Object", "Infoga förankrat objekt"), + ("Insert Cross-Reference", "Infoga korsreferens"), + ("Insert Math Expression", "Infoga matematiskt uttryck"), + ("Insert Pages", "Infoga sidor"), + ("Insert field", "Infoga fält"), + ("Internet Standard RGB (sRGB)", "Internetstandard-RGB (sRGB)"), + ("Join the ArtCraft Discord", "Gå med i ArtCraft på Discord"), + ("Layout/Index", "Layout/Register"), + ("Layout/Pages", "Layout/Sidor"), + ("Legacy Macintosh RGB", "Äldre Macintosh RGB"), + ("Light", "Ljus"), + ("Limit", "Gränsvärde"), + ("Link images", "Länka bilder"), + ("Load DTD", "Läs in DTD"), + ("Load Swatches", "Läs in färgrutor"), + ("Low Resolution", "Låg upplösning"), + ("Magenta", "Magenta"), + ("Margins and Columns", "Marginaler och spalter"), + ("Margins:", "Marginaler:"), + ("Medium Dark", "Mellanmörk"), + ("Medium Light", "Mellanljus"), + ("Medium Resolution", "Medelhög upplösning"), + ("Menus", "Menyer"), + ("Merge options", "Sammanfogningsalternativ"), + ("Monitor RGB", "Bildskärms-RGB"), + ("Move Pages", "Flytta sidor"), + ("Multiple records", "Flera poster"), + ("New Condition", "Nytt villkor"), + ("New Hyperlink", "Ny hyperlänk"), + ("New Hyperlink Destination", "Nytt hyperlänkmål"), + ("New Page Reference", "Ny sidreferens"), + ("New Parent", "Ny mallsida"), + ("New Stroke Style", "Ny konturstil"), + ("Next", "Nästa"), + ("Next Page Number", "Nästa sidnummer"), + ("No data source", "Ingen datakälla"), + ("Nonbreaking Hyphen", "Hårt bindestreck"), + ("Nonbreaking Space", "Hårt blanksteg"), + ("None (keep transparency)", "Ingen (behåll transparens)"), + ("Numbering & Section Options", "Numrerings- och avsnittsalternativ"), + ("Object/Anchored Object", "Objekt/Förankrat objekt"), + ("Object/Arrange", "Objekt/Ordna"), + ("Object/Captions", "Objekt/Bildtexter"), + ("Object/Clipping Path", "Objekt/Klippbana"), + ("Object/Content", "Objekt/Innehåll"), + ("Object/Convert Shape", "Objekt/Konvertera form"), + ("Object/Effects", "Objekt/Effekter"), + ("Object/Fitting", "Objekt/Anpassning"), + ("Object/Pathfinder", "Objekt/Banfinnare"), + ("Object/Paths", "Objekt/Banor"), + ("Object/Select", "Objekt/Markera"), + ("Object/Transform", "Objekt/Transformera"), + ("Object/Transform Again", "Objekt/Transformera igen"), + ("One record", "En post"), + ("Package", "Paketera"), + ("Page Break", "Sidbrytning"), + ("Paragraph Return", "Styckeretur"), + ("Paragraph Symbol", "Styckesymbol"), + ("Pass csv, rows, or bytes from the control channel.", "Skicka csv, rader eller byte från kontrollkanalen."), + ("Picas", "Pica"), + ("Place with Import Options", "Placera med importalternativ"), + ("Preflight", "Förhandskontroll"), + ("Previous", "Föregående"), + ("Previous Page Number", "Föregående sidnummer"), + ("Print Booklet", "Skriv ut häfte"), + ("Punctuation Space", "Blanksteg för skiljetecken"), + ("Quarter Space", "Fjärdedels blanksteg"), + ("Quick Apply", "Snabbanvänd"), + ("RGB:", "RGB:"), + ("Range", "Omfång"), + ("Record", "Post"), + ("Records per page", "Poster per sida"), + ("Registered Trademark Symbol", "Symbol för registrerat varumärke"), + ("Relink", "Länka om"), + ("Relink to Folder", "Länka om till mapp"), + ("Report an Issue", "Rapportera ett problem"), + ("Right Indent Tab", "Högerindragstabb"), + ("Row spacing", "Radavstånd"), + ("Rows first", "Rader först"), + ("Save As", "Spara som"), + ("Save Swatches for Exchange", "Spara färgrutor för utbyte"), + ("Save a Copy", "Spara en kopia"), + ("Scripts", "Skript"), + ("Section Marker", "Avsnittsmarkör"), + ("Section Symbol", "Paragraftecken"), + ("Select Data Source…", "Välj datakälla…"), + ("Sentence case", "Inledande versal"), + ("Simulate Paper Color", "Simulera pappersfärg"), + ("Single Left Quotation Mark", "Enkelt vänster citattecken"), + ("Single Right Quotation Mark", "Enkelt höger citattecken"), + ("Single record", "En post"), + ("Sixth Space", "Sjättedelsblanksteg"), + ("Slug:", "Informationsområde:"), + ("Smart Guide Options", "Alternativ för smarta stödlinjer"), + ("Step and Repeat", "Steg och upprepa"), + ("Story Column Direction", "Artikelns spaltriktning"), + ("Straight Double Quotation Marks", "Raka dubbla citattecken"), + ("Straight Single Quotation Mark (Apostrophe)", "Rakt enkelt citattecken (apostrof)"), + ("Subscript:", "Nedsänkt:"), + ("Superscript:", "Upphöjd:"), + ("Tab", "Tabb"), + ("Table of Contents", "Innehållsförteckning"), + ("Table/Convert Cell Type", "Tabell/Konvertera celltyp"), + ("Table/Delete", "Tabell/Radera"), + ("Table/Insert", "Tabell/Infoga"), + ("Table/Select", "Tabell/Markera"), + ("Thin Space", "Smalt mellanrum"), + ("Third Space", "Tredjedelsmellanrum"), + ("Title Case", "Inledande versaler"), + ("Trademark Symbol", "Varumärkessymbol"), + ("Transform Again Individually", "Transformera igen individuellt"), + ("Transform Sequence Again", "Transformera sekvens igen"), + ("Transform Sequence Again Individually", "Transformera sekvens igen individuellt"), + ("Type on a Path Options", "Alternativ för text på bana"), + ("Type/Bulleted and Numbered Lists", "Text/Punktlistor och numrerade listor"), + ("Type/Change Case", "Text/Ändra skiftläge"), + ("Type/Hyperlinks & Cross-References", "Text/Hyperlänkar och korsreferenser"), + ("Type/Insert Break Character", "Text/Infoga brytningstecken"), + ("Type/Insert Special Character/Hyphens and Dashes", "Text/Infoga specialtecken/Bindestreck och tankstreck"), + ("Type/Insert Special Character/Markers", "Text/Infoga specialtecken/Markörer"), + ("Type/Insert Special Character/Other", "Text/Infoga specialtecken/Annat"), + ("Type/Insert Special Character/Quotation Marks", "Text/Infoga specialtecken/Citattecken"), + ("Type/Insert Special Character/Symbols", "Text/Infoga specialtecken/Symboler"), + ("Type/Insert White Space", "Text/Infoga blanktecken"), + ("Type/Proof Setup", "Text/Provtrycksinställningar"), + ("Type/Rotate Spread", "Text/Rotera uppslag"), + ("Type/Separations Preview", "Text/Förhandsgranskning av separationer"), + ("Type/Story Direction", "Text/Artikelriktning"), + ("Type/Text Variables", "Text/Textvariabler"), + ("Type/Track Changes", "Text/Spåra ändringar"), + ("Type/Type on a Path", "Text/Text på bana"), + ("URL:", "URL:"), + ("Unassigned", "Ej tilldelad"), + ("View/Display Performance", "Visa/Visningsprestanda"), + ("View/Grids & Guides", "Visa/Rutnät och stödlinjer"), + ("Window/Arrange", "Fönster/Ordna"), + ("Window/Interactive", "Fönster/Interaktivt"), + ("Window/Interface Color Theme", "Fönster/Färgtema för gränssnittet"), + ("Window/Output", "Fönster/Utdata"), + ("Window/Panels", "Fönster/Paneler"), + ("Window/Type & Tables", "Fönster/Text och tabeller"), + ("Window/Utilities", "Fönster/Tillbehör"), + ("Working CMYK", "Arbets-CMYK"), + ("Yellow", "Gul"), + ("a, b, c, d...", "a, b, c, d..."), + ("i, ii, iii, iv...", "i, ii, iii, iv..."), + ("lowercase", "gemener"), + ("All pages", "Alla sidor"), + ("Between Entry and Number:", "Mellan post och nummer:"), + ("Both pages of the parent spread", "Båda sidorna i mallsidans uppslag"), + ("Both pages of the spread", "Båda sidorna i uppslaget"), + ("Close Panel", "Stäng panel"), + ("Data Fields", "Datafält"), + ("Data Merge Options", "Alternativ för datasammanfogning"), + ("Distribute Rows Evenly", "Fördela rader jämnt"), + ("Don't Save", "Spara inte"), + ("Drag Rows or Columns", "Dra rader eller kolumner"), + ("Entry Style:", "Formatmall för post:"), + ("Even Page Break", "Brytning till jämn sida"), + ("Export PowerPoint…", "Exportera PowerPoint…"), + ("Formatting Affects Text", "Formateringen påverkar texten"), + ("Georgian", "Georgiska"), + ("Group with", "Gruppera med"), + ("Import Warnings", "Importvarningar"), + ("Include Paragraph Styles:", "Inkludera styckestilar:"), + ("Insert Field", "Infoga fält"), + ("Level:", "Nivå:"), + ("List Composite Fonts", "Lista sammansatta typsnitt"), + ("Odd Page Break", "Brytning till udda sida"), + ("Optical Margin Alignment", "Optisk marginaljustering"), + ("Other Styles:", "Andra stilar:"), + ("Page Number:", "Sidnummer:"), + ("Pin Bar Position", "Fäst fältets position"), + ("Presentation", "Presentation"), + ("Preview Record", "Förhandsgranska post"), + ("Remove Data Source", "Ta bort datakälla"), + ("Remove Existing Ruler Guides", "Ta bort befintliga stödlinjer"), + ("Remove Field", "Ta bort fält"), + ("Remove »", "Ta bort »"), + ("Replace Existing Table of Contents", "Ersätt befintlig innehållsförteckning"), + ("Reset Bar Position", "Återställ fältets position"), + ("Resize panel window", "Ändra storlek på panelfönster"), + ("Resize panels", "Ändra storlek på paneler"), + ("Select an object to color its fill or stroke.", "Markera ett objekt för att färglägga dess fyllning eller kontur."), + ("Set Composite Font", "Ange komposittypsnitt"), + ("Stop Preview", "Stoppa förhandsgranskning"), + ("Styles in Table of Contents", "Stilar i innehållsförteckningen"), + ("This page", "Den här sidan"), + ("This page is", "Den här sidan är"), + ("Title:", "Titel:"), + ("Unsaved Changes", "Osparade ändringar"), + ("Update Data Source", "Uppdatera datakälla"), + ("Validate from Root Element", "Validera från rotelement"), + ("View/Extras", "Visa/Extrafunktioner"), + ("View/Proof Setup", "Visa/Provtrycksinställningar"), + ("View/Rotate Spread", "Visa/Rotera uppslag"), + ("View/Screen Mode", "Visa/Skärmläge"), + ("View/Separations Preview", "Visa/Förhandsgranskning av separationer"), + ("View/Structure", "Visa/Struktur"), + ("« Add", "« Lägg till"), + ( + "“{}” has changes that are not saved. Save them before closing?", + "”{}” har ändringar som inte har sparats. Vill du spara dem innan du stänger?", + ), + ("Inches Decimal", "Tum decimal"), + ("Back", "Tillbaka"), + ("Warning", "Varning"), + ("Don't show again", "Visa inte igen"), + ("The affected page contains objects. Delete the page anyway?", "Sidan innehåller objekt. Vill du radera sidan ändå?"), + ("The affected pages contain objects. Delete the pages anyway?", "Sidorna innehåller objekt. Vill du radera sidorna ändå?"), + ("Capture", "Inspelning"), + ("Panel Locations", "Panelplaceringar"), + ("Close Tab Group", "Stäng flikgrupp"), + ("Minimize", "Minimera"), + ("Collapse to Icons", "Komprimera till ikoner"), + ("Panel Options", "Panelalternativ"), + ("Panel Options…", "Panelalternativ…"), + ("Show Thumbnails", "Visa miniatyrer"), + ("Show Vertically", "Visa vertikalt"), + ("Panel Layout", "Panellayout"), + ("Pages on Top", "Sidor överst"), + ("Parents on Top", "Mallsidor överst"), + ("Resize:", "Ändra storlek:"), + ("Extra Small", "Extra liten"), + ("Small", "Liten"), + ("Medium", "Medel"), + ("Large", "Stor"), + ("Extra Large", "Extra stor"), + ("Jumbo", "Jumbo"), + ("Proportional", "Proportionell"), + ("Pages Fixed", "Fasta sidor"), + ("Parents Fixed", "Fasta mallsidor"), + ("Parents", "Mallsidor"), + ("Settings…", "Inställningar…"), + ("Left-aligned tab", "Vänsterjusterad tabb"), + ("Centered tab", "Centrerad tabb"), + ("Right-aligned tab", "Högerjusterad tabb"), + ("Tab aligned on a character", "Tabb justerad mot ett tecken"), + ("X:", "X:"), + ("Leader:", "Utfyllnadstecken:"), + ("Align On:", "Justera mot:"), + ("Position panel above text frame", "Placera panelen ovanför textramen"), + ("Clear All", "Rensa alla"), + ("Delete Tab", "Radera tabb"), + ("Repeat Tab", "Upprepa tabb"), + ("Reset Indents", "Återställ indrag"), + ("Redefine Style", "Omdefiniera stil"), + ("Select; drag to move the selection to another layer", "Markera; dra för att flytta markeringen till ett annat lager"), + ("Alt-click clears overrides", "Alt-klick rensar lokala ändringar"), + ("Apply \"{name}\", Clear Overrides", "Använd \"{name}\", rensa lokala ändringar"), + ("Rotate 180°", "Rotera 180°"), + ("No matching commands", "Inga matchande kommandon"), + ("Kanji", "Kanji"), + ("Show CJK Features", "Visa CJK-funktioner"), + ("Page Size:", "Sidstorlek:"), + ("Adjust page elements to document changes", "Anpassa sidelement efter dokumentändringar"), + ("Apply the changes; objects follow the new page size and margins", "Använd ändringarna; objekten följer den nya sidstorleken och marginalerna"), + ("Preflight Panel…", "Panelen Förhandskontroll…"), + ("Recent", "Senaste"), + ("Saved", "Sparat"), + ("[Default]", "[Standard]"), + ("Your Recent Items", "Dina senaste objekt"), + ("Saved Presets", "Sparade förinställningar"), + ("Blank Document Presets", "Förinställningar för tomma dokument"), + ("Delete Preset", "Radera förinställning"), + ("Preset Details", "Detaljer för förinställning"), + ("Document Name", "Dokumentnamn"), + ("Save Document Preset", "Spara dokumentförinställning"), + ("Save Document Preset As:", "Spara dokumentförinställning som:"), + ("Save Preset", "Spara förinställning"), + ("Start #", "Startnr"), + ("Column Gutter", "Spaltmellanrum"), + ("Start Section", "Börja avsnitt"), + ("Automatic Page Numbering", "Automatisk sidnumrering"), + ("Start Page Numbering at:", "Börja sidnumrering vid:"), + ("Section Prefix:", "Avsnittsprefix:"), + ("Section Marker:", "Avsnittsmarkör:"), + ("Include Prefix when Numbering Pages", "Inkludera prefix vid sidnumrering"), + ("Document Chapter Numbering", "Kapitelnumrering för dokument"), + ("Automatic Chapter Numbering", "Automatisk kapitelnumrering"), + ("Start Chapter Numbering at:", "Börja kapitelnumrering vid:"), + ("Same as Previous Document in the Book", "Samma som föregående dokument i boken"), + ("Book Name:", "Boknamn:"), + ("N/A", "Saknas"), + ("Preset Name", "Namn på förinställning"), + ("Bounding Box (Visible Layers Only)", "Avgränsningsram (endast synliga lager)"), + ("Bounding Box (All Layers)", "Avgränsningsram (alla lager)"), + ("Origin:", "Origo:"), + ("Spine", "Rygg"), + ("Lock Zero Point", "Lås nollpunkt"), + ("Unlock Zero Point", "Lås upp nollpunkt"), + ("All", "Alla"), + ("All Lines in Paragraph", "Alla rader i stycket"), + ("Anywhere", "Var som helst"), + ("At Start/End of Paragraph", "Vid styckets början/slut"), + ("Bullet Character:", "Punkttecken:"), + ("Bullets", "Punktlista"), + ("Continue from Previous Number", "Fortsätt från föregående nummer"), + ("End:", "Slut:"), + ("In Next Column", "I nästa spalt"), + ("In Next Frame", "I nästa ram"), + ("Inside Gutter:", "Inre spaltmellanrum:"), + ("Keep Lines Together", "Håll ihop raderna"), + ("Keep Options", "Alternativ för att hålla ihop"), + ("Keep with Next:", "Håll ihop med nästa:"), + ("List Type:", "Listtyp:"), + ("List:", "Lista:"), + ("Mode:", "Läge:"), + ("Numbers", "Siffror"), + ("On Next Even Page", "På nästa jämna sida"), + ("On Next Odd Page", "På nästa udda sida"), + ("On Next Page", "På nästa sida"), + ("Outside Gutter:", "Yttre spaltmellanrum:"), + ("Paragraph Border", "Styckekantlinje"), + ("Paragraph Layout:", "Styckelayout:"), + ("Paragraph Shading", "Styckeskuggning"), + ("Single Column", "Enkel spalt"), + ("Span Columns", "Sträck över spalter"), + ("Split Column", "Dela spalt"), + ("Start At", "Börja vid"), + ("Start Paragraph:", "Första stycke:"), + ("Text After:", "Text efter:"), + ("(Paragraph Font)", "(Styckets typsnitt)"), + ("Any Previous Level", "Valfri föregående nivå"), + ("Bottom Edge:", "Nederkant:"), + ("Bottom Left:", "Nere till vänster:"), + ("Bottom Right:", "Nere till höger:"), + ("Bullets and Numbering…", "Punkter och numrering…"), + ("Clip to Frame", "Beskär till ram"), + ("Corner Size and Shape", "Hörnstorlek och form"), + ("Descent", "Underlängd"), + ("Display Border if Paragraph Splits Across Frames/Columns", "Visa kant om stycket delas mellan ramar/spalter"), + ("Do not Print or Export", "Skriv inte ut eller exportera"), + ("Em Box Bottom", "Em-ruta nederkant"), + ("Em Box Top", "Em-ruta överkant"), + ("Fancy", "Dekorativ"), + ("Gap Tint:", "Färgton för mellanrum:"), + ("Strokes and Fills…", "Konturer och fyllningar…"), + ("Cell Stroke", "Cellkontur"), + ("Cell Fill", "Cellfyllning"), + ("Cell edges", "Cellkanter"), + ( + "Click a line to include or exclude it. Double-click selects all outer or all inner lines; triple-click selects or clears all.", + "Klicka på en linje för att ta med eller utesluta den. Dubbelklicka för att markera alla yttre eller alla inre linjer; trippelklicka för att markera eller rensa alla.", + ), + ("Glyphs…", "Glyfer…"), + ("Inset", "Infällning"), + ("Inverse Rounded", "Omvänt rundad"), + ("Merge Consecutive Borders and Shading", "Sammanfoga intilliggande kantlinjer och skuggning"), + ("Paragraph Borders and Shading", "Kantlinjer och skuggning för stycke"), + ("Paragraph Borders and Shading…", "Kantlinjer och skuggning för stycke…"), + ("Restart Numbers at This Level After:", "Börja om numreringen på den här nivån efter:"), + ("Rounded", "Rundad"), + ("Space After Span:", "Avstånd efter sträckning:"), + ("Space After Split:", "Avstånd efter delning:"), + ("Space Before Span:", "Avstånd före sträckning:"), + ("Space Before Split:", "Avstånd före delning:"), + ("Span Columns…", "Sträck över spalter…"), + ("Tab Position:", "Tabbposition:"), + ("Top Edge:", "Överkant:"), + ("Top Left:", "Uppe till vänster:"), + ("Top Right:", "Uppe till höger:"), + ("Absorb Ideographic Space at Line End", "Absorbera ideografiskt blanksteg vid radslut"), + ("Align Only First Line to Grid", "Justera endast första raden mot rutnätet"), + ("Black Circle", "Svart cirkel"), + ("Black Triangle", "Svart triangel"), + ("Bullseye", "Måltavla"), + ("Bunri-Kinshi", "Bunri-kinshi"), + ("Character:", "Tecken:"), + ("Characters After Break:", "Tecken efter brytning:"), + ("Characters Before Break:", "Tecken före brytning:"), + ("Default Rules", "Standardregler"), + ("Double Circle", "Dubbel cirkel"), + ("Em Box Bottom/Left", "Em-ruta nederkant/vänster"), + ("Em Box Center", "Em-ruta mitt"), + ("Em Box Center (Down)", "Em-ruta mitt (ned)"), + ("Em Box Top/Right", "Em-ruta överkant/höger"), + ("Force", "Tvinga"), + ("Grid Alignment:", "Rutnätsjustering:"), + ("Grid Settings", "Rutnätsinställningar"), + ("Hanging Type:", "Hängningstyp:"), + ("Hard Kinsoku", "Hård kinsoku"), + ("Japanese Composition Settings", "Inställningar för japansk komposition"), + ("Kenten Settings", "Kenten-inställningar"), + ("Kenten Type:", "Kenten-typ:"), + ("Kinsoku Set:", "Kinsoku-uppsättning:"), + ("Kinsoku Type:", "Kinsoku-typ:"), + ("Korean Kinsoku", "Koreansk kinsoku"), + ("Leading Model:", "Radavståndsmodell:"), + ("Left/Right Position:", "Vänster/höger-position:"), + ("Line Break Options", "Alternativ för radbrytning"), + ("Line Spacing:", "Radavstånd:"), + ("Mojikumi Set:", "Mojikumi-uppsättning:"), + ("Prioritize Adjustment Amount", "Prioritera justeringsmängd"), + ("Push In First", "Skjut in först"), + ("Push Out First", "Skjut ut först"), + ("Push Out Only", "Skjut endast ut"), + ("Rensuuji", "Rensuuji"), + ("Roman Baseline", "Baslinje (roman)"), + ("Sesame Dot", "Sesampunkt"), + ("Simplified Chinese Kinsoku", "Kinsoku för förenklad kinesiska"), + ("Small Black Circle", "Liten svart cirkel"), + ("Small White Circle", "Liten vit cirkel"), + ("Soft Kinsoku", "Mjuk kinsoku"), + ("Tate-chu-yoko Settings", "Inställningar för tate-chu-yoko"), + ("Traditional Chinese Kinsoku", "Kinsoku för traditionell kinesiska"), + ("Up/Down Position:", "Position upp/ned:"), + ("Warichu Settings", "Warichu-inställningar"), + ("Warichu Size:", "Warichu-storlek:"), + ("White Circle", "Vit cirkel"), + ("White Sesame Dot", "Vit sesampunkt"), + ("White Triangle", "Vit triangel"), + ("1 Parent Character", "1 bastecken"), + ("1 Ruby Character", "1 ruby-tecken"), + ("1 Ruby Character Aki", "1 ruby-tecken aki"), + ("1-2-1 (JIS) Rule", "1-2-1-regel (JIS)"), + ("1-2-1 Aki", "1-2-1 aki"), + ("1/2 Parent Character", "1/2 bastecken"), + ("1/2 Ruby Character", "1/2 ruby-tecken"), + ("Above/Right", "Ovanför/höger"), + ("Across the Line:", "Tvärs över raden:"), + ("Adjust Rotation", "Justera rotation"), + ("Adjust Tsume", "Justera tsume"), + ("Adjustment When Ruby Is Longer Than Parent", "Justering när ruby är längre än bastexten"), + ("Along the Line:", "Längs raden:"), + ("Auto Align at Line Start/End", "Automatisk justering vid radbörjan/radslut"), + ("Auto Tate-chu-yoko Settings", "Inställningar för automatisk tate-chu-yoko"), + ("Below/Left", "Nedanför/vänster"), + ("Both Sides", "Båda sidor"), + ("Centered", "Centrerad"), + ("Distance from Parent:", "Avstånd från överordnad:"), + ("Equal Aki", "Lika aki"), + ("Fisheye", "Fisköga"), + ("Fit to Width", "Anpassa till bredd"), + ("Full Justify", "Full marginaljustering"), + ("Group Ruby", "Grupp-ruby"), + ("Gyoudori:", "Gyoudori:"), + ("ICF Bottom", "ICF nederkant"), + ("ICF Top", "ICF överkant"), + ("Include Roman Characters", "Inkludera romerska tecken"), + ("Kenten Color", "Kenten-färg"), + ("Left/Top", "Vänster/överkant"), + ("Magnification:", "Förstoring:"), + ("Minimum Scale:", "Minsta skala:"), + ("No Adjustment", "Ingen justering"), + ("Offset from Parent", "Förskjutning från bastext"), + ("Parent Character Spacing:", "Teckenavstånd för bastext:"), + ("Parent's Font", "Bastextens typsnitt"), + ("Per-Character Ruby", "Ruby per tecken"), + ("Right/Bottom", "Höger/nederkant"), + ("Roman Word Break", "Ordbrytning (roman)"), + ("Rotate Roman Characters in Vertical Text", "Rotera romerska tecken i lodrät text"), + ("Ruby Color", "Rubyfärg"), + ("Ruby Font and Size", "Rubytypsnitt och storlek"), + ("Ruby Overhang:", "Ruby-överhäng:"), + ("Ruby Placement and Spacing", "Rubyplacering och avstånd"), + ("Ruby Type:", "Rubytyp:"), + ("Shatai", "Shatai"), + ("Shrink Ruby Width Automatically", "Krymp rubybredd automatiskt"), + ("Tate-chu-yoko Digits:", "Tate-chu-yoko-siffror:"), + ("Text Color", "Textfärg"), + ("Unlimited", "Obegränsat"), + ("Use OpenType Pro Ruby Glyphs", "Använd OpenType Pro-rubyglyfer"), + ("Use Paragraph Gyoudori", "Använd styckets gyoudori"), + ("Swatch Options…", "Färgrutealternativ…"), + ("Swatch Options", "Färgrutealternativ"), + ("Color Group Options…", "Alternativ för färggrupp…"), + ("Layer Options", "Lageralternativ"), + ("Layer Options for \"{name}\"…", "Lageralternativ för \"{name}\"…"), + ("Swatch Name:", "Färgrutenamn:"), + ("Name with Color Value", "Namn med färgvärde"), + ("Color Type:", "Färgtyp:"), + ("Process", "Process"), + ("Spot", "Dekor"), + ("Color Mode:", "Färgläge:"), + ("Base Color:", "Basfärg:"), + ("Edit the gradient's stops in the Gradient panel.", "Redigera gradientens stopp i panelen Gradient."), + ("This swatch can't be edited.", "Den här färgrutan kan inte redigeras."), + ("Light Blue", "Ljusblå"), + ("Red", "Röd"), + ("Green", "Grön"), + ("Blue", "Blå"), + ("Orange", "Orange"), + ("Dark Green", "Mörkgrön"), + ("Teal", "Blågrön"), + ("Tan", "Sandbrun"), + ("Brown", "Brun"), + ("Violet", "Violett"), + ("Gold", "Guld"), + ("Show Layer", "Visa lager"), + ("Lock Layer", "Lås lager"), + ("Print Layer", "Skriv ut lager"), + ("Suppress Text Wrap When Layer is Hidden", "Undertryck textombrytning när lagret är dolt"), + ("Text Anchor", "Textankare"), + ("Open Recent", "Öppna senaste"), + ("Open Recent File", "Öppna senaste fil"), + ("Clear Recent Files", "Rensa senaste filer"), + ("Recent files", "Senaste filer"), + ("Number of Recent Items to Display:", "Antal senaste objekt att visa:"), + ("Paste Vector Art", "Klistra in vektorgrafik"), + ("Baseline Options", "Baslinjealternativ"), + ("Fixed Number", "Fast antal"), + ("Fixed Width", "Fast bredd"), + ("Inset Spacing", "Infällningsavstånd"), + ("Vertical Justification", "Vertikal justering"), + ("Align:", "Justera:"), + ("Ignore Text Wrap", "Ignorera textombrytning"), + ("Technique:", "Teknik:"), + ("Softer", "Mjukare"), + ("Precise", "Exakt"), + ("Corners:", "Hörn:"), + ("Sharp", "Skarp"), + ("Diffused", "Diffus"), + ("Effects…", "Effekter…"), + ("Settings for:", "Inställningar för:"), + ("Transparency…", "Transparens…"), + ("Basic Blending", "Grundläggande blandning"), + ("Opacity:", "Opacitet:"), + ("Blending", "Blandning"), + ("Distance:", "Avstånd:"), + ("X Offset:", "X-förskjutning:"), + ("Y Offset:", "Y-förskjutning:"), + ("Spread:", "Spridning:"), + ("Choke:", "Krympning:"), + ("Source:", "Källa:"), + ("Edge", "Kant"), + ("Depth:", "Djup:"), + ("Highlight:", "Högdager:"), + ("Shadow:", "Skugga:"), + ("Feather Width:", "Fjädringsbredd:"), + ("Feather Widths", "Fjädringsbredder"), + ("Gradient Stops", "Gradientstopp"), + ("Start Opacity:", "Startopacitet:"), + ("End Opacity:", "Slutopacitet:"), + ("(no effects)", "(inga effekter)"), + ("OBJECT", "OBJEKT"), + ("Add an object effect to the selected target", "Lägg till en objekteffekt på det markerade målet"), + ("Load Paragraph Styles…", "Läs in styckestilar…"), + ("Load Character Styles…", "Läs in teckenstilar…"), + ("Load All Text Styles…", "Läs in alla textstilar…"), + ("Load Styles", "Läs in stilar"), + ("Differs from this document", "Skiljer sig från det här dokumentet"), + ("Redefine Document Style", "Omdefiniera dokumentstil"), + ("Use Document Style Definition", "Använd dokumentets formatdefinition"), + ("Auto Rename", "Byt namn automatiskt"), + ("Toggle Presentation", "Visa/dölj presentation"), + ("Balance Columns", "Balansera spalter"), + ("Digits:", "Siffror:"), + ("Justification…", "Justering…"), + ("Apply To:", "Använd på:"), + ("Frame", "Ram"), + ("Filters:", "Filter:"), + ("All Classes", "Alla klasser"), + ("Serif", "Serif"), + ("Slab Serif", "Slab serif"), + ("Sans Serif", "Sans serif"), + ("Script", "Skrivstil"), + ("Monospaced", "Fast teckenbredd"), + ("Decorative", "Dekorativ"), + ("Show Recently Added", "Visa nyligen tillagda"), + ("Small Sample Text Size", "Liten exempeltextstorlek"), + ("Medium Sample Text Size", "Medelstor exempeltext"), + ("Large Sample Text Size", "Stor storlek på exempeltext"), + ("Selected Text", "Markerad text"), + ("Sample", "Exempel"), + ("Similar to {family}", "Likartade {family}"), + ("No fonts match", "Inga typsnitt matchar"), + ("Show Similar Fonts", "Visa likartade typsnitt"), + ("Loading fonts…", "Läser in typsnitt…"), + ("Show Styles", "Visa stilar"), + ("Hide Styles", "Dölj stilar"), + ("TrueType font", "TrueType-typsnitt"), + ("OpenType font", "OpenType-typsnitt"), + ("Variable font", "Variabelt typsnitt"), + ("Color font", "Färgtypsnitt"), + ("Sort Entries in Alphabetical Order", "Sortera poster i bokstavsordning"), + ("% of all commits", "% av alla commits"), + ("(any)", "(valfri)"), + ("(unchanged)", "(oförändrad)"), + ("[No Language]", "[Inget språk]"), + ("About", "Om"), + ("Activate Panel", "Aktivera panel"), + ("Add Anchor Point Tool", "Verktyget Lägg till ankarpunkt"), + ("Align To", "Justera mot"), + ("Alternate layout", "Alternativ layout"), + ("Binary assets added", "Binära resurser tillagda"), + ("Binary assets removed", "Binära resurser borttagna"), + ("Cell Number:", "Cellnummer:"), + ("Change fill to:", "Ändra fyllning till:"), + ("Change Panel Layout", "Ändra panellayout"), + ("Chinese: Simplified", "Kinesiska: förenklad"), + ("Chinese: Traditional", "Kinesiska: traditionell"), + ("Color Theme Tool", "Färgtemaverktyg"), + ("Commits", "Commits"), + ("Company", "Företag"), + ("Content Collector Tool", "Innehållsinsamlingsverktyg"), + ("Content Placer Tool", "Innehållsplaceringsverktyg"), + ("Contributors", "Bidragsgivare"), + ("Convert Rows", "Konvertera rader"), + ("Corner Size", "Hörnstorlek"), + ("Couldn't write recovery data: {error}", "Kunde inte skriva återställningsdata: {error}"), + ("Delete Anchor Point Tool", "Verktyg för att radera ankarpunkt"), + ("DesignCraft page", "DesignCraft-sida"), + ("Direct Selection Tool", "Direktmarkeringsverktyg"), + ("Display name", "Visningsnamn"), + ("Ellipse Frame Tool", "Ellipsramverktyg"), + ("Ellipse Tool", "Ellipsverktyg"), + ("Email Address:", "E-postadress:"), + ( + "Equal fixed heights for the selected rows; cell text may become overset.", + "Lika fasta höjder för de markerade raderna; celltexten kan bli överskjutande.", + ), + ("Erase Tool", "Raderingsverktyg"), + ( + "Export a PDF, IDML or package to share — no cloud account needed.", + "Exportera en PDF, IDML eller ett paket för att dela – inget molnkonto behövs.", + ), + ("Eyedropper Tool", "Pipettverktyg"), + ("Find fill:", "Sök fyllning:"), + ("First commit", "Första commit"), + ("Found", "Hittade"), + ("Free Transform Tool", "Verktyg för fri transformering"), + ("Gap Tool", "Mellanrumsverktyg"), + ("Grab bag", "Blandat"), + ("Gradient Feather Tool", "Verktyg för gradientfjädring"), + ("Gradient Swatch Tool", "Verktyg för gradientfärgruta"), + ("Hand Tool", "Handverktyg"), + ("image editing", "bildredigering"), + ("Import cancelled: the originating document has closed", "Importen avbröts: ursprungsdokumentet har stängts"), + ("Invalid panel layout; restored the default dock", "Ogiltig panellayout; standarddockningen har återställts"), + ("Key Object", "Nyckelobjekt"), + ("Last commit", "Senaste commit"), + ("layer", "lager"), + ("layers", "lager"), + ("Line delta", "Raddifferens"), + ("Line Tool", "Linjeverktyg"), + ("Lines +/−", "Rader +/−"), + ("Lines added", "Tillagda rader"), + ("Lines deleted", "Borttagna rader"), + ("Measure Tool", "Verktyget Mät"), + ("Merged PRs", "Sammanslagna PR:er"), + ("Message:", "Meddelande:"), + ("Models", "Modeller"), + ("motion graphics and visual effects", "rörlig grafik och visuella effekter"), + ("Move Panel", "Flytta panel"), + ("Name (A–Z)", "Namn (A–Ö)"), + ("No contributor data was built into this copy.", "Inga uppgifter om bidragsgivare finns inbyggda i den här kopian."), + ("No matches", "Inga träffar"), + ("No model credits were built into this copy.", "Inga modellkrediteringar finns inbyggda i den här kopian."), + ("Note Tool", "Anteckningsverktyg"), + ("Organization:", "Organisation:"), + ("Packaged {count} files into {dir}", "Paketerade {count} filer i {dir}"), + ("Page Tool", "Sidverktyg"), + ("PDF import cancelled: the originating document has closed", "PDF-importen avbröts: ursprungsdokumentet har stängts"), + ("Pen Tool", "Pennverktyg"), + ("Pencil Tool", "Blyertsverktyg"), + ("Phone:", "Telefon:"), + ("photo library and raw development", "fotobibliotek och raw-framkallning"), + ("Polygon Frame Tool", "Polygonramverktyg"), + ("Polygon Tool", "Polygonverktyg"), + ("Press keys…", "Tryck på tangenter…"), + ("Range:", "Omfång:"), + ("reading, organizing and protecting PDFs", "läsa, ordna och skydda PDF:er"), + ("Real name", "Riktigt namn"), + ("Rectangle Frame Tool", "Rektangelramverktyg"), + ("Rectangle Tool", "Rektangelverktyg"), + ("Relinked {count}", "Omlänkade {count}"), + ("Reverse the order", "Vänd på ordningen"), + ("Rotate Tool", "Rotationsverktyg"), + ("Scale Tool", "Skalningsverktyg"), + ("Scissors Tool", "Saxverktyg"), + ("Selection Tool", "Markeringsverktyg"), + ("Sent {count} page(s) to {printer}", "Skickade {count} sida/sidor till {printer}"), + ("Shear Tool", "Skjuvningsverktyg"), + ("Show/Hide Document Grid", "Visa/dölj dokumentrutnät"), + ("Smooth Tool", "Utjämningsverktyg"), + ("Story Editor — {count} words", "Artikelredigerare – {count} ord"), + ("Story {story} ← story {parent}", "Artikel {story} ← artikel {parent}"), + ("Stroke Weight", "Konturtjocklek"), + ("Subject:", "Ämne:"), + ("Symbol", "Symbol"), + ("Text:", "Text:"), + ("the default printer", "standardskrivaren"), + ("To Body", "Till brödtext"), + ("To Footer", "Till sidfot"), + ("To Header", "Till sidhuvud"), + ("Type on a Path Tool", "Verktyg för text på bana"), + ("Type Tool", "Textverktyg"), + ("Username", "Användarnamn"), + ("vector illustration", "vektorillustration"), + ("Vertical Type on a Path Tool", "Verktyg för vertikal text på bana"), + ("Vertical Type Tool", "Vertikalt textverktyg"), + ("video editing, color and sound", "videoredigering, färg och ljud"), + ("Zoom Tool", "Zoomverktyg"), + ("{contributors} contributors · {commits} commits", "Bidragsgivare: {contributors} · Commits: {commits}"), + ("{count} conflict(s): {names}", "Konflikter ({count}): {names}"), + ("{count} match(es)", "Träffar: {count}"), + ("{count} replacement(s) made", "Antal ersättningar: {count}"), + ("{label}: {name} (double-click: Color Picker)", "{label}: {name} (dubbelklicka för färgväljare)"), + ( + "{name}: import cancelled while a dialog is open; choose the file again", + "{name}: importen avbröts eftersom en dialogruta är öppen; välj filen igen", + ), + ("{name}: {count} step(s) done", "{name}: klart med {count} steg"), + ("Add Tab", "Lägg till tabbstopp"), + ("Auto Tate-Chu-Yoko", "Automatisk tate-chu-yoko"), + ("Break Thread", "Bryt textlänk"), + ("Cancel Preview", "Avbryt förhandsgranskning"), + ("Change Tab", "Ändra tabbstopp"), + ("Character Style", "Teckenstil"), + ("Clear All Tabs", "Rensa alla tabbstopp"), + ("Clear Object Style Overrides", "Rensa lokala ändringar av objektstil"), + ("Clipping Path Options…", "Alternativ för klippbana…"), + ("Close Window", "Stäng fönster"), + ("Color Theme…", "Färgtema…"), + ("Commit Preview", "Godkänn förhandsgranskning"), + ("Convert Direction Point Tool", "Verktyget Konvertera riktningspunkt"), + ("Delete Character Style", "Radera teckenstil"), + ("Delete Document Preset", "Radera dokumentförinställning"), + ("Delete Parent", "Radera mallsida"), + ("Deselect Guides", "Avmarkera stödlinjer"), + ("Email:", "E-post:"), + ("Extract Color Theme", "Extrahera färgtema"), + ("Get Cell Stroke", "Hämta cellkontur"), + ("Get Tabs", "Hämta tabbstopp"), + ("Import InDesign Shortcut Set", "Importera kortkommandon från InDesign"), + ("Join", "Foga samman"), + ("Kerning/Tracking Step", "Steg för kerning/spärrning"), + ("List Styles in File", "Lista stilar i filen"), + ("Maximize/Restore Window", "Maximera/återställ fönster"), + ("Minimize Window", "Minimera fönster"), + ("Move Column Guide", "Flytta spaltstödlinje"), + ("Move Tab", "Flytta tabbstopp"), + ("Move Window", "Flytta fönster"), + ("New Cell Style", "Ny cellstil"), + ("New Table Style", "Ny tabellstil"), + ("Object Style", "Objektstil"), + ("Object Style Options…", "Alternativ för objektstil…"), + ("PRs", "PR:er"), + ("Page Options", "Sidalternativ"), + ("Paragraph Style", "Styckestil"), + ("Parent Options…", "Alternativ för mallsida…"), + ("Preview Change", "Förhandsgranska ändring"), + ("Redefine Character Style", "Definiera om teckenstil"), + ("Redefine Paragraph Style", "Definiera om styckestil"), + ("Reload Fonts", "Läs in typsnitt igen"), + ("Remove Frame from Thread", "Ta bort ram från länkningen"), + ("Reorder Objects", "Ändra ordning på objekt"), + ("Report an issue", "Rapportera ett problem"), + ("Resize Row or Column", "Ändra storlek på rad eller kolumn"), + ("Resize Window", "Ändra storlek på fönster"), + ("Ruler Zero Point", "Linjalens nollpunkt"), + ("Select Guide", "Markera stödlinje"), + ("Select Page", "Markera sida"), + ("Select Paragraph", "Markera stycke"), + ("Select Sentence", "Markera mening"), + ("Select Story", "Markera artikel"), + ("Select Text Unit", "Markera textenhet"), + ("Set OpenType Features", "Ange OpenType-funktioner"), + ("Swap Text Fill and Stroke", "Byt plats på textfyllning och kontur"), + ("Thread Text Frames", "Länka textramar"), + ("Select All Guides", "Markera alla stödlinjer"), + ("Ruler Guide", "Linjalstödlinje"), + ("Lock Column Guides", "Lås spaltstödlinjer"), + ("Character Formatting Controls", "Kontroller för teckenformatering"), + ("Paragraph Formatting Controls", "Kontroller för styckeformatering"), + ("Font Size", "Teckenstorlek"), + ("Vertical Scale", "Vertikal skala"), + ("Horizontal Scale", "Horisontell skala"), + ("Baseline Shift", "Baslinjeförskjutning"), + ("Skew (False Italic)", "Skevning (falsk kursiv)"), + ("Left Indent", "Vänsterindrag"), + ("Right Indent", "Högerindrag"), + ("First Line Left Indent", "Vänsterindrag för första raden"), + ("Last Line Right Indent", "Högerindrag för sista raden"), + ("Drop Cap Number of Lines", "Antal rader för anfang"), + ("Drop Cap One or More Characters", "Antal tecken för anfang"), + ("Align to Baseline Grid", "Justera mot baslinjerutnät"), + ("Go Back", "Gå bakåt"), + ("Go Forward", "Gå framåt"), + ("Restart Numbering", "Starta om numrering"), + ("Continue Numbering", "Fortsätt numrering"), + ("Keep Options…", "Alternativ för att hålla ihop…"), + ("Space Between Paragraphs Using Same Style", "Avstånd mellan stycken med samma format"), + ("Ignore", "Ignorera"), + ("Do Not Align to Baseline Grid", "Justera inte mot baslinjerutnät"), + ("Only Align First Line to Grid", "Justera endast första raden mot rutnätet"), + ("Balance Ragged Lines", "Balansera ojämna rader"), + ("Drop Caps and Nested Styles…", "Anfanger och kapslade stilar…"), + ("GREP Styles…", "GREP-format…"), + ("Restart/Continue Numbering", "Starta om/fortsätt numrering"), + ("Convert Bullets and Numbering to Text", "Konvertera punkter och numrering till text"), + ("Hyphenation…", "Avstavning…"), + ("Hyphenation Settings", "Avstavningsinställningar"), + ("hyphens", "bindestreck"), + ("Hyphenation Zone:", "Avstavningszon:"), + ("Better Spacing", "Bättre avstånd"), + ("Fewer Hyphens", "Färre bindestreck"), + ("Hyphenate Capitalized Words", "Avstava ord med inledande versal"), + ("Hyphenate Last Word", "Avstava sista ordet"), + ("Hyphenate Across Column", "Avstava mellan spalter"), + ("Convert to Note", "Konvertera till anteckning"), + ("Apply Bullets", "Använd punktlista"), + ("Apply Numbers", "Använd numrering"), + ("Convert Variable to Text", "Konvertera variabel till text"), + ("Next Note", "Nästa anteckning"), + ("Previous Note", "Föregående anteckning"), + ("Optical Margin Size", "Storlek för optisk marginal"), + ("Dismiss", "Stäng"), + ("Show/Hide Depth Ruler", "Visa/dölj djuplinjal"), + ("Split Note", "Dela anteckning"), + ("Arrangement", "Arrangemang"), + ("All Inks", "Alla tryckfärger"), + ("Apply Numbering & Section Options", "Använd numrerings- och avsnittsalternativ"), + ("Apply Text Attributes (Eyedropper)", "Använd textattribut (pipett)"), + ("Convert Point", "Konvertera punkt"), + ("Load Styles…", "Läs in stilar…"), + ("Recent Files", "Senaste filer"), + ("Apply to:", "Använd på:"), + ("Auto-Size", "Automatisk storlek"), + ("Create Links When Placing Text Files", "Skapa länkar när textfiler placeras"), + ("Kerning/Tracking:", "Kerning/spärrning:"), + ("Replace All", "Ersätt alla"), + ( + "Replace All updates this font throughout text and styles. Done keeps remaining fallback fonts.", + "Ersätt alla uppdaterar det här typsnittet i all text och alla stilar. Klart behåller återstående reservtypsnitt.", + ), + ("Replace with:", "Ersätt med:"), + ("Stroke Behind Fill", "Kontur bakom fyllning"), + ("Delete the character style \"{name}\"?", "Radera teckenstilen ”{name}”?"), + ("Delete the paragraph style \"{name}\"?", "Radera styckestilen ”{name}”?"), + ("Currently Rendered With", "Återges för närvarande med"), + ( + "“{}” has been edited in this layout. Updating replaces those edits with the text from the file.", + "”{}” har ändrats i den här layouten. Om du uppdaterar ersätts ändringarna med texten från filen.", + ), + ("Proportional Metrics", "Proportionella mått"), + ("Horizontal Kana", "Horisontell kana"), + ("Localized Forms", "Lokaliserade former"), + ("Mark Positioning", "Placering av diakritiska tecken"), + ("Historical Forms", "Historiska former"), + ("Stylistic Alternates", "Stilistiska alternativ"), + ("Justification Alternates", "Alternativa justeringstecken"), + ("Roman Italics", "Roman kursiv"), + ("Letter", "Letter"), + ("Legal", "Legal"), + ("Tabloid", "Tabloid"), + ("Letter - Half", "Letter - halv"), + ("Postcard", "Vykort"), + ("Web 1920 × 1080", "Webb 1920 × 1080"), + ("Web 1366 × 768", "Webb 1366 × 768"), + ("Web 1024 × 768", "Webb 1024 × 768"), + ("Mobile 1080 × 1920", "Mobil 1080 × 1920"), + ("Mobile 390 × 844", "Mobil 390 × 844"), + ("Tablet 1024 × 1366", "Surfplatta 1024 × 1366"), + ("Action", "Åtgärd"), + ("Advance seconds", "Sekunder till nästa"), + ("Affix in", "Fäst i"), + ("Allow", "Tillåt"), + ("Anchor", "Ankare"), + ("Artifact", "Artefakt"), + ("Asset", "Resurs"), + ("Assets", "Resurser"), + ("At end", "I slutet"), + ("Attrs", "Attribut"), + ("Author", "Författare"), + ("Auto fit", "Anpassa automatiskt"), + ("Auto size", "Automatisk storlek"), + ("Auto size reference", "Referenspunkt för automatisk storlek"), + ("Axis", "Axel"), + ("Bands", "Band"), + ("Bookmarks panel", "Panelen Bokmärken"), + ("Case", "Skiftläge"), + ("Columns kind", "Spalttyp"), + ("Continue across stories", "Fortsätt över artiklar"), + ("Cover image", "Omslagsbild"), + ("Definition", "Definition"), + ("Dir", "Riktning"), + ("Dry run", "Testkörning"), + ("Embed fonts", "Bädda in typsnitt"), + ("End", "End"), + ("Extension", "Förlängning"), + ("Family", "Familj"), + ("First baseline min", "Min. första baslinje"), + ("Fit to", "Anpassa till"), + ("Fixed layout", "Fast layout"), + ("Flatten", "Platta till"), + ("Font size", "Teckenstorlek"), + ("From", "Från"), + ("Full screen", "Helskärm"), + ("Grep", "GREP"), + ("Heading", "Rubrik"), + ("Heading style", "Rubrikstil"), + ("Hidden", "Dold"), + ("Identifier", "Identifierare"), + ("Idml", "IDML"), + ("Ids", "Id:n"), + ("Include inside edges", "Ta med inre kanter"), + ("Include prefix", "Ta med prefix"), + ("Instructions", "Instruktioner"), + ("Keep within column", "Håll inom spalten"), + ("Kind", "Typ"), + ("Label", "Etikett"), + ("Latex", "Latex"), + ("Layout page", "Layoutsida"), + ("Line breaks", "Radbrytningar"), + ("Load cursor", "Laddad markör"), + ("Marks", "Märken"), + ("Minimum height", "Minsta höjd"), + ("Minimum width", "Minsta bredd"), + ("Missing only", "Endast saknade"), + ("Mode", "Läge"), + ("Object point", "Objektpunkt"), + ("Org", "Origo"), + ("Page break before", "Sidbrytning före"), + ("Page layout", "Sidlayout"), + ("Para style", "Styckeformat"), + ("Pattern", "Mönster"), + ("Pdf", "PDF"), + ("Pdf crop", "PDF-beskärning"), + ("Pdf page", "PDF-sida"), + ("Pos", "Pos"), + ("Primary text frame", "Primär textram"), + ("Rasterize", "Rastrera"), + ("Rect", "Rekt"), + ("Redefine styles", "Omdefiniera stilar"), + ("Ref", "Ref"), + ("Ref char style", "Ref. teckenformat"), + ("Ref position", "Ref. position"), + ("Remove existing", "Ta bort befintliga"), + ("Restart", "Starta om"), + ("Row gutter", "Radmellanrum"), + ("Rule", "Linje"), + ("Run in", "Inlagd"), + ("Scope", "Omfång"), + ("Section headings", "Avsnittsrubriker"), + ("Shape", "Form"), + ("Space", "Blanksteg"), + ("Start number", "Startnummer"), + ("Start page", "Startsida"), + ("Subpath", "Delbana"), + ("Suggestions", "Förslag"), + ("Tagged", "Taggad"), + ("Target", "Mål"), + ("Threshold", "Tröskel"), + ("To family", "Till familj"), + ("To spread", "Till uppslag"), + ("To style", "Till stil"), + ("Toc title", "Rubrik för innehållsförteckning"), + ("Tolerance", "Tolerans"), + ("Topics", "Ämnen"), + ("Url", "URL"), + ("Use", "Använd"), + ("Use minimum height", "Använd minsta höjd"), + ("Use minimum width", "Använd minsta bredd"), + ("Value", "Värde"), + ("Visible", "Synlig"), + ("Word", "Ord"), + ("X offset", "X-förskjutning"), + ("X relative", "X relativt"), + ("Y offset", "Y-förskjutning"), + (" Save ", " Spara "), + (" Update ", " Uppdatera "), + ("(cycles)", "(växlar)"), + ("(modified)", "(ändrad)"), + ("+Bin", "+Bin"), + ("+LOC", "+LOC"), + ("A command can have at most {n} shortcuts", "Ett kommando kan ha högst {n} kortkommandon"), + ("A set with this name exists", "Det finns redan en uppsättning med det här namnet"), + ("Add a shortcut to {label}", "Lägg till ett kortkommando för {label}"), + ("All Commands", "Alla kommandon"), + ("Also used by {label}", "Används även av {label}"), + ("Alt", "Alt"), + ("Application", "Program"), + ("Approximated", "Approximerad"), + ("Assign", "Tilldela"), + ("Assign to Selected", "Tilldela markerade"), + ("Assign {chord} to {label}", "Tilldela {chord} till {label}"), + ("Auto-Collapse Iconic Panels", "Fäll ihop ikonpaneler automatiskt"), + ("Based on:", "Baserat på:"), + ("Built-in sets cannot be changed", "Inbyggda uppsättningar kan inte ändras"), + ("Cancel Import", "Avbryt import"), + ("Cancel the new shortcut", "Avbryt det nya kortkommandot"), + ("Caps Lock", "Caps Lock"), + ("Check for Updates…", "Sök efter uppdateringar…"), + ("Choose a command…", "Välj ett kommando…"), + ("Click a key to see its shortcuts", "Klicka på en tangent för att se dess kortkommandon"), + ("Close Other Panels in Group", "Stäng andra paneler i gruppen"), + ("Close panel window", "Stäng panelfönstret"), + ("Command", "Kommando"), + ("Command for {item}", "Kommando för {item}"), + ("Commands", "Kommandon"), + ("Conflicts", "Konflikter"), + ("Context:", "Sammanhang:"), + ("Copy Report", "Kopiera rapport"), + ("Ctrl", "Ctrl"), + ("Current shortcuts", "Aktuella kortkommandon"), + ("Current shortcuts:", "Aktuella kortkommandon:"), + ("Currently assigned to:", "Tilldelat för närvarande:"), + ("Default: {chord}", "Standard: {chord}"), + ("Delete Surrounding Text", "Radera omgivande text"), + ("Delete the set “{set}”?", "Radera uppsättningen ”{set}”?"), + ("Detail pane size", "Storlek på detaljrutan"), + ("EPUB Split Document", "EPUB – dela dokument"), + ("Expand", "Expandera"), + ("Expand Panels", "Expandera paneler"), + ("File Info", "Filinformation"), + ("File Info…", "Filinformation…"), + ("Fixed Keys", "Fasta tangenter"), + ("Float Tab Group", "Gör flikgruppen flytande"), + ("Go to Conflict", "Gå till konflikt"), + ("Group Tab Group with", "Gruppera flikgruppen med"), + ("Import", "Importera"), + ("Import as New Set", "Importera som ny uppsättning"), + ("Import “{set}”", "Importera ”{set}”"), + ("Imported", "Importerad"), + ("Imported as “{set}”", "Importerad som ”{set}”"), + ("Import…", "Importera…"), + ("Keep Both", "Behåll båda"), + ("Key: {key}", "Tangent: {key}"), + ("Keyboard", "Tangentbord"), + ("Keyboard Shortcuts (JSON)", "Kortkommandon (JSON)"), + ("Keyboard: US", "Tangentbord: amerikanskt"), + ("Keyboard: US (macOS)", "Tangentbord: amerikanskt (macOS)"), + ("Lock Workspace", "Lås arbetslayout"), + ("Matched without change", "Matchad utan ändring"), + ("Maximize Frame", "Maximera ram"), + ("Modifier on", "Modifierartangent på"), + ("More", "Mer"), + ("Move Guides", "Flytta stödlinjer"), + ("Move Panel to New Window", "Flytta panelen till ett nytt fönster"), + ("Move panel column", "Flytta panelkolumn"), + ("Move panel window", "Flytta panelfönster"), + ("Move tab group", "Flytta flikgrupp"), + ("New Set…", "Ny uppsättning…"), + ("New shortcut:", "Nytt kortkommando:"), + ("New shortcut: {chord}", "Nytt kortkommando: {chord}"), + ("Not assigned", "Inte tilldelad"), + ("Not changed", "Inte ändrad"), + ("Not matched", "Inte matchad"), + ("Nothing uses this key", "Ingenting använder den här tangenten"), + ("Now used by {label}", "Används nu av {label}"), + ("PDF/X-1a:2003", "PDF/X-1a:2003"), + ("Panel or context", "Panel eller sammanhang"), + ("Remove {chord} from {label}", "Ta bort {chord} från {label}"), + ("Reset All", "Återställ alla"), + ("Reset All…", "Återställ alla…"), + ("Restore Frame Size", "Återställ ramstorlek"), + ("Restore every default shortcut in this set?", "Vill du återställa alla standardkortkommandon i den här uppsättningen?"), + ("Restore the default for {label}", "Återställ standardvärdet för {label}"), + ("Revert Set", "Återgå till sparad uppsättning"), + ("Save changes to “{set}”?", "Vill du spara ändringarna i ”{set}”?"), + ("Search by key", "Sök efter tangent"), + ("Select a command first", "Markera ett kommando först"), + ("Select a command to see and change its shortcuts", "Markera ett kommando för att se och ändra dess kortkommandon"), + ("Set name:", "Namn på uppsättning:"), + ("Set:", "Uppsättning:"), + ("Shift", "Shift"), + ("Shortcut", "Kortkommando"), + ("Shortcut Sets", "Uppsättningar av kortkommandon"), + ("Show:", "Visa:"), + ("Skipped", "Överhoppade"), + ("Story Options", "Artikelalternativ"), + ("Suggestions ({n})", "Förslag ({n})"), + ("Summarize…", "Sammanfatta…"), + ("Super", "Super"), + ("Take It", "Ta över"), + ("Take It and Go to Conflict", "Ta över och gå till konflikt"), + ("Taken from other commands", "Övertagna från andra kommandon"), + ("The file is too large", "Filen är för stor"), + ("The name is empty, too long or has control characters", "Namnet är tomt, för långt eller innehåller kontrolltecken"), + ("The scope {scope} is unknown", "Omfånget {scope} är okänt"), + ("The set was not found", "Uppsättningen hittades inte"), + ("There are too many sets", "Det finns för många uppsättningar"), + ("This command is unknown", "Det här kommandot är okänt"), + ("This file is from a newer version (format {n})", "Den här filen kommer från en nyare version (format {n})"), + ("This is not a craft-keymap file", "Det här är inte en craft-keymap-fil"), + ("This is not a shortcut set file", "Det här är inte en fil med en kortkommandouppsättning"), + ("Too many shortcuts are assigned", "För många kortkommandon är tilldelade"), + ("Tools", "Verktyg"), + ("Transparency Flattener", "Utplattning av transparens"), + ("Type…", "Text…"), + ("Unreadable", "Oläsbar"), + ("Use Default", "Använd standard"), + ("Will change", "Ändras"), + ("Win", "Win"), + ("With Shortcut", "Med kortkommando"), + ("empty shortcut", "tomt kortkommando"), + ("in a context that is not imported", "i ett sammanhang som inte importeras"), + ("key sequences are not supported", "tangentsekvenser stöds inte"), + ("line {n}", "rad {n}"), + ("modifier {key} given twice", "modifierartangenten {key} anges två gånger"), + ("more than one command fits", "fler än ett kommando passar"), + ("no command has this name", "inget kommando har det här namnet"), + ("numeric keypad key read as main key", "tangent på det numeriska tangentbordet tolkas som huvudtangent"), + ("only a command of the other kind fits (tool or menu command)", "endast ett kommando av den andra typen passar (verktyg eller menykommando)"), + ("press keys", "tryck på tangenter"), + ("reserved by the system", "reserverad av systemet"), + ("taken by a later entry", "upptaget av en senare post"), + ("taken by {label}", "upptaget av {label}"), + ("the scope is unknown", "omfånget är okänt"), + ("the scope name is not valid", "omfångets namn är inte giltigt"), + ("the set's shortcuts could not be added", "uppsättningens kortkommandon kunde inte läggas till"), + ("the shortcut has no key", "kortkommandot har ingen tangent"), + ("the shortcut is too long", "kortkommandot är för långt"), + ("the shortcuts could not be removed", "kortkommandona kunde inte tas bort"), + ("unknown key {key}", "okänd tangent {key}"), + ("unknown modifier {key}", "okänd modifierartangent {key}"), + ("unsupported modifier {key}", "modifierartangenten {key} stöds inte"), + ("{chord} (Default)", "{chord} (Standard)"), + ("{chord} (press again for a double press)", "{chord} (tryck igen för dubbeltryck)"), + ("{chord} ({scope})", "{chord} ({scope})"), + ("{chord} is reserved by the system", "{chord} är reserverat av systemet"), + ("{chord} was removed from {label}", "{chord} togs bort från {label}"), + ("{chord}: {label}", "{chord}: {label}"), + ("{key}: listed more than once", "{key}: finns med mer än en gång"), + ("{key}: no command has this name", "{key}: inget kommando har det här namnet"), + ("{key}: part of the entry was dropped", "{key}: en del av posten ignorerades"), + ("{key}: the scope in “{text}” is unknown", "{key}: omfånget i ”{text}” är okänt"), + ("{key}: {text} is reserved by the system", "{key}: {text} är reserverat av systemet"), + ("{key}: “{text}” is not a shortcut ({error})", "{key}: ”{text}” är inte ett kortkommando ({error})"), + ("{label} (Changed)", "{label} (Ändrad)"), + ("{label} (cycles)", "{label} (växlar)"), + ("{label} ({scope})", "{label} ({scope})"), + ("{label} ({scope}, both work)", "{label} ({scope}, båda fungerar)"), + ("{label} keeps its shortcuts (matched by a panel menu name)", "{label} behåller sina kortkommandon (matchat via ett namn i en panelmeny)"), + ("{label}: press keys", "{label}: tryck på tangenter"), + ("{label}: {chord}", "{label}: {chord}"), + ("{label}: {chord} (Now used by {holder})", "{label}: {chord} (används nu av {holder})"), + ("{menu} Menu", "Menyn {menu}"), + ("{n} command changed", "{n} kommando ändrades"), + ("{n} command will change", "{n} kommando kommer att ändras"), + ("{n} commands changed", "{n} kommandon ändrades"), + ("{n} commands will change", "{n} kommandon kommer att ändras"), + ("{n} entries cut at a limit", "{n} poster kapades vid en gräns"), + ("{n} entries were ignored: too many commands", "{n} poster ignorerades: för många kommandon"), + ("{n} entry cut at a limit", "{n} post kapades vid en gräns"), + ("{n} entry was ignored: too many commands", "{n} post ignorerades: för många kommandon"), + ("{n} item could not be read", "{n} objekt kunde inte läsas"), + ("{n} item not matched", "{n} objekt matchades inte"), + ("{n} item without a shortcut", "{n} objekt utan kortkommando"), + ("{n} items could not be read", "{n} objekt kunde inte läsas"), + ("{n} items not matched", "{n} objekt matchades inte"), + ("{n} items without a shortcut", "{n} objekt utan kortkommando"), + ("{n} key read approximately", "{n} tangent lästes ungefärligt"), + ("{n} keys read approximately", "{n} tangenter lästes ungefärligt"), + ("{n} saved shortcut could not be used", "{n} sparat kortkommando kunde inte användas"), + ("{n} saved shortcuts could not be used", "{n} sparade kortkommandon kunde inte användas"), + ("{n} shortcut also works in another scope", "{n} kortkommando fungerar även i ett annat omfång"), + ("{n} shortcut is assigned more than once", "{n} kortkommando är tilldelat mer än en gång"), + ("{n} shortcuts also work in another scope", "{n} kortkommandon fungerar även i ett annat omfång"), + ("{n} shortcuts are assigned more than once", "{n} kortkommandon är tilldelade mer än en gång"), + ("{panel} Panel", "Panelen {panel}"), + ("{title} ({n})", "{title} ({n})"), + ("{title}: {set}", "{title}: {set}"), + ("ΔLOC", "ΔLOC"), + ("−Bin", "−Bin"), + ("−LOC", "−LOC"), + ("⚠ {chord} is used by {label}", "⚠ {chord} används av {label}"), + ("Y:", "Y:"), + ("Show the Typography panel", "Visa panelen Typografi"), + ( + "This story was edited in the layout: you'll be asked before those edits are replaced", + "Den här artikeln har ändrats i layouten: du får en fråga innan de ändringarna ersätts", + ), + ("Fewer Options", "Färre alternativ"), + ("Tab Character", "Tabbtecken"), + ("Keep In Frame", "Behåll i ramen"), + ("New Paragraph Style", "Ny styckestil"), + ("New Character Style", "Ny teckenstil"), + ("Next Style:", "Nästa stil:"), + ("[Same style]", "[Samma stil]"), + ("Reset To Base", "Återställ till bas"), + ("Apply Style to Selection", "Använd stil på markeringen"), + ("Change/Find", "Ändra/Sök"), + ("Find Format:", "Sök format:"), + ("Find Format Settings", "Formatinställningar för sökning"), + ("Specify attributes to find", "Ange attribut att söka efter"), + ( + "With a Find Format, only text that has it is found; with nothing in Find what, all text that has it", + "Med Sök format hittas bara text som har formatet; om Sök efter är tomt hittas all text som har det", + ), + ("Change Format:", "Ändra format:"), + ("Change Format Settings", "Formatinställningar för ändring"), + ("Style Options", "Stilalternativ"), + ("[Any Style]", "[Valfri stil]"), + ("Specify attributes to change", "Ange attribut att ändra"), + ("Clear specified attributes", "Rensa angivna attribut"), + ( + "With a Change Format and nothing in Change to, the found text stays and is only formatted", + "Om Ändra format är angivet och Ändra till är tomt behålls den hittade texten och formateras bara", + ), + ("Double Column", "Dubbel kolumn"), + ("Tools Panel:", "Verktygspanel:"), + ("Color Theme:", "Färgtema:"), + ("At End of Document", "I slutet av dokumentet"), + ("At Start of Document", "I början av dokumentet"), + ("Before Page", "Före sidan"), + ("After Page", "Efter sidan"), + ("Parent:", "Mallsida:"), + ("Insert:", "Infoga:"), + ("Stroke Type", "Konturtyp"), + ("Corner Shape", "Hörnform"), + ("Corner Size and Shape:", "Hörnstorlek och form:"), + ("New Section", "Nytt avsnitt"), + ("Clear Frame Fitting Options", "Rensa alternativ för ramanpassning"), + ("Cannot be edited", "Kan inte redigeras"), + ("Maximum:", "Maximum:"), + ("Paragraph Spacing Limit:", "Gränsvärde för styckeavstånd:"), + ("Overprint", "Övertryck"), + ("Top Left Corner", "Övre vänstra hörnet"), + ("Top Right Corner", "Övre högra hörnet"), + ("Bottom Left Corner", "Nedre vänstra hörnet"), + ("Bottom Right Corner", "Nedre högra hörnet"), + ("Anchor Point(s)", "Ankarpunkt(er)"), + ("Convert Points", "Konvertera punkter"), + ("Delete Point(s)", "Radera punkt(er)"), + ("Convert Text to Table…", "Konvertera text till tabell…"), + ("Convert Table to Text…", "Konvertera tabell till text…"), + ("Column Separator", "Kolumnavgränsare"), + ("Row Separator", "Radavgränsare"), + ("Number of Columns", "Antal spalter"), + ("Comma", "Komma"), + ("RGB Color Space View", "Vy för RGB-färgrymd"), + ("Lab Color Space View", "Vy för Lab-färgrymd"), + ("Add CMYK Swatch", "Lägg till CMYK-färgruta"), + ("Added swatch", "Tillagd färgruta"), + ("Eyedropper: drag it onto the page", "Pipett: dra den till sidan"), + ("PDF Preset:", "PDF-förinställning:"), + ("Continue", "Fortsätt"), + ("Printing Instructions", "Utskriftsinstruktioner"), + ("Show Problems Only", "Visa endast problem"), + ("Include PDF (Print)", "Inkludera PDF (utskrift)"), + ("Include IDML", "Inkludera IDML"), + ("Create Printing Instructions", "Skapa utskriftsinstruktioner"), + ("Entire Publication", "Hela publikationen"), + ("Scope:", "Omfång:"), + ("Source", "Källa"), + ("Installed", "Installerade"), + ("Bundled", "Medföljande"), + ("Substituted", "Ersatt"), + ("Modified", "Ändrad"), + ("Missing", "Saknas"), + ("Colors and Inks: {process} process, {spot} spot", "Färger och tryckfärger: {process} processfärger, {spot} dekorfärger"), + ("Embedded images: {embedded}", "Inbäddade bilder: {embedded}"), + ("Instructions:", "Instruktioner:"), + ("Fax:", "Fax:"), + ("Address:", "Adress:"), + ("Company:", "Företag:"), + ("Contact:", "Kontakt:"), + ("Process Black", "Processvart"), + ("Process Yellow", "Processgul"), + ("Process Magenta", "Processmagenta"), + ("Process Cyan", "Processcyan"), + ("Colors and Inks", "Färger och tryckfärger"), + ("Links and Images", "Länkar och bilder"), + ("Fonts", "Typsnitt"), + ("Summary", "Sammanfattning"), + ("Color spectrum", "Färgspektrum"), + ("Object/Convert Point", "Objekt/Konvertera punkt"), + ("Fonts: {used} in use, {missing} unavailable", "Typsnitt: {used} används, {missing} inte tillgängliga"), + ( + "Links and Images: {found} linked, {modified} changed, {missing} not found", + "Länkar och bilder: {found} länkade, {modified} ändrade, {missing} hittades inte", + ), +]; + +pub(super) const CONTEXT: &[(&str, &str, &str)] = &[ + ("Group", "selection", "Gruppera"), + ("Columns", "table", "Kolumner"), + ("Name", "contributors", "Namn"), + ("Number:", "count", "Antal:"), + ("Columns:", "grid", "Kolumner:"), + ("Span Columns", "paragraphLayout", "Sträck över spalter"), +]; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index d1e41d549..a31c7a8f0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -65,3 +65,37 @@ fn cmd_item(id: &str, params: Value) -> Item { #[cfg(test)] mod tests; + +#[cfg(test)] +mod swedish_tests { + use super::*; + + #[test] + fn swedish_interface_is_selectable_and_translates_every_menu_label() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + run_ui(&mut app, "app.language", &json!({"lang": "sv"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "sv"); + assert_eq!(checked(&app, "app.language", &json!({"lang": "sv"})), Some(true)); + assert_eq!(crate::i18n::tr("sv", "File"), "Arkiv"); + // Every menu title, item and submenu has a Swedish entry. + 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()), + _ => {} + } + } + // Swedish shares many words with English ("Layout", "Cyan"), so look for a catalog entry. + 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::swedish_has(l))); + } + assert!(untranslated.is_empty(), "untranslated Swedish menu labels: {untranslated:?}"); + } +} diff --git a/docs/localization-parity.md b/docs/localization-parity.md index df015a34b..2ac3b8277 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 (missing translations are reported, never a failure; catalog counts remain the 2026-10-10 measurement) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (missing translations are reported, never a failure; Swedish interface; catalog counts remain the 2026-10-10 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/table.rs` (a shared table with German, French, -Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,it,ja,ka,pt_br,uk}.rs`. +Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,it,ja,ka,pt_br,uk,sv}.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 @@ -61,8 +61,9 @@ craft-fonts. The language totals above retain their dated measurement; this is a | 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) | +| Swedish | sv | 2,857 of 2,891 (99%, 2026-11-01 missing-translations report): menus, panels, dialogs and messages; singular for a count of one; AI-drafted with Claude Code from the English source, spot-checked by a native speaker (not every string) | partial | -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 @@ -93,6 +94,7 @@ German variants have no automatic hyphenation; their German shaping and quotes a | Date | Change | Summary | |---|---|---| | 2026-10-11 | minor | Localized Japanese print, import and table controls; checked Japanese and English renders | +| 2026-10-15 | minor | Swedish (`sv`) interface added | | 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 | | 2026-10-11 | minor | Czech (cs) added: 1,722 strings, plural categories handled | From 7b8858d08ce5b3e6428f27e0725090cf9bee1c21 Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Sun, 1 Nov 2026 16:53:00 +0100 Subject: [PATCH 66/78] 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/78] 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/78] 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 472869598f72a51d3cdf1b18503e270ceb8d9134 Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 13:31:06 -0500 Subject: [PATCH 69/78] font.list: a face without an OS/2 table reports a null fsType A font with no readable OS/2 table doesn't say what its licence allows, and reporting fsType 0 made it look installable. FontFace::fs_type() now returns None for it, and font.list reports fsType and the five flags as null. restricted_licence() and bitmap_only() still treat it as unrestricted, so export behaviour doesn't change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ApvpyStzzHbKyDBNxpzn2Y --- ROADMAP.md | 2 +- crates/engine/src/cmd/fonts.rs | 23 +++++++++++++++-------- crates/fonts/src/fontdb/face.rs | 12 ++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9d2955ca0..b1ee7f04b 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** · `font.list` reports the embedding licence of the face that sets each font's text: its OS/2 `fsType` and what the bits mean (`restrictedLicence`, `bitmapOnly`, `noSubsetting`, `editable`, `previewPrint`; null when the font is missing). A script or host that makes PDFs can check every face, bundled and installed ones included, without reading font files. Export behaviour is unchanged. No readiness percentage change. +- **2026-10-31** · `font.list` reports the embedding licence of the face that sets each font's text: its OS/2 `fsType` and what the bits mean (`restrictedLicence`, `bitmapOnly`, `noSubsetting`, `editable`, `previewPrint`; null when the font is missing or has no OS/2 table). A script or host that makes PDFs can check every face, bundled and installed ones included, without reading font files. Export behaviour is unchanged. 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/fonts.rs b/crates/engine/src/cmd/fonts.rs index 52ddf4239..77cebe369 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -14,7 +14,7 @@ use crate::{Result, Session}; pub fn specs() -> Vec { vec![ - cmd!(query "font.list", "Fonts in Document", [], None, "{} → [{family, style, characters, missing, styleMissing, source: bundled|installed|document|added (null when missing), matchStatus: exact|styleSubstitute|missing, resolvedFamily, resolvedStyle, replacementFamily, replacementStyle, fsType, restrictedLicence, bitmapOnly, noSubsetting, editable, previewPrint (the OS/2 fsType embedding bits of the face that sets the text and what they mean; null when missing)}] (missing first)", has_doc, |s, _| { + cmd!(query "font.list", "Fonts in Document", [], None, "{} → [{family, style, characters, missing, styleMissing, source: bundled|installed|document|added (null when missing), matchStatus: exact|styleSubstitute|missing, resolvedFamily, resolvedStyle, replacementFamily, replacementStyle, fsType, restrictedLicence, bitmapOnly, noSubsetting, editable, previewPrint (the OS/2 fsType embedding bits of the face that sets the text and what they mean; null when missing or the face has no OS/2 table)}] (missing first)", has_doc, |s, _| { Ok(Value::Array(list(&s.doc()?.doc))) }), cmd!( @@ -75,7 +75,8 @@ pub(super) struct UsedFont { pub resolved_style: String, pub replacement_family: String, pub replacement_style: String, - /// The OS/2 `fsType` of the face that sets the text (`resolved_*`); None when missing. + /// The OS/2 `fsType` of the face that sets the text (`resolved_*`); None when missing or the + /// face has no OS/2 table (as are the bits below). pub fs_type: Option, /// Restricted License embedding (bit 1 without bits 2–3). pub restricted_licence: Option, @@ -136,7 +137,7 @@ pub(super) fn used_fonts(d: &Document) -> Vec { let resolved = db.face(&family, &style); let characters: Vec = characters.into_iter().filter(|c| !('\u{E000}'..='\u{E1FF}').contains(c)).collect(); let replacement = db.replacement_face(&family, &style, &characters); - let fs_type = (!missing).then(|| resolved.fs_type()); + let fs_type = (!missing).then(|| resolved.fs_type()).flatten(); let bit = |mask: u16| fs_type.map(|t| t & mask != 0); let source = (!missing).then(|| match db.face(&family, &style).source { FontSource::Bundled => "bundled", @@ -163,7 +164,7 @@ pub(super) fn used_fonts(d: &Document) -> Vec { replacement_family: replacement.family.clone(), replacement_style: replacement.style.clone(), fs_type, - restricted_licence: (!missing).then(|| resolved.restricted_licence()), + restricted_licence: fs_type.map(|_| resolved.restricted_licence()), bitmap_only: bit(0x0200), no_subsetting: bit(0x0100), editable: bit(0x0008), @@ -321,18 +322,20 @@ mod tests { /// refuse a face whose licence doesn't allow the export it is about to make. #[test] fn font_list_reports_the_embedding_licence_bits() { - use designcraft_fonts::testing::{font_with, with_fs_type}; + use designcraft_fonts::testing::{font_with, with_fs_type, with_table_len}; const FAMILY: &str = "DocFont List FsType"; const FS_TYPES: [u16; 3] = [0x0002, 0x0108, 0x0204]; for fs_type in FS_TYPES { designcraft_fonts::FontDb::global() .add_font(with_fs_type(font_with(&format!("{FAMILY} {fs_type:04x}"), &['W']).unwrap(), fs_type).unwrap()); } + // An OS/2 table cut to nothing can't be read: as if the font had none. + designcraft_fonts::FontDb::global().add_font(with_table_len(font_with(&format!("{FAMILY} no OS/2"), &['W']).unwrap(), b"OS/2", 0).unwrap()); let mut s = Session::new(); s.execute("file.new", &json!({})).unwrap(); - // "W W W Plain Gone": each W in one of the fonts above, Plain in the default (bundled) + // "W W W W Plain Gone": each W in one of the fonts above, Plain in the default (bundled) // font, Gone in a family that isn't available. - let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "W W W Plain Gone"})).unwrap(); + let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "W W W W Plain Gone"})).unwrap(); let sid = s.doc().unwrap().doc.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story; let set = |s: &mut Session, at: usize, end: usize, family: &str| { s.execute("text.select", &json!({"story": sid.0, "anchor": at, "focus": end})).unwrap(); @@ -341,7 +344,8 @@ mod tests { for (i, fs_type) in FS_TYPES.into_iter().enumerate() { set(&mut s, i * 2, i * 2 + 1, &format!("{FAMILY} {fs_type:04x}")); } - set(&mut s, 12, 16, "Nonexistent Sans"); + set(&mut s, 6, 7, &format!("{FAMILY} no OS/2")); + set(&mut s, 14, 18, "Nonexistent Sans"); let l = s.execute("font.list", &json!({})).unwrap(); let rows = l.as_array().unwrap(); let entry = |family: &str| rows.iter().find(|f| f["family"] == family).unwrap_or_else(|| panic!("{family}: {l}")); @@ -354,6 +358,9 @@ mod tests { // The bundled fonts are OFL: installable embedding, every bit clear. let bundled = rows.iter().find(|r| r["source"] == "bundled").unwrap_or_else(|| panic!("no bundled face: {l}")); assert_eq!(bits(bundled), [json!(0), f.clone(), f.clone(), f.clone(), f.clone(), f], "{l}"); + // A face without an OS/2 table doesn't say what its licence allows. + assert_eq!(entry(&format!("{FAMILY} no OS/2"))["missing"], false, "{l}"); + assert_eq!(bits(entry(&format!("{FAMILY} no OS/2"))), [(); 6].map(|()| Value::Null), "{l}"); // A missing face has no file to read. let missing = entry("Nonexistent Sans"); assert_eq!(missing["missing"], true, "{l}"); diff --git a/crates/fonts/src/fontdb/face.rs b/crates/fonts/src/fontdb/face.rs index 2283682dd..014220a16 100644 --- a/crates/fonts/src/fontdb/face.rs +++ b/crates/fonts/src/fontdb/face.rs @@ -297,21 +297,21 @@ impl FontFace { /// (bit 1) without a less restrictive bit (Preview & Print, Editable). Fonts without an OS/2 /// table aren't restricted. pub fn restricted_licence(&self) -> bool { - let fs_type = self.fs_type(); + let fs_type = self.fs_type().unwrap_or(0); fs_type & 0x0002 != 0 && fs_type & 0x000C == 0 } /// Does the font's licence allow embedding bitmaps only? OS/2 `fsType` bit 9: the outlines /// may not be passed on, so the font file can't be packaged whole. pub fn bitmap_only(&self) -> bool { - self.fs_type() & 0x0200 != 0 + self.fs_type().is_some_and(|t| t & 0x0200 != 0) } - /// The font's OS/2 `fsType` (embedding licence bits), 0 (installable) when the font has no - /// OS/2 table. - pub fn fs_type(&self) -> u16 { + /// The font's OS/2 `fsType` (embedding licence bits), None when the font has no OS/2 table it + /// can read: the font doesn't say, which isn't the same as 0 (installable). + pub fn fs_type(&self) -> Option { use skrifa::raw::TableProvider; - self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type()) + self.skrifa().and_then(|f| f.os2().ok()).map(|t| t.fs_type()) } } From 700c4bb344af3ba3e9fa0bd663778c61e8779329 Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 13:31:07 -0500 Subject: [PATCH 70/78] 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 71/78] 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 72/78] 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 73/78] #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 74/78] 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 75/78] 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 4a5b9ffbcdce2d78d03f58c74255a9b0efb8851e Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 1 Nov 2026 14:38:10 -0500 Subject: [PATCH 76/78] Swedish: register in the i18n tests, app.language docs and macOS localizations Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n/tests.rs | 12 ++++++++++-- crates/ui-egui/src/menus/ui_commands.rs | 2 +- packaging/macos/Info.plist.in | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index db13138cf..065afa599 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -13,12 +13,20 @@ const LANGUAGE_TABLES: &[(&str, &[(&str, &str)])] = &[ ("pl", pl::TABLE), ("pt-br", pt_br::TABLE), ("ru", ru::TABLE), + ("sv", sv::TABLE), ("uk", uk::TABLE), ]; /// 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), + ("ja", ja::CONTEXT), + ("pl", pl::CONTEXT), + ("ru", ru::CONTEXT), + ("sv", sv::CONTEXT), + ("uk", uk::CONTEXT), +]; /// The languages of the shared tables' columns, in column order. const SHARED_COLUMNS: [&str; 5] = ["de", "fr", "es", "ja", "zh"]; diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 4a75a994e..5d927fba7 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -71,7 +71,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ "app.language", "Interface Language", None, - "{lang?: \"\"|de|fr|es|ja|zh|ar|pt-br|it|uk|ru|cs|pl|ka} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting", + "{lang?: \"\"|de|fr|es|ja|zh|ar|pt-br|it|uk|ru|cs|pl|ka|sv} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting", ), ( "app.flattener", diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in index 03d7cb8e4..28bd0cd84 100644 --- a/packaging/macos/Info.plist.in +++ b/packaging/macos/Info.plist.in @@ -22,6 +22,7 @@ pl pt-BR ru + sv uk zh-Hans From 34b7317cf4bcaf35ffd1602a6d61e77829029ee9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 1 Nov 2026 14:38:23 -0500 Subject: [PATCH 77/78] 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 }); } From cc3c70d4be5092262b61c43b607e26b4a0b82fc6 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:39:44 -0500 Subject: [PATCH 78/78] Windows: keep the custom title bar window borderless (#2139) winit 0.30 gives an undecorated window WS_CAPTION and WS_BORDER; on some systems the compositor reserves that frame, so the content is drawn offset (black bars at the top and left) while the pointer keeps the client coordinates. A per-frame guard strips the styles again after every winit flag change and keeps a maximized window clear of the taskbar. Fixes #1295. --- ROADMAP.md | 1 + apps/designcraft/Cargo.toml | 3 +- apps/designcraft/src/main.rs | 4 + apps/designcraft/src/window_frame.rs | 352 +++++++++++++++++++++++ crates/ui-egui/src/app/input.rs | 5 + crates/ui-egui/src/lib.rs | 5 +- crates/ui-egui/src/services.rs | 7 + crates/ui-egui/src/window_frame_tests.rs | 39 +++ 8 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 apps/designcraft/src/window_frame.rs create mode 100644 crates/ui-egui/src/window_frame_tests.rs diff --git a/ROADMAP.md b/ROADMAP.md index b3cafbb3a..58a1913e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -382,6 +382,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- **2026-11-02** · Windows: the custom title bar window stays borderless, so the content is no longer drawn offset by the caption and border (black bars at the top and left, clicks landing off target). The workaround goes when winit carries the fix (storytold/winit#17). No readiness percentage change. - **2026-10-30** · The Color Picker takes an eyedropper: dragged from the picker onto the page, it takes the colour the canvas shows (proofed under Proof Colors). No readiness percentage change. - **2026-10-30** · The Color Picker is laid out as the reference app's: a field and slider for the component chosen, HSB, Lab, RGB and CMYK values, the old colour, and Add CMYK Swatch; Enter and Esc close it once. No readiness percentage change. - **2026-10-18** · Table of Contents gains Create PDF Bookmarks: included headings become nested page destinations, regenerated in their existing outline position on update without replacing or moving surrounding manual bookmarks. The setting survives native and IDML files. No readiness percentage change. diff --git a/apps/designcraft/Cargo.toml b/apps/designcraft/Cargo.toml index d706374c7..47eafee71 100644 --- a/apps/designcraft/Cargo.toml +++ b/apps/designcraft/Cargo.toml @@ -37,7 +37,8 @@ winit = { version = "0.30", default-features = false, features = ["x11"] } craft-update = { workspace = true } # EnumDisplayDevices (src/gpu.rs): which GPU drives the display, so the window draws on the GPU # that shows it; drawing on a GPU without a monitor reset the display driver. Safe wrappers over -# Win32 (MIT); the `user` feature is User32.dll only. +# Win32 (MIT); the `user` feature is User32.dll only. Also the window frame for the custom title +# bar (src/window_frame.rs, window styles). winsafe = { version = "0.0.29", default-features = false, features = ["user"] } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/apps/designcraft/src/main.rs b/apps/designcraft/src/main.rs index 4b4a1ab7f..ea0747a46 100644 --- a/apps/designcraft/src/main.rs +++ b/apps/designcraft/src/main.rs @@ -22,6 +22,7 @@ mod logging; #[cfg(target_os = "macos")] mod native_menu; mod profile; +mod window_frame; use designcraft_engine::Session; use designcraft_ui_egui::{DesignApp, Services}; @@ -428,6 +429,9 @@ fn main() -> std::process::ExitCode { } app.integrated_titlebar = cfg!(target_os = "macos"); app.custom_titlebar = cfg!(target_os = "windows"); + // The custom title bar's undecorated window must stay borderless, or its caption + // and border offset the content and the pointer on some Windows systems (#1295). + app.services.window_frame = window_frame::service(cc, app.custom_titlebar); if let Some(port) = control_port { let rx = control_server::start(port, cc.egui_ctx.clone()); app = app.with_control(rx); diff --git a/apps/designcraft/src/window_frame.rs b/apps/designcraft/src/window_frame.rs new file mode 100644 index 000000000..c50e1870c --- /dev/null +++ b/apps/designcraft/src/window_frame.rs @@ -0,0 +1,352 @@ +//! Keeps the custom (undecorated) title bar window truly borderless (#1295). +//! +//! winit 0.30 gives an undecorated window the `WS_CAPTION | WS_BORDER` styles and hides the +//! non-client area by spreading the client rect over it (`WM_NCCALCSIZE`). On some systems +//! (reported: Windows 10 LTSB, Intel HD Graphics 4600, 1366 × 768) the desktop compositor still +//! reserves that frame for the window surface: the content is drawn offset by the caption and +//! border, leaving black bars at the window's top-left, while the pointer keeps the un-offset +//! client coordinates, so it no longer points at what is drawn under it. +//! +//! winit 0.27 — before rust-windowing/winit#2419 — simply omitted those styles for undecorated +//! windows, and there was no offset. [`service`] restores that: it strips `WS_CAPTION` and +//! `WS_BORDER` and forces Windows to recalculate the frame. winit puts them back whenever it +//! changes the window flags (maximize, restore, …), so the returned guard checks every frame. +//! +//! Windows sizes a maximize by the frame its styles describe, so a window maximized while its +//! caption is gone is sized without one: its rect covers the whole monitor, taskbar included. +//! The shell reads a caption-less window covering its monitor as a full-screen app and drops the +//! taskbar's backdrop behind it — a black band on stock Windows 11, a glitched unresponsive one +//! under ExplorerPatcher (photocraft#3144). The guard therefore also trims a parked maximize's rect back to +//! the work area plus its own frame, clear of the taskbar. winit pins the client area to the work +//! area either way (`WM_NCCALCSIZE`), so the app's pixels do not move. +//! +//! An auto-hidden taskbar reserves no work area, so that trim leaves the maximize covering the +//! whole monitor, and the shell, taking it for a full-screen app, no longer reveals the taskbar +//! when the pointer reaches its edge (photocraft#4845). On each edge with an auto-hidden taskbar the guard +//! instead stops the window one pixel short of the monitor, as other custom-frame apps do. +//! +//! The long-term fix is in winit itself (storytold/winit#17); this module goes when that lands. +//! +//! The Windows calls go through safe `winsafe` wrappers; the workspace forbids `unsafe`, and +//! winsafe can't wrap a raw handle without it, so the window is found by its handle instead. + +pub use platform::service; + +#[cfg(target_os = "windows")] +mod platform { + use designcraft_ui_egui::WindowFrameFn; + use eframe::wgpu::rwh::{HasWindowHandle, RawWindowHandle}; + use winsafe::prelude::Handle; + use winsafe::{AtomStr, HWND, HwndPlace, POINT, SIZE, co}; + + /// A rect in desktop coordinates: `(left, top, right, bottom)`, right and bottom exclusive. + type Rect = (i32, i32, i32, i32); + + /// A monitor edge. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Edge { + Left, + Top, + Right, + Bottom, + } + + /// A guard that keeps the window borderless each frame, or `None` when the app uses the + /// system title bar (the window is decorated) or its handle can't be found. + pub fn service(cc: &eframe::CreationContext<'_>, custom_titlebar: bool) -> Option { + if !custom_titlebar { + return None; + } + let target = match cc.window_handle().ok()?.as_raw() { + RawWindowHandle::Win32(h) => h.hwnd.get(), + _ => return None, + }; + let hwnd = find_window(target)?; + reapply(&hwnd); + Some(Box::new(move || reapply(&hwnd))) + } + + /// The top-level window with the given raw handle. `EnumWindows` hands out real `HWND`s, so + /// this matches winit's own window without wrapping a raw handle (which needs `unsafe`). + fn find_window(target: isize) -> Option { + let mut found = None; + let _ = winsafe::EnumWindows(|hwnd| { + if hwnd.ptr() as isize == target { + found = Some(hwnd); + false + } else { + true + } + }); + found + } + + /// One pass of the per-frame guard: the styles winit put back come off again, and a parked + /// maximize stays clear of the taskbar. + fn reapply(hwnd: &HWND) { + strip(hwnd); + keep_clear_of_the_taskbar(hwnd); + } + + /// Removes `WS_CAPTION`/`WS_BORDER` and has Windows recalculate the frame. A no-op once they + /// are gone, so it is cheap to call every frame. + fn strip(hwnd: &HWND) { + let style = hwnd.style(); + let without = borderless(style); + if without.raw() == style.raw() { + return; + } + hwnd.set_style(without); + let _ = hwnd.SetWindowPos( + HwndPlace::None, + POINT::new(), + SIZE::new(), + co::SWP::FRAMECHANGED | co::SWP::NOCOPYBITS | co::SWP::NOMOVE | co::SWP::NOSIZE | co::SWP::NOZORDER | co::SWP::NOACTIVATE, + ); + } + + /// How far a parked maximize reaches past a monitor/work-area edge: the resize + /// border (about 8 pt at 100 %, more with DPI). Past that, a maximized window's rect is not + /// parked but being dragged or snapped, and is left alone. + const FRAME: i32 = 32; + + /// Keep a parked maximize inside all four work-area edges, including a top taskbar. + fn keep_clear_of_the_taskbar(hwnd: &HWND) { + if !hwnd.IsZoomed() { + return; + } + let (Ok(r), Ok(mi)) = (hwnd.GetWindowRect(), hwnd.MonitorFromWindow(co::MONITOR::DEFAULTTONEAREST).GetMonitorInfo()) else { + return; + }; + let (m, w) = (mi.rcMonitor, mi.rcWork); + let (rect, monitor, work) = ((r.left, r.top, r.right, r.bottom), (m.left, m.top, m.right, m.bottom), (w.left, w.top, w.right, w.bottom)); + // The taskbars (a couple of `FindWindowEx` calls) are only looked up while the window + // covers the whole monitor, which a trimmed maximize no longer does. + let Some((left, top, right, bottom)) = clamped_rect(rect, monitor, work) + .or_else(|| covers(rect, monitor).then(|| short_of_autohide_taskbars(rect, monitor, work, &taskbar_edges(monitor))).flatten()) + else { + return; + }; + let (Some(width), Some(height)) = (right.checked_sub(left), bottom.checked_sub(top)) else { + return; + }; + // Moving the outer frame while winit pins the client rect to the work area must + // not copy the old frame's pixels into the custom title bar. + let _ = hwnd.SetWindowPos( + HwndPlace::None, + POINT { x: left, y: top }, + SIZE { cx: width, cy: height }, + co::SWP::FRAMECHANGED | co::SWP::NOCOPYBITS | co::SWP::NOZORDER | co::SWP::NOACTIVATE, + ); + let _ = hwnd.InvalidateRect(None, false); + } + + /// Return the corrected outer rect in desktop coordinates, preserving the resize border. + /// Each edge must be parked just outside either the monitor or work-area edge. Accepting + /// both makes the guard idempotent and handles Windows already correcting some edges. + /// Dragged/snapped windows are left alone; no monitor-origin or DPI assumptions are needed. + fn clamped_rect(rect: (i32, i32, i32, i32), monitor: (i32, i32, i32, i32), work: (i32, i32, i32, i32)) -> Option<(i32, i32, i32, i32)> { + let (l, t, r, b) = rect; + let (ml, mt, mr, mb) = monitor; + let (wl, wt, wr, wb) = work; + if ml >= mr || mt >= mb || wl < ml || wt < mt || wr > mr || wb > mb || wl >= wr || wt >= wb { + return None; + } + // Infer the frame from an edge without a taskbar. On an obstructed edge, the + // monitor and work-area offsets can both look like valid borders (e.g. auto-hide). + let border = [ + (ml == wl).then(|| ml.checked_sub(l)).flatten(), + (mt == wt).then(|| mt.checked_sub(t)).flatten(), + (mr == wr).then(|| r.checked_sub(mr)).flatten(), + (mb == wb).then(|| b.checked_sub(mb)).flatten(), + ] + .into_iter() + .flatten() + .filter(|n| (0..=FRAME).contains(n)) + .min()?; + let parked = |monitor: Option, work: Option| monitor == Some(border) || work == Some(border); + if !parked(ml.checked_sub(l), wl.checked_sub(l)) + || !parked(mt.checked_sub(t), wt.checked_sub(t)) + || !parked(r.checked_sub(mr), r.checked_sub(wr)) + || !parked(b.checked_sub(mb), b.checked_sub(wb)) + { + return None; + } + let corrected = (wl.checked_sub(border)?, wt.checked_sub(border)?, wr.checked_add(border)?, wb.checked_add(border)?); + (corrected != rect).then_some(corrected) + } + + /// Whether `rect` reaches every edge of `monitor`: what the shell takes for a full-screen app. + fn covers(rect: Rect, monitor: Rect) -> bool { + rect.0 <= monitor.0 && rect.1 <= monitor.1 && rect.2 >= monitor.2 && rect.3 >= monitor.3 + } + + /// The edges of `monitor` that have a taskbar (the primary `Shell_TrayWnd` or a secondary + /// monitor's `Shell_SecondaryTrayWnd`), shown or hidden. + fn taskbar_edges(monitor: Rect) -> Vec { + let mut edges = Vec::new(); + for class in ["Shell_TrayWnd", "Shell_SecondaryTrayWnd"] { + let mut after: Option = None; + // One taskbar per monitor; the bound only guards against a misbehaving shell. + for _ in 0..64 { + let Ok(Some(tray)) = HWND::NULL.FindWindowEx(after.as_ref(), AtomStr::from_str(class), None) else { + break; + }; + if let Ok(r) = tray.GetWindowRect() { + edges.extend(taskbar_edge((r.left, r.top, r.right, r.bottom), monitor)); + } + after = Some(tray); + } + } + edges + } + + /// The edge of `monitor` a taskbar with rect `taskbar` sits on: the one nearest its centre, + /// or `None` when it is on another monitor. An auto-hidden taskbar is parked mostly past its + /// edge with a sliver left on screen, so its centre may lie outside the monitor. + fn taskbar_edge(taskbar: Rect, monitor: Rect) -> Option { + let (tl, tt, tr, tb) = (i64::from(taskbar.0), i64::from(taskbar.1), i64::from(taskbar.2), i64::from(taskbar.3)); + let (ml, mt, mr, mb) = (i64::from(monitor.0), i64::from(monitor.1), i64::from(monitor.2), i64::from(monitor.3)); + if tl >= tr || tt >= tb || ml >= mr || mt >= mb || tr <= ml || tl >= mr || tb <= mt || tt >= mb { + return None; + } + // Doubled centre and distances, so nothing rounds. + let (cx, cy) = (tl + tr, tt + tb); + [(Edge::Left, cx - 2 * ml), (Edge::Top, cy - 2 * mt), (Edge::Right, 2 * mr - cx), (Edge::Bottom, 2 * mb - cy)] + .into_iter() + .min_by_key(|&(_, distance)| distance) + .map(|(edge, _)| edge) + } + + /// A maximize covering its monitor, trimmed to stop one pixel short of each edge where a + /// taskbar reserves no work area (it auto-hides), so the shell still reveals it there. + /// `None` when there is nothing to trim. + fn short_of_autohide_taskbars(rect: Rect, monitor: Rect, work: Rect, taskbars: &[Edge]) -> Option { + if !covers(rect, monitor) { + return None; + } + let (mut l, mut t, mut r, mut b) = rect; + for edge in taskbars { + match edge { + Edge::Left if work.0 == monitor.0 => l = monitor.0.checked_add(1)?, + Edge::Top if work.1 == monitor.1 => t = monitor.1.checked_add(1)?, + Edge::Right if work.2 == monitor.2 => r = monitor.2.checked_sub(1)?, + Edge::Bottom if work.3 == monitor.3 => b = monitor.3.checked_sub(1)?, + _ => {} + } + } + let trimmed = (l, t, r, b); + (l < r && t < b && trimmed != rect).then_some(trimmed) + } + + /// `style` without the OS title bar and frame (`WS_CAPTION`, which already includes + /// `WS_BORDER`). The sizing border (`WS_THICKFRAME`) stays, so edge resizing and Aero snap + /// keep working, as they did with winit 0.27's undecorated windows. + fn borderless(style: co::WS) -> co::WS { + style & !(co::WS::CAPTION | co::WS::BORDER) + } + + #[cfg(test)] + mod tests { + use super::{Edge, borderless, clamped_rect, short_of_autohide_taskbars, taskbar_edge}; + use winsafe::co; + + #[test] + fn the_borderless_style_drops_the_caption_and_border_only() { + // `WS_CAPTION` is 0x00c0_0000 and already contains `WS_BORDER` (0x0080_0000). + let decorated = co::WS::CAPTION | co::WS::BORDER | co::WS::SYSMENU | co::WS::THICKFRAME | co::WS::MAXIMIZEBOX | co::WS::MINIMIZEBOX; + let bare = borderless(decorated); + assert!(!bare.has(co::WS::CAPTION), "caption gone"); + assert!(!bare.has(co::WS::BORDER), "border gone"); + assert!(bare.has(co::WS::THICKFRAME), "edge resizing kept"); + assert!(bare.has(co::WS::MAXIMIZEBOX) && bare.has(co::WS::SYSMENU), "window buttons kept"); + // Already borderless: unchanged, so the per-frame call does nothing. + assert_eq!(bare.raw(), borderless(bare).raw()); + } + + #[test] + fn a_maximize_respects_taskbars_on_every_edge() { + let monitor = (0, 0, 1920, 1080); + let rect = (-8, -8, 1928, 1088); + for (work, expected) in [ + ((0, 0, 1920, 1046), (-8, -8, 1928, 1054)), + ((0, 48, 1920, 1080), (-8, 40, 1928, 1088)), + ((0, 2, 1920, 1080), (-8, -6, 1928, 1088)), + ((0, 0, 1920, 1078), (-8, -8, 1928, 1086)), + ((48, 0, 1920, 1080), (40, -8, 1928, 1088)), + ((0, 0, 1872, 1080), (-8, -8, 1880, 1088)), + ] { + assert_eq!(clamped_rect(rect, monitor, work), Some(expected)); + assert_eq!(clamped_rect(expected, monitor, work), None, "already corrected"); + } + } + + #[test] + fn a_top_taskbar_keeps_its_offset_at_other_origins_and_frame_sizes() { + let monitor = (-1920, -1080, 0, 0); + let work = (-1920, -1032, 0, 0); + for border in [0, 8, 16, 32] { + let rect = (-1920 - border, -1080 - border, border, border); + let expected = (-1920 - border, -1032 - border, border, border); + assert_eq!(clamped_rect(rect, monitor, work), Some(expected)); + assert_eq!(clamped_rect(expected, monitor, work), None); + } + } + + #[test] + fn a_maximize_without_a_reserved_taskbar_is_left_alone() { + let monitor = (0, 0, 1920, 1080); + assert_eq!(clamped_rect((-8, -8, 1928, 1088), monitor, monitor), None); + } + + #[test] + fn a_maximize_being_dragged_or_snapped_is_left_alone() { + let monitor = (0, 0, 1920, 1080); + let work = (0, 48, 1920, 1080); + assert_eq!(clamped_rect((100, 200, 1100, 900), monitor, work), None, "moved"); + assert_eq!(clamped_rect((-500, -8, 1428, 1088), monitor, work), None, "dragged"); + assert_eq!(clamped_rect((0, 48, 960, 1080), monitor, work), None, "half-screen"); + } + + #[test] + fn a_maximize_leaves_a_pixel_strip_on_an_auto_hidden_taskbar_edge() { + // photocraft#4845: an auto-hidden taskbar reserves no work area. + let monitor = (0, 0, 1920, 1080); + let rect = (-8, -8, 1928, 1088); + for (edge, expected) in [ + (Edge::Bottom, (-8, -8, 1928, 1079)), + (Edge::Top, (-8, 1, 1928, 1088)), + (Edge::Left, (1, -8, 1928, 1088)), + (Edge::Right, (-8, -8, 1919, 1088)), + ] { + assert_eq!(short_of_autohide_taskbars(rect, monitor, monitor, &[edge]), Some(expected)); + assert_eq!(short_of_autohide_taskbars(expected, monitor, monitor, &[edge]), None, "already trimmed"); + } + assert_eq!(short_of_autohide_taskbars(rect, monitor, monitor, &[]), None, "no taskbar"); + // A taskbar that reserves its work area is the clamp's job. + assert_eq!(short_of_autohide_taskbars(rect, monitor, (0, 0, 1920, 1032), &[Edge::Bottom]), None); + assert_eq!(short_of_autohide_taskbars((0, 0, 960, 1080), monitor, monitor, &[Edge::Bottom]), None, "half-screen"); + } + + #[test] + fn a_taskbar_sits_on_the_monitor_edge_nearest_its_centre() { + let monitor = (0, 0, 1920, 1080); + assert_eq!(taskbar_edge((0, 1032, 1920, 1080), monitor), Some(Edge::Bottom), "shown"); + assert_eq!(taskbar_edge((0, 1078, 1920, 1126), monitor), Some(Edge::Bottom), "auto-hidden"); + assert_eq!(taskbar_edge((0, -46, 1920, 2), monitor), Some(Edge::Top)); + assert_eq!(taskbar_edge((-60, 0, 2, 1080), monitor), Some(Edge::Left)); + assert_eq!(taskbar_edge((1918, 0, 1980, 1080), monitor), Some(Edge::Right)); + assert_eq!(taskbar_edge((1920, 1032, 3840, 1080), monitor), None, "other monitor"); + assert!(taskbar_edge((i32::MIN, i32::MIN, i32::MAX, i32::MAX), monitor).is_some(), "no overflow"); + } + } +} + +#[cfg(not(target_os = "windows"))] +mod platform { + use designcraft_ui_egui::WindowFrameFn; + + pub fn service(_cc: &eframe::CreationContext<'_>, _custom_titlebar: bool) -> Option { + None + } +} diff --git a/crates/ui-egui/src/app/input.rs b/crates/ui-egui/src/app/input.rs index 8d248561c..e889c3287 100644 --- a/crates/ui-egui/src/app/input.rs +++ b/crates/ui-egui/src/app/input.rs @@ -23,6 +23,11 @@ impl DesignApp { /// Inject synthetic events (one pointer event per host iteration), retaining control /// provenance until a settled UI pass has actually taken the batch. pub fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) { + // An undecorated window gets its caption back when the toolkit changes its flags + // (maximize, restore, …): take it off again before the frame is laid out (#1295). + if let Some(window_frame) = self.services.window_frame.as_mut() { + window_frame(); + } if !self.injected_settle_hook { let pending = self.injected_pending.clone(); let settled = self.injected_settled.clone(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index de364b91f..ef8629896 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -71,7 +71,7 @@ use app::browser_import_tests; pub use app::opens_as_document; pub use clock::now_ms; pub use key_names::{drop_key_name_text, is_key_name}; -pub use services::{ClipboardFn, DownloadFn, Inbox, OpenAsyncFn, ReadFn, Services, WriteFn}; +pub use services::{ClipboardFn, DownloadFn, Inbox, OpenAsyncFn, ReadFn, Services, WindowFrameFn, WriteFn}; pub(crate) use state::second_window_viewport; pub use state::{CanvasCache, HiddenPanels, ImportRequest, ImportedFile, PdfExportSettings, Perf, SavedWorkspace, ScreenMode, Shown, UiState, View}; @@ -314,6 +314,9 @@ pub fn cjk_features(app: &DesignApp) -> bool { #[cfg(test)] mod tests; +#[cfg(test)] +mod window_frame_tests; + /// The whole window in a headless test harness, at a chosen window size. #[cfg(test)] pub(crate) mod test_window { diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index b413e90c4..47459cddb 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -11,6 +11,9 @@ pub type DownloadFn = Box Result<(), String>>; pub type ClipboardFn = Box Option>; +/// Keeps the window's frame as the app needs it; called once per frame, before it is laid out. +pub type WindowFrameFn = Box; + /// Files delivered asynchronously with their original operation and document identity. pub type Inbox = std::sync::Arc>>; @@ -32,4 +35,8 @@ pub struct Services { /// The system clipboard's text, for Paste chosen from a menu (egui only delivers it with the /// paste keys, which a native menu bar takes first). pub clipboard_text: Option, + /// Keeps the undecorated window borderless, so the content and the pointer stay aligned + /// (Windows custom title bar, #1295). `None` when the window is decorated or the platform + /// needs no fixing. + pub window_frame: Option, } diff --git a/crates/ui-egui/src/window_frame_tests.rs b/crates/ui-egui/src/window_frame_tests.rs new file mode 100644 index 000000000..18be1ae5b --- /dev/null +++ b/crates/ui-egui/src/window_frame_tests.rs @@ -0,0 +1,39 @@ +//! The window-frame service keeps the custom title bar's undecorated window borderless (#1295). +//! +//! winit 0.30 gives an undecorated window a caption and border (`WS_CAPTION | WS_BORDER`), which +//! on some Windows systems offset the drawn content and the pointer. The app shell strips them +//! and re-strips them after every toolkit flag change (maximize, restore, …), driven from +//! `raw_input_hook`. These tests pin the shell side: the hook must run the service once per frame, +//! before the frame is laid out. + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use super::*; + +#[test] +fn the_window_frame_service_runs_every_raw_input_hook() { + let calls = Arc::new(AtomicUsize::new(0)); + let n = calls.clone(); + let services = Services { + window_frame: Some(Box::new(move || { + n.fetch_add(1, Ordering::SeqCst); + })), + ..Default::default() + }; + let mut app = DesignApp::new(designcraft_engine::Session::new(), services); + let ctx = egui::Context::default(); + for _ in 0..3 { + app.raw_input_hook(&ctx, &mut egui::RawInput::default()); + } + assert_eq!(calls.load(Ordering::SeqCst), 3); +} + +#[test] +fn no_window_frame_service_is_fine() { + // Decorated windows and non-Windows platforms leave the service unset; the hook must simply + // do nothing. + let mut app = DesignApp::new(designcraft_engine::Session::new(), Services::default()); + let ctx = egui::Context::default(); + app.raw_input_hook(&ctx, &mut egui::RawInput::default()); +}