diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 622ace75c..29f36a0d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,6 +63,7 @@ jobs: needs: git-sources permissions: contents: write + packages: write # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/release.yml@main with: diff --git a/Cargo.lock b/Cargo.lock index c89a20002..34b5455fd 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", @@ -1504,6 +1504,7 @@ dependencies = [ "log", "quick-xml 0.41.0", "resvg", + "skrifa 0.47.0", "tiff", "zune-core", "zune-jpeg", @@ -1511,7 +1512,7 @@ dependencies = [ [[package]] name = "designcraft-mcp" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-engine", "designcraft-geom", @@ -1524,7 +1525,7 @@ dependencies = [ [[package]] name = "designcraft-pdf" -version = "0.15.2" +version = "0.16.0" dependencies = [ "chrono", "craft-cms", @@ -1550,7 +1551,7 @@ dependencies = [ [[package]] name = "designcraft-pptx" -version = "0.15.2" +version = "0.16.0" dependencies = [ "chrono", "designcraft-color", @@ -1568,7 +1569,7 @@ dependencies = [ [[package]] name = "designcraft-render" -version = "0.15.2" +version = "0.16.0" dependencies = [ "craft-pdf", "designcraft-color", @@ -1587,7 +1588,7 @@ dependencies = [ [[package]] name = "designcraft-textimport" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-doc", "quick-xml 0.41.0", @@ -1596,7 +1597,7 @@ dependencies = [ [[package]] name = "designcraft-tools" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1608,7 +1609,7 @@ dependencies = [ [[package]] name = "designcraft-ui-egui" -version = "0.15.2" +version = "0.16.0" dependencies = [ "craft-dialogs", "craft-keymap", @@ -1640,7 +1641,7 @@ dependencies = [ [[package]] name = "designcraft-web" -version = "0.15.2" +version = "0.16.0" dependencies = [ "designcraft-engine", "designcraft-format", @@ -7604,7 +7605,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" diff --git a/NOTICE b/NOTICE index 188a3dc76..b96185401 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/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. diff --git a/ROADMAP.md b/ROADMAP.md index b1ee7f04b..4e9b46cda 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -389,6 +389,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 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-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. @@ -405,6 +406,7 @@ Newest first. - **2026-10-20** · Edit › Keyboard Shortcuts is craft-keymap's shared dialog: commands grouped by menu with Show and Filter, recording that asks before taking a key from another command, undo and redo, named shortcut sets (edits of Default become Custom; an imported set becomes a set of its own), an on-screen US keyboard, InDesign Show Set files reviewed before they apply, JSON export and an HTML summary. ui.json keeps the `shortcuts` map earlier versions read, for the active set. No readiness percentage change. - **2026-10-19** · Import InDesign Set… runs on craft-keymap's importer: a set's Window menu panel items (Layers F7, Pages F12, Swatches F5) and Tools items set the panel and tool shortcuts, all of an item's Default keys apply, and the report adds numeric keypad keys read as main keys and likely commands for unmatched items (`window.importShortcuts`). On InDesign's Windows defaults it matches 366 of 1,629 items (was 233) and changes 23 commands. No readiness percentage change. - **2026-10-19** · Keyboard shortcuts run on the shared craft-keymap crate. Panels take shortcuts, with InDesign's defaults where the key is free (Swatches F5, Color F6, Layers F7, Info F8, Stroke F10, Paragraph Styles F11, Character Styles Shift+F11, Pages F12, Align Shift+F7); tool keys are editable in Edit › Keyboard Shortcuts and `window.setShortcut`, and tools sharing a key take turns (B). Saved shortcuts load unchanged. No readiness percentage change. +- **2026-10-17** · HU1: initial Hungarian (`hu` / Magyar) interface localization (updated 2026-11-01): 1,906 catalog entries, four context captions and all 37 tool labels. AI-assisted; the initial catalog was personally reviewed in the app by Veress Róbert, a native Hungarian speaker with over 30 years of DTP experience. Uncatalogued strings keep their English fallback. Hungarian spelling and hyphenation are separate work in #1484. No readiness percentage change. - **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change. - **2026-10-20** · PDF/X-1a:2003 export (craft-libs#194): `file.exportPdf {standard: "x1a"}`, the Export PDF dialog's PDF/X-1a:2003 preset and standard. The PDF/X-4 print file is flattened in its inks by craft-prepress (no transparency; spot, process, overprint and live text kept; complex regions as DeviceN images at the Transparency Flattener preset's resolution), then written as PDF 1.4 with the X-1a identification and checked; `validate-pdf --standard x1a`. Soft drop shadows reach the flattener in their swatch's inks; other soft effects are still RGB images. No readiness percentage change. - **2026-10-20** · Print color (craft-libs#190): CMYK print PDFs blend in DeviceCMYK through craft-pdf's print settings instead of byte rewrites, with the CMYK working space as the output intent in PDF/X-4 only; overprint (Overprint Fill, Stroke and Gap, overprinting text, 100% [Black]) is written as `/op` `/OP` `/OPM 1`; Knockout Group writes `/K true` instead of becoming an image; File › Print prints the PDF Export PDF writes. No readiness percentage change. 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/contributors/people.toml b/contributors/people.toml index cd1cb022a..3e1b92d94 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -63,11 +63,23 @@ real_name = "Marco Arnulfo" display_name = "Marco Arnulfo" sync_github_name = false +[people.macrobi] +real_name = "Veress Róbert" +display_name = "Veress Róbert" +sync_github_name = false + [people.4waymedia] real_name = "Paul Gemignani" [people.skylartaylor] display_name = "skylartaylor" +[people.y-az] +real_name = "Youssef Azriouil" +display_name = "youssef" + [people.dylanmtaylor] sync_github_name = true + +[people.Tom-R-Main] +sync_github_name = true 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(); diff --git a/crates/compose/Cargo.toml b/crates/compose/Cargo.toml index ea6a0bf42..37092adb8 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", "french", "german", "greek", "italian", "portuguese", "spanish", "norwegian"] } +hypher = { version = "0.1.8", default-features = false, features = ["alloc", "dutch", "french", "german", "greek", "italian", "portuguese", "spanish", "norwegian"] } [dev-dependencies] designcraft-fonts = { workspace = true, features = ["testing"] } diff --git a/crates/compose/src/hyphen.rs b/crates/compose/src/hyphen.rs index be9e7b7e6..b63c1b110 100644 --- a/crates/compose/src/hyphen.rs +++ b/crates/compose/src/hyphen.rs @@ -3,7 +3,7 @@ //! `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 +//! Dutch is Piet Tutelaers's `hyph-nl.tex` 1.1 from hyph-utf8, MIT; 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. French is `hyph-fr.tex` 2.13 from hyph-utf8 (Daniel //! Flipo, Bernard Gaulle, Arthur Reutenauer), MIT; Italian is `hyph-it.tex` 4.9 from hyph-utf8 @@ -588,6 +588,28 @@ mod tests { assert_eq!(marked("dell'impaginazione", it), "dell'im-pa-gi-na-zio-ne", "after an elision"); } + #[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"); + // 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] fn heuristic_fallback() { let c: Vec = "happen".chars().collect(); 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..4e90f30fb 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,41 @@ 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"), + // 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]; + assert_eq!((st.doc.title.as_str(), st.title()), (title, title.to_string()), "{name}"); + } + } +} + #[cfg(test)] mod recent_tests { use serde_json::json; 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(); diff --git a/crates/engine/src/math.rs b/crates/engine/src/math.rs index 468ce1c5a..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; diff --git a/crates/engine/src/math/tests.rs b/crates/engine/src/math/tests.rs index 9d1e75884..ddc006be6 100644 --- a/crates/engine/src/math/tests.rs +++ b/crates/engine/src/math/tests.rs @@ -474,3 +474,12 @@ 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:?}"); + // 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); +} 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(); 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); +} 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))); + } } 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"); + } } 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(); 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()); +} 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"); 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() } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 3c2565052..f1f574ce4 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -12,6 +12,7 @@ mod ar; mod cs; mod de; mod fr; +mod hu; mod it; mod ja; mod ka; @@ -36,6 +37,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[ ("cs", "Čeština"), ("pl", "Polski"), ("ka", "ქართული"), + ("hu", "Magyar"), ]; /// The interface languages in the order the Preferences picker lists them: English first (the @@ -77,6 +79,8 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str { if text.trim().is_empty() { s } else { text } } +/// Looks up a label in lazily cached language tables, using the shared table where supported. +/// Unknown languages and missing keys keep the source text; `tr` also handles blank entries. fn lookup<'a>(lang: &str, s: &'a str) -> &'a str { static GERMAN: OnceLock> = OnceLock::new(); static JAPANESE: OnceLock> = OnceLock::new(); @@ -90,6 +94,10 @@ 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 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); } @@ -145,6 +153,7 @@ const CONTEXT: &[(&str, &str, [&str; 5])] = &[ /// Context-sensitive UI captions; unsupported contexts keep the shared translation. pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str { let contexts = match lang { + "hu" => hu::CONTEXT, "uk" => uk::CONTEXT, "ru" => ru::CONTEXT, "cs" => cs::CONTEXT, diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs new file mode 100644 index 000000000..890959761 --- /dev/null +++ b/crates/ui-egui/src/i18n/hu.rs @@ -0,0 +1,1963 @@ +//! 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"), + ("Arrangement", "Elrendezés"), + ("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"), + ("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/Átlátszóság-összeolvasztá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: Canadian", "Angol: kanadai"), + ("English: UK", "Angol: brit"), + ("English: USA", "Angol: amerikai"), + ("Entire Pasteboard", "Teljes munkaterület"), + ("Erase", "Radírozás"), + ("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…"), + ("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"), + ("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:", "Szá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"), + ("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ö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ö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ö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"), + ("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 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"), + ("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ö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. 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. 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. 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. 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."), + ("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í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"), + ("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 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/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"), + ("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/Kiegészítők"), + ("View/Grids & Guides", "Nézet/Rácsok és segédvonalak"), + ("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"), + ("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álasszon 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"), + ("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"), + ("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í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?"), + ("⇧", "⇧"), + ("⌃", "⌃"), + ("⌘", "⌘"), + ("⌥", "⌥"), + ("⌥-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"), ("Columns:", "grid", "Oszlopok:"), ("Group", "selection", "Csoport"), ("Number:", "count", "Darabszám:")]; diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index db13138cf..a954fd376 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| { @@ -61,6 +70,167 @@ fn cached_lookup_preserves_every_translation_across_language_switches() { } } +/// Checks Hungarian catalog integrity, context captions and count substitutions, including +/// English fallback and preservation of user-defined style and workspace names. +#[test] +fn hungarian_catalog_preserves_placeholders_context_and_counts() { + let catalog: HashMap<_, _> = hu::TABLE.iter().copied().collect(); + assert_eq!(catalog.len(), hu::TABLE.len(), "duplicate Hungarian keys"); + let placeholders = |s: &str| { + let mut names: Vec<_> = s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name.to_owned())).collect(); + names.sort(); + names + }; + for (key, translation) in hu::TABLE { + assert!(!translation.trim().is_empty(), "empty {key}"); + assert_eq!(placeholders(key), placeholders(translation), "placeholders: {key}"); + assert_eq!(key.ends_with('…'), translation.ends_with('…'), "ellipsis: {key}"); + } + assert!(!is_rtl("hu")); + assert_eq!(tr("hu", "File"), "Fájl"); + assert_eq!(tr("hu", "Bleed"), "Kifutó"); + assert_eq!(tr("hu", "Arrangement"), "Elrendezés"); + assert_eq!(tr("hu", "Unknown label"), "Unknown label"); + assert_eq!(tr("hu", "Columns"), "Hasábok"); + assert_eq!(tr_context("hu", "Columns", "table"), "Oszlopok"); + assert_eq!(tr("hu", "Columns:"), "Hasábok:"); + assert_eq!(tr_context("hu", "Columns:", "grid"), "Oszlopok:"); + assert_eq!(tr_context("hu", "Columns:", "unknown"), "Hasábok:"); + assert_eq!(tr("hu", "Group"), "Csoportosítás"); + assert_eq!(tr_context("hu", "Group", "selection"), "Csoport"); + assert_eq!(tr_context("hu", "Columns", "unknown"), "Hasábok"); + assert_eq!(tr_context("", "Columns", "table"), "Columns"); + for n in [0, 1, 2, 5, 21] { + assert_eq!(count_label("hu", "pages", n), format!("{n} oldal")); + assert_eq!(count_label("hu", "objects", n), format!("{n} objektum")); + assert_eq!(count_label("hu", "errors", n), format!("{n} hiba")); + assert_eq!( + tr("hu", "{pages} Pages in {spreads} Spreads").replace("{pages}", &n.to_string()).replace("{spreads}", &n.to_string()), + format!("{n} oldal, {n} oldalpáron") + ); + } + assert_eq!(style_name("hu", "[Basic Paragraph]"), "[Alapbekezdés]"); + assert_eq!(style_name("hu", "Saját stílus"), "Saját stílus"); + assert_eq!(workspace_name("hu", "Essentials"), "Alapok"); + assert_eq!(workspace_name("hu", "Saját munkaterület"), "Saját munkaterület"); +} + +/// Checks every Latin catalog letter and all Hungarian accented vowels against the +/// configured bundled UI faces, with system and optional craft-fonts fallbacks disabled. +#[test] +fn hungarian_catalog_glyphs_exist_in_the_bundled_ui_fonts() { + let definitions = crate::theme::font_definitions(&[], "hu"); + let db = designcraft_fonts::FontDb::with_font_dirs(Vec::new()); + db.set_system_fallback(false); + let letters: std::collections::BTreeSet<_> = hu::TABLE + .iter() + .flat_map(|(_, text)| text.chars()) + .chain(hu::CONTEXT.iter().flat_map(|(_, _, text)| text.chars())) + .chain("ÁáÉéÍíÓóÖöŐőÚúÜüŰű".chars()) + .filter(|c| c.is_alphabetic() && *c <= '\u{024f}') + .collect(); + for (name, family, style, ui_family) in [ + ("ui", "Source Sans 3", "Regular", egui::FontFamily::Proportional), + ("ui-semibold", "Source Sans 3", "Semibold", egui::FontFamily::Name("semibold".into())), + ("mono", "JetBrains Mono", "Regular", egui::FontFamily::Monospace), + ] { + assert_eq!(definitions.families[&ui_family].first().map(String::as_str), Some(name)); + let face = db.face(family, style); + assert_eq!(face.data(), definitions.font_data[name].bytes(), "configured UI face {name}"); + for letter in &letters { + assert_ne!(face.glyph_for(*letter), 0, "missing {letter} in {family} {style}"); + } + } + eprintln!( + "Hungarian glyph coverage: {} Latin catalog letters, including ŐőŰű, in Source Sans 3 Regular/Semibold and JetBrains Mono Regular; no system/craft fallback", + letters.len() + ); + if let Some(dir) = std::env::var_os("DESIGNCRAFT_HU_REVIEW_SHOTS") { + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(820.0, 260.0)).build_ui_state( + |ui, ready: &mut bool| { + if !*ready { + ui.ctx().set_fonts(crate::theme::font_definitions(&[], "hu")); + *ready = true; + return; + } + ui.style_mut().visuals = egui::Visuals::light(); + ui.painter().rect_filled(ui.ctx().content_rect(), 0.0, egui::Color32::WHITE); + ui.heading("Magyar ékezetek · ő, ű / Ő, Ű"); + for (name, family) in [ + ("Source Sans 3 Regular", egui::FontFamily::Proportional), + ("Source Sans 3 Semibold", egui::FontFamily::Name("semibold".into())), + ("JetBrains Mono Regular", egui::FontFamily::Monospace), + ] { + ui.label(egui::RichText::new(name).color(egui::Color32::DARK_GRAY)); + ui.label( + egui::RichText::new("Árvíztűrő tükörfúrógép · Őő Űű · ÁÉÍÓÖŐÚÜŰ") + .font(egui::FontId::new(22.0, family)) + .color(egui::Color32::BLACK), + ); + } + }, + false, + ); + h.run_steps(3); + h.render().unwrap().save(std::path::Path::new(&dir).join("hungarian-bundled-ui-fonts.png")).unwrap(); + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.ui.language = "hu".into(); + app.run("file.new", serde_json::json!({"pages": 1})).unwrap(); + app.run( + "frame.create", + serde_json::json!({"rect": [60, 60, 420, 180], "content": "text", "text": "Magyar kiadvány +Árvíztűrő tükörfúrógép +Őő Űű · ÁÉÍÓÖŐÚÜŰ"}), + ) + .unwrap(); + app.run("type.char", serde_json::json!({"attrs": {"fontFamily": "Source Sans 3", "size": 28}})).unwrap(); + app.run("window.panel.activate", serde_json::json!({"panel": "character"})).unwrap(); + let mut interface = crate::test_window::open(app, egui::vec2(1400.0, 900.0)); + interface.render().unwrap().save(std::path::Path::new(&dir).join("designcraft-hungarian-interface.png")).unwrap(); + } +} + +/// Checks translated labels and source-text fallback, plus key uniqueness and nonempty rows +/// in the shared, Arabic, Brazilian Portuguese and Italian catalogs. +#[test] +fn translates_known_strings_and_keeps_the_rest() { + assert_eq!(tr("de", "File"), "Datei"); + assert_eq!(tr("ja", "Swatches"), "スウォッチ"); + assert_eq!(tr("fr", "Something new"), "Something new"); + assert_eq!(tr("", "File"), "File"); + assert_eq!(tr("ar", "File"), "ملف"); + assert_eq!(tr("ar", "Unknown label"), "Unknown label"); + assert_eq!(tr("pt-br", "File"), "Arquivo"); + assert_eq!(tr("pt-br", "New Document…"), "Novo documento…"); + assert_eq!(tr("pt-br", "Unknown label"), "Unknown label"); + assert_eq!(tr("it", "Edit"), "Modifica"); + assert_eq!(tr("it", "New Document…"), "Nuovo documento…"); + assert_eq!(tr("it", "Unknown label"), "Unknown label"); + assert!(is_rtl("ar")); + for (i, (en, translation)) in ar::TABLE.iter().enumerate() { + assert!(ar::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}"); + assert!(!translation.is_empty(), "{en}"); + } + for (i, (en, translation)) in pt_br::TABLE.iter().enumerate() { + assert!(pt_br::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}"); + assert!(!translation.is_empty(), "{en}"); + } + for (i, (en, translation)) in it::TABLE.iter().enumerate() { + assert!(it::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}"); + assert!(!translation.is_empty(), "{en}"); + } + // Italian entries are for keys the shared or Arabic table knows. A new English string + // falls back to English until it is translated, so it doesn't fail here. + for (en, _) in it::TABLE { + assert!(TABLE.iter().any(|(key, _)| key == en) || ar::TABLE.iter().any(|(key, _)| key == en), "it: unknown key {en}"); + } + // Every row is unique and complete. + for (i, (en, t)) in TABLE.iter().enumerate() { + assert!(TABLE[..i].iter().all(|(e, _)| e != en), "duplicate {en}"); + assert!(t.iter().all(|x| !x.is_empty()), "{en}"); + } +} + /// The Data Merge arrangement combo takes its accessible name from `tr(lang, "Arrangement")`. /// A row in only the shared table leaves the languages whose lookup returns directly with the /// English name. @@ -77,6 +247,7 @@ fn arrangement_accessible_name_is_localized_in_every_interface_language() { } } +/// Checks language-specific context precedence and fallback to the plain translation. #[test] fn context_captions_come_from_their_context_or_the_plain_translation() { for (lang, table) in CONTEXT_TABLES { @@ -110,6 +281,38 @@ fn context_captions_come_from_their_context_or_the_plain_translation() { } } +/// Keeps quantity labels distinct from numeric values and checks translated Pages-panel captions. +#[test] +fn a_column_count_and_a_list_number_read_differently() { + for (lang, count, numeral) in [ + ("hu", "Darabszám:", "Szám:"), + ("de", "Anzahl:", "Zahl:"), + ("ja", "数:", "番号:"), + ("uk", "Кількість:", "Номер:"), + ("ar", "العدد:", "الرقم:"), + ("fr", "Nombre :", "Numéro:"), + ("zh", "数量:", "编号:"), + ] { + assert_eq!((tr_context(lang, "Number:", "count"), tr(lang, "Number:")), (count, numeral), "{lang}"); + } + assert_eq!(tr_context("en", "Number:", "count"), "Number:"); + assert_eq!(tr("de", "Gutter:"), "Spaltenabstand:"); + // The Pages panel menu. + for key in [ + "Insert Pages…", + "Move Pages…", + "New Parent…", + "Override All Parent Page Items", + "Remove All Local Overrides", + "Detach All Objects from Parent", + "Create Alternate Layout…", + "Allow Selected Spread to Shuffle", + ] { + assert_ne!(tr("pt-br", key), key); + } + assert_ne!(tr("ar", "Apply Parent to Pages"), "Apply Parent to Pages"); +} + /// The tables `tr` reads, as (language, key, translation), context captions included. fn entries() -> Vec<(&'static str, &'static str, &'static str)> { let mut entries: Vec<_> = LANGUAGE_TABLES.iter().flat_map(|(lang, table)| table.iter().map(|(key, text)| (*lang, *key, *text))).collect(); @@ -263,6 +466,23 @@ fn interface_strings() -> HashSet<&'static str> { keys.extend(crate::panel_docking::DOCK_LABELS.iter().map(|(_, english)| *english)); // UI labels passed to `tr` that no other catalog has yet. keys.extend([ + "180°", + "Create Links When Placing Text Files", + "Delete Character Style…", + "Delete Paragraph Style…", + "Delete the character style \"{name}\"?", + "Delete the paragraph style \"{name}\"?", + "IT", + "Replace with:", + "Show the Typography panel", + "Stroke Behind Fill", + "Stylistic Set", + "The selected fonts have no OpenType features.", + "This story was edited in the layout: you'll be asked before those edits are replaced", + "This style is in use. Text using it will be reassigned to:", + "VA", + "Y:", + "“{}” has been edited in this layout. Updating replaces those edits with the text from the file.", "(any)", "(unchanged)", "[No Language]", 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..4df325e8a 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -575,7 +575,14 @@ 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 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 }); } Ok(Value::Null) } diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index efe978dc5..3fbd56424 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -154,6 +154,7 @@ fn define_lists_command_path_matches_the_type_menu() { assert!(menu_ids("Type").contains(&"list.define")); } +/// Checks that screen-mode menu checkmarks follow the current Normal or Preview mode. #[test] fn screen_mode_shows_the_current_mode() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); @@ -163,6 +164,24 @@ fn screen_mode_shows_the_current_mode() { assert_eq!((mode(&app, "normal"), mode(&app, "preview")), (Some(false), Some(true))); } +/// Checks that translated customization paths match the visible submenu captions, while +/// allowing uncatalogued paths to keep their English fallback. +#[test] +fn hungarian_menu_paths_match_the_visible_submenu_labels() { + // These paths are displayed by menu customization; they must use the same + // captions as the actual menu tree, including nested special characters. + for title in ["Edit", "Type", "View"] { + for path in submenu_paths(title) { + let translated = crate::i18n::tr("hu", &path); + if translated == path { + continue; // New, uncatalogued paths still fall back to English. + } + let visible = path.split('/').map(|part| crate::i18n::tr("hu", part)).collect::>().join("/"); + assert_eq!(translated, visible, "{path}"); + } + } +} + fn walk(items: &[Item], out: &mut Vec<(String, String, Value)>) { for it in items { match it { @@ -648,6 +667,7 @@ fn split_window_contextual_bars_have_independent_widget_ids() { assert!(!bars[0].intersects(bars[1]), "each pane keeps its own bar: {bars:?}"); } +/// Checks the document-grid menu label, shortcut, checkmark and repeated toggle behavior. #[test] fn show_document_grid_toggles_from_the_grids_and_guides_menu() { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); @@ -666,6 +686,8 @@ fn show_document_grid_toggles_from_the_grids_and_guides_menu() { assert!(!app.ui.document_grid); } +/// Checks language selection and rejection of unknown language codes, Preferences dispatch, +/// translated menu labels and preservation of document data when switching to Arabic. #[test] fn interface_language_switches_and_validates() { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); @@ -683,6 +705,113 @@ fn interface_language_switches_and_validates() { run_ui(&mut app, "app.language", &json!({})).unwrap().unwrap(); assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some("preferences")); assert_eq!(app.ui.language, "de"); + app.ui.dialog = None; + + run_ui(&mut app, "app.language", &json!({"lang": "zh"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "zh"); + assert_eq!(crate::i18n::tr(&app.ui.language, "File"), "文件"); + + app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap(); + let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap(); + run_ui(&mut app, "app.language", &json!({"lang": "ar"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "ar"); + assert_eq!(checked(&app, "app.language", &json!({"lang": "ar"})), Some(true)); + assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap()); + for (title, _) in menu_tree() { + assert_ne!(crate::i18n::tr("ar", title), title, "{title}"); + } + // Every menu title has a translation (Japanese never matches the English). + for (title, _) in menu_tree() { + assert_ne!(crate::i18n::tr("ja", title), title, "{title}"); + } + + run_ui(&mut app, "app.language", &json!({"lang": "pt-br"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "pt-br"); + assert_eq!(checked(&app, "app.language", &json!({"lang": "pt-br"})), Some(true)); + for (title, _) in menu_tree() { + // "Layout" is the natural Brazilian Portuguese word, so it stays identical + // to English; every other title must be translated. + if title != "Layout" { + assert_ne!(crate::i18n::tr("pt-br", title), title, "{title}"); + } + } + + run_ui(&mut app, "app.language", &json!({"lang": "it"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "it"); + assert_eq!(checked(&app, "app.language", &json!({"lang": "it"})), Some(true)); + for (title, _) in menu_tree() { + // "File" and "Layout" are the usual Italian menu names, so they stay identical + // to English; every other title must be translated. + if title != "File" && title != "Layout" { + assert_ne!(crate::i18n::tr("it", title), title, "{title}"); + } + } +} + +/// Checks Hungarian selection, menu captions and saved UI preferences without changing +/// document data or translating user-defined names. +#[test] +fn hungarian_language_is_selectable_persisted_and_keeps_document_data() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap(); + app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "text", "text": "My words · Árvíztűrő tükörfúrógép"})).unwrap(); + let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap(); + run_ui(&mut app, "app.language", &json!({"lang": "hu"})).unwrap().unwrap(); + assert_eq!(checked(&app, "app.language", &json!({"lang": "hu"})), Some(true)); + assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap()); + for (title, _) in menu_tree() { + assert_ne!(crate::i18n::tr("hu", title), title, "{title}"); + } + let bytes = serde_json::to_vec(&app.ui).unwrap(); + let restored: crate::UiState = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(restored.language, "hu"); + assert_eq!(crate::i18n::tr(&restored.language, "File"), "Fájl"); + assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name"); + assert!(crate::i18n::LANGUAGES.contains(&("hu", "Magyar")), "Preferences lists Hungarian by its own name"); +} + +/// Checks Ukrainian selection, menu captions and saved UI preferences without changing +/// document data or translating user-defined names. +#[test] +fn ukrainian_language_is_selectable_persisted_and_keeps_document_data() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap(); + app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "text", "text": "My words · Мої слова"})).unwrap(); + let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap(); + run_ui(&mut app, "app.language", &json!({"lang": "uk"})).unwrap().unwrap(); + assert_eq!(checked(&app, "app.language", &json!({"lang": "uk"})), Some(true)); + assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap()); + for (title, _) in menu_tree() { + assert_ne!(crate::i18n::tr("uk", title), title, "{title}"); + } + let bytes = serde_json::to_vec(&app.ui).unwrap(); + let restored: crate::UiState = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(restored.language, "uk"); + assert_eq!(crate::i18n::tr(&restored.language, "File"), "Файл"); + assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name"); + assert!(crate::i18n::LANGUAGES.contains(&("uk", "Українська")), "Preferences lists Ukrainian by its own name"); +} + +/// Checks Russian selection, menu captions and saved UI preferences without changing +/// document data or translating user-defined names. +#[test] +fn russian_language_is_selectable_persisted_and_keeps_document_data() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap(); + app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "text", "text": "My words · Мои слова"})).unwrap(); + let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap(); + run_ui(&mut app, "app.language", &json!({"lang": "ru"})).unwrap().unwrap(); + assert_eq!(checked(&app, "app.language", &json!({"lang": "ru"})), Some(true)); + assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap()); + for (title, _) in menu_tree() { + assert_ne!(crate::i18n::tr("ru", title), title, "{title}"); + } + let bytes = serde_json::to_vec(&app.ui).unwrap(); + let restored: crate::UiState = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(restored.language, "ru"); + assert_eq!(crate::i18n::tr(&restored.language, "File"), "Файл"); + assert_eq!(crate::i18n::tr(&restored.language, "A custom name"), "A custom name"); + assert!(crate::i18n::LANGUAGES.contains(&("ru", "Русский")), "Preferences lists Russian by its own name"); } #[test] @@ -3152,3 +3281,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..e46eea4cb 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|hu} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting", ), ( "app.flattener", @@ -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"), "{}"), diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index ec5b6f67b..d909d6a04 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)); @@ -173,43 +175,77 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { ui.ctx(), |ui| { egui::Frame::popup(ui.style()).show(ui, |ui| { - for tl in g { - ui.horizontal(|ui| { - let (r, row) = ui.allocate_exact_size(vec2(210.0, 24.0), Sense::click()); - if row.hovered() { - ui.painter().rect_filled(r, 2.0, t.hover); - } - icons::paint( - ui.painter(), - egui::Rect::from_min_size(r.min + vec2(4.0, 3.0), vec2(18.0, 18.0)), - tl.icon, - t.icon, - ); - if tl.id == current { - ui.painter().circle_filled(r.min + vec2(-2.0, 12.0), 2.0, t.accent); - } - ui.painter().text( - r.min + vec2(28.0, 12.0), - egui::Align2::LEFT_CENTER, - crate::i18n::tr(&app.ui.language, tl.label), - egui::FontId::proportional(12.5), - t.text, - ); - if let Some(sc) = crate::menus::tool_shortcut_label(app, tl.id) { - ui.painter().text( - r.right_center() - vec2(6.0, 0.0), - egui::Align2::RIGHT_CENTER, - sc, - egui::FontId::proportional(11.5), - t.text_dim, - ); - } - row.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tl.label)); - if row.clicked() { - chosen = Some(tl.id); - } - }); + let row_width = g.iter().fold(210.0_f32, |width, tl| { + let label = crate::i18n::tr(&app.ui.language, tl.label); + let label_width = ui + .painter() + .layout_no_wrap(label.to_owned(), egui::FontId::proportional(12.5), t.text) + .size() + .x; + let shortcut_width = ui + .painter() + .layout_no_wrap( + crate::menus::tool_shortcut_label(app, tl.id).unwrap_or_default(), + egui::FontId::proportional(11.5), + t.text_dim, + ) + .size() + .x; + width.max(label_width + shortcut_width + 46.0) + }); + let max_width = (ui.ctx().content_rect().width() + - egui::Frame::popup(ui.style()).total_margin().sum().x) + .max(0.0); + let mut scroll = + egui::ScrollArea::horizontal().id_salt(("tool-flyout-scroll", gi)).max_width(max_width); + if opened_now { + scroll = scroll.horizontal_scroll_offset(0.0); } + scroll.show(ui, |ui| { + for tl in g { + ui.horizontal(|ui| { + let (r, row) = ui.allocate_exact_size(vec2(row_width, 24.0), Sense::click()); + if row.hovered() { + ui.painter().rect_filled(r, 2.0, t.hover); + } + icons::paint( + ui.painter(), + egui::Rect::from_min_size(r.min + vec2(4.0, 3.0), vec2(18.0, 18.0)), + tl.icon, + t.icon, + ); + if tl.id == current { + ui.painter().circle_filled(r.min + vec2(-2.0, 12.0), 2.0, t.accent); + } + ui.painter().text( + r.min + vec2(28.0, 12.0), + egui::Align2::LEFT_CENTER, + crate::i18n::tr(&app.ui.language, tl.label), + egui::FontId::proportional(12.5), + t.text, + ); + if let Some(sc) = crate::menus::tool_shortcut_label(app, tl.id) { + ui.painter().text( + r.right_center() - vec2(6.0, 0.0), + egui::Align2::RIGHT_CENTER, + sc, + egui::FontId::proportional(11.5), + t.text_dim, + ); + } + row.widget_info(|| { + egui::WidgetInfo::labeled( + egui::Role::Button, + true, + crate::i18n::tr(&app.ui.language, tl.label), + ) + }); + if row.clicked() { + chosen = Some(tl.id); + } + }); + } + }); }); }, ); diff --git a/crates/ui-egui/src/toolbar/tests.rs b/crates/ui-egui/src/toolbar/tests.rs index 7a0948817..df06bd020 100644 --- a/crates/ui-egui/src/toolbar/tests.rs +++ b/crates/ui-egui/src/toolbar/tests.rs @@ -19,10 +19,203 @@ fn column_width(cols: f32) -> f32 { 8.0 + super::BTN * cols + 4.0 * (cols - 1.0) + 8.0 } +/// Returns the bounds of the labeled test widget, failing if the label cannot be found. fn rect(h: &egui_kittest::Harness<'static, test_window::Window>, label: &str) -> egui::Rect { h.get_by_label(label).rect() } +/// Collects painted text bounds and their clip rectangles, including nested shapes. +fn collect_text(shape: &egui::Shape, clip: egui::Rect, texts: &mut Vec<(String, egui::Rect, egui::Rect)>) { + match shape { + egui::Shape::Text(text) => texts.push((text.galley.text().to_owned(), text.visual_bounding_rect(), clip)), + egui::Shape::Vec(shapes) => shapes.iter().for_each(|shape| collect_text(shape, clip, texts)), + _ => {} + } +} + +/// Checks translated tooltips and right-click flyout rows in both Tools-panel layouts, +/// including painted labels, unchanged shortcuts and unclipped, nonoverlapping text. +#[test] +fn hungarian_tool_buttons_translate_every_tool_and_keep_shortcuts() { + use egui::{Event, Modifiers, PointerButton}; + + for double in [false, true] { + let mut app = app(double); + app.ui.language = "hu".into(); + let mut h = test_window::open(app, vec2(1400.0, 1000.0)); + for tool in designcraft_tools::TOOL_GROUPS.iter().flat_map(|group| group.iter()) { + h.state_mut().app.run("tool.select", serde_json::json!({"tool": tool.id})).unwrap(); + h.run_steps(3); + let label = crate::i18n::tr("hu", tool.label); + assert_ne!(label, tool.label, "untranslated tool: {}", tool.id); + let shortcut = tool.shortcut.map(crate::menus::shortcut_text); + assert_eq!(crate::menus::tool_shortcut_label(&h.state().app, tool.id), shortcut, "default binding for {}", tool.id); + let tip = shortcut.map_or_else(|| label.to_owned(), |shortcut| format!("{label} ({shortcut})")); + assert!(rect(&h, &tip).is_finite(), "{tip}"); + } + // A user-assigned chord also has to fit beside a translated flyout label. + let custom_shortcut = "Ctrl+Alt+Shift+F12"; + h.state_mut() + .app + .run( + "window.setShortcut", + serde_json::json!({"id": "tool.select", "params": {"tool": "ellipseFrame"}, "shortcut": custom_shortcut, "steal": true}), + ) + .unwrap(); + for (gi, group) in designcraft_tools::TOOL_GROUPS.iter().enumerate() { + // Single tools have no flyout; the collector group is split into separate + // buttons in two-column mode and is covered by the tooltip checks above. + if group.len() < 2 || (double && designcraft_tools::SPLIT_IN_TWO_COLUMNS.contains(&group[0].id)) { + continue; + } + let first = group[0]; + h.state_mut().app.run("tool.select", serde_json::json!({"tool": first.id})).unwrap(); + h.run_steps(3); + let label = crate::i18n::tr("hu", first.label); + let tip = first.shortcut.map(crate::menus::shortcut_text).map_or_else(|| label.to_owned(), |shortcut| format!("{label} ({shortcut})")); + let at = rect(&h, &tip).center(); + h.input_mut().events.push(Event::PointerMoved(at)); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE }); + } + h.run_steps(3); + assert_eq!(h.state().app.ui.flyout, Some(gi), "right-click opens {} (double={double})", first.id); + let mut texts = Vec::new(); + for shape in &h.output().shapes { + collect_text(&shape.shape, shape.clip_rect, &mut texts); + } + for tool in *group { + let label = crate::i18n::tr("hu", tool.label); + // Flyout accessibility labels omit the shortcut; inspect the painted + // text inside this row so another button cannot satisfy the assertion. + let row = rect(&h, label); + assert!(row.is_finite() && row.is_positive(), "{label}"); + let painted = |expected: &str| { + let (_, bounds, clip) = texts + .iter() + .find(|(text, bounds, _)| text == expected && row.contains(bounds.center())) + .unwrap_or_else(|| panic!("missing flyout text {expected:?} for {} (double={double})", tool.id)); + assert!(row.contains_rect(*bounds) && clip.contains_rect(*bounds), "clipped flyout text {expected:?}: {bounds:?}"); + *bounds + }; + let label_bounds = painted(label); + let binding = if tool.id == "ellipseFrame" { Some(custom_shortcut) } else { tool.shortcut }; + if let Some(shortcut) = binding.map(crate::menus::shortcut_text) { + let shortcut_bounds = painted(&shortcut); + assert!(label_bounds.right() < shortcut_bounds.left(), "label overlaps shortcut for {} (double={double})", tool.id); + } + } + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert!(h.state().app.ui.flyout.is_none()); + } + assert_eq!(crate::i18n::tr("hu", "Direct Selection Tool"), "Közvetlen kijelölő eszköz"); + } +} + +/// Keeps long translated flyout rows reachable by scrolling in a narrow viewport, +/// resetting on reopening while preserving the open scroll position, shortcut and selection. +#[test] +fn hungarian_flyout_scrolls_horizontally_in_narrow_windows() { + use egui::{Event, Modifiers, PointerButton}; + + for double in [false, true] { + for width in [180.0, 240.0] { + let mut app = app(double); + app.ui.language = "hu".into(); + let chord = "Ctrl+Alt+Shift+F12"; + app.run( + "window.setShortcut", + serde_json::json!({"id": "tool.select", "params": {"tool": "ellipseFrame"}, "shortcut": chord, "steal": true}), + ) + .unwrap(); + app.select_tool("rectangleFrame"); + let mut h = test_window::open(app, vec2(width, 1000.0)); + let at = rect(&h, "Téglalapkeret eszköz (F)").center(); + h.input_mut().events.push(Event::PointerMoved(at)); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE }); + } + h.run_steps(3); + let gi = h.state().app.ui.flyout.unwrap(); + let area = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("flyout", gi)))).unwrap(); + let viewport = h.ctx.content_rect(); + assert!(viewport.expand(0.5).contains_rect(area), "flyout {area:?} exceeds viewport {viewport:?}"); + let label = "Ellipsziskeret eszköz"; + let shortcut = crate::menus::shortcut_text(chord); + let painted = |h: &egui_kittest::Harness<'static, test_window::Window>, text: &str| { + let row = rect(h, label); + let mut texts = Vec::new(); + for shape in &h.output().shapes { + collect_text(&shape.shape, shape.clip_rect, &mut texts); + } + let (_, bounds, clip) = texts.into_iter().find(|(t, b, _)| t == text && row.contains(b.center())).unwrap(); + (bounds, clip) + }; + let (label_bounds, label_clip) = painted(&h, label); + assert!(label_clip.contains_rect(label_bounds), "label must be readable at the start"); + let (before, clip) = painted(&h, &shortcut); + let oversized = rect(&h, label).width() > clip.width(); + if width == 180.0 { + assert!(oversized, "the tighter case must exercise horizontal overflow on every platform"); + } + review_shot(&mut h, &format!("flyout-{width}-double-{double}-left.png")); + if oversized { + assert!(!clip.contains_rect(before), "the shortcut starts beyond the narrow viewport"); + wheel(&mut h, area.center(), vec2(-1000.0, 0.0)); + let (after, clip) = painted(&h, &shortcut); + assert!(clip.contains_rect(after), "scrolling must make the complete shortcut readable"); + assert!(after.left() < before.left(), "horizontal input must move the row"); + h.run_steps(3); + let (settled, clip) = painted(&h, &shortcut); + assert!(clip.contains_rect(settled), "an open flyout must retain its horizontal scroll position"); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert!(h.state().app.ui.flyout.is_none()); + // Reopen through the toolbar input path, without changing the stored scroll state. + let at = rect(&h, "Téglalapkeret eszköz (F)").center(); + h.input_mut().events.push(Event::PointerMoved(at)); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { + pos: at, + button: PointerButton::Secondary, + pressed, + modifiers: Modifiers::NONE, + }); + } + h.run_steps(3); + assert_eq!(h.state().app.ui.flyout, Some(gi)); + let (reopened, clip) = painted(&h, label); + assert!(clip.contains_rect(reopened), "a reopened flyout must show the complete translated label"); + assert!((reopened.left() - label_bounds.left()).abs() < 0.5, "reopening must return to the initial left edge"); + review_shot(&mut h, &format!("flyout-{width}-double-{double}-reopened.png")); + wheel(&mut h, area.center(), vec2(-1000.0, 0.0)); + let (rescrolled, clip) = painted(&h, &shortcut); + assert!(clip.contains_rect(rescrolled), "a reopened flyout must still allow scrolling to the shortcut"); + } else { + assert!(clip.contains_rect(before)); + } + review_shot(&mut h, &format!("flyout-{width}-double-{double}-right.png")); + assert_eq!(crate::menus::tool_shortcut_label(&h.state().app, "ellipseFrame"), Some(shortcut)); + let at = rect(&h, label).intersect(area).center(); + h.input_mut().events.push(Event::PointerMoved(at)); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + h.run_steps(3); + assert_eq!(h.state().app.session.tool_id(), "ellipseFrame"); + assert!(h.state().app.ui.flyout.is_none()); + } + } +} + +/// Saves synthetic UI evidence only when the local review explicitly requests it. +fn review_shot(h: &mut egui_kittest::Harness<'static, test_window::Window>, name: &str) { + if let Some(dir) = std::env::var_os("DESIGNCRAFT_HU_REVIEW_SHOTS") { + h.render().unwrap().save(std::path::Path::new(&dir).join(name)).unwrap(); + } +} + /// As InDesign's: a tool group's flyout closes on Esc and on a click outside it (the fill /// swatch, a panel), not only on the canvas. #[test] @@ -197,7 +390,8 @@ fn the_tools_panel_lays_out_like_indesigns() { for double in [false, true] { let h = test_window::open(app(double), vec2(1400.0, 900.0)); let _ = rect(&h, "Gradient Swatch Tool (G)"); - let _ = rect(&h, "Gradient Feather Tool (Shift+G)"); + let feather = format!("Gradient Feather Tool ({})", crate::menus::shortcut_text("Shift+G")); + let _ = rect(&h, &feather); let _ = rect(&h, "View Options"); let split = ["Content Placer Tool (B)", "Apply Color (,)", "Apply Gradient (.)", "Normal"]; for label in split { diff --git a/docs/gaps.md b/docs/gaps.md index 7e78c0eda..c2ca44ad2 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, Greek, German (2006 reform), French and Italian; spelling for +- **Missing:** hyphenation patterns for every language but English, Spanish, Portuguese, Norwegian, Greek, German (2006 reform), French, Italian 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 df015a34b..ca8f1ffe7 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-11-01 · **Change:** minor (Hungarian interface catalog, bundled-font coverage and flyout regression checks) · **Target:** Adobe InDesign 2026 (21.6) Per-language status of the DesignCraft interface and of document typesetting in each script. Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-typography.md), @@ -11,7 +11,7 @@ Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-ty ## How it was measured Interface strings live in `crates/ui-egui/src/i18n/table.rs` (a shared table with German, French, -Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,it,ja,ka,pt_br,uk}.rs`. +Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,de,fr,hu,it,ja,ka,pl,pt_br,ru,uk}.rs`. Untranslated strings fall back to English. A script (2026-10-10) built the set of English interface strings as the union of every catalog's keys, every engine command label and every literal passed to `tr`/`t` in the UI crate: **1,724 strings**. A further ~145 capitalized literals @@ -19,9 +19,11 @@ in the UI crate appear in no catalog (some are test data or proper names), so th somewhat higher and every percentage below is a slight overestimate. Help: DesignCraft has no help content in any language (Help ▸ opens the project's pages). -A missing translation never fails a test, a build or a release. `cargo xtask i18n-report` (also -a step of `cargo xtask ci`, so the list is in every gate log) prints how many interface strings -each language lacks, then the strings. +Missing translations normally fall back to English and are reported without blocking a build. +Targeted regression tests can require specific captions, such as the Data Merge arrangement +label, in every registered interface language. `cargo xtask i18n-report` (also a step of +`cargo xtask ci`, so the list is in every gate log) prints how many interface strings each +language lacks, then the strings. InDesign 2026 ships 25 interface localizations (`.lproj` folders: cs, da, de, el, en, en_GB, es, fi, fr, hu, it, ja, ko, nb, nl, pl, pt, ro, ru, sq, sv, tr, uk, zh_CN, zh_TW); Arabic and Hebrew @@ -52,23 +54,45 @@ caption, table row/column movement and row-height distribution now use the exist catalog. Print, import options and an active table were rendered in Japanese and English with craft-fonts. The language totals above retain their dated measurement; this is a targeted fix. +Hungarian interface (2026-11-01): `hu.rs` contains **1,906 entries and four context-specific +captions**, including all 37 tool names. The initial catalog was prepared with AI assistance +and personally reviewed in the app by **Veress Róbert**, a native Hungarian speaker with over +30 years of DTP experience. Further terminology and in-context feedback is welcome. This is a +partial catalog, not a new coverage measurement against current main: uncatalogued strings +fall back to English. Preferences lists `Magyar`; interface selection does not enable the +separate spelling and hyphenation work in #1484. + +The interface uses bundled **Source Sans 3 Regular** and **Source Sans 3 Semibold**; monospace +labels use **JetBrains Mono Regular** (`assets/fonts/`, registered by `theme::font_definitions`). +`hungarian_catalog_glyphs_exist_in_the_bundled_ui_fonts` checks the actual configured font +bytes, every Latin letter used by the catalog and all Hungarian accented vowels in both cases, +including **ŐőŰű**, with system and optional craft-fonts fallbacks disabled. No new font asset +or font dependency is needed. + +Hungarian regressions cover placeholder/context integrity, formal address and count/column +terminology, language persistence, matching menu paths, translated tool labels and real +right-click flyouts in both Tools-panel layouts. Narrow-window coverage exercises horizontal +scrolling, complete label/shortcut accessibility and selecting a scrolled row at 180 and +240 points; the existing wide-window checks keep their assertions. + ## Other shipped languages | Language | Code | UI strings translated | Status | |---|---|---|---| +| Hungarian | hu | 1,906 entries + 4 context captions (2026-11-01); initial catalog native-reviewed | partial | | Czech | cs | 1,722 (99%), plural categories handled | partial | | Italian | it | 1,464 (85%) | partial | | Ukrainian | uk | 1,432 (83%), plural categories handled | partial | | Polish | pl | 1,717 (99%), count captions category-first; AI-drafted, awaiting native review | partial | | Georgian | ka | 1,724 (~100%: every catalog key plus the command labels the others lack), count captions as `noun: n` | partial (reviewed by a native speaker) | -DesignCraft ships **5** interface languages beyond the twelve; InDesign ships 17 beyond its +DesignCraft ships **6** interface languages beyond the twelve; InDesign ships 17 beyond its eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, tr, uk, zh_TW). ## Document languages InDesign 2026 hyphenates and spell-checks ~40 languages (Hunspell and Duden dictionaries). -DesignCraft hyphenates **English, French, German (2006 reform), Greek, Italian, Norwegian, Portuguese and Spanish** and spell-checks **English** only. Text language +DesignCraft hyphenates **English, Dutch, French, German (2006 reform), Greek, Italian, Norwegian, Portuguese and Spanish** 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). German hyphenation accepts `German: 2006 Reform`, `German: Swiss 2006 Reform` and `de` locale @@ -92,6 +116,7 @@ German variants have no automatic hyphenation; their German shaping and quotes a | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Initial Hungarian interface: 1,906 entries, four context captions, native review, bundled-font glyph coverage and wide/narrow flyout regressions | | 2026-10-11 | minor | Localized Japanese print, import and table controls; checked Japanese and English renders | | 2026-10-14 | minor | Georgian (`ka`) interface added | | 2026-10-10 | major | Created: catalogs measured (1,724 strings), the twelve key languages plus Italian and Ukrainian | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2ac146e1f..4f2dc5ec2 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, Greek and German 2006 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, German 2006, French, Italian 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; Greek uses `hyph-el-monoton.tex` (MIT). German uses hyph-utf8's `hyph-de-1996.tex` 2024-02-28 (MIT), which contains the reformed 2006 patterns despite its filename. Only the explicit 2006 reform names and plain or 2006 German locale codes use this set; `German: Traditional`, `de_DE_1901` and unknown German variants have no automatic hyphenation. No separate old-spelling or Swiss set is bundled. 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) and 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; Greek uses `hyph-el-monoton.tex` (MIT). German uses hyph-utf8's `hyph-de-1996.tex` 2024-02-28 (MIT), which contains the reformed 2006 patterns despite its filename. Only the explicit 2006 reform names and plain or 2006 German locale codes use this set; `German: Traditional`, `de_DE_1901` and unknown German variants have no automatic hyphenation. No separate old-spelling or Swiss set is bundled. 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 9382443aa..3e06b2007 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), Greek (hyph-el-monoton), German 2006 (hyph-de-1996), French (hyph-fr) and Italian (hyph-it); zone, limits, exceptions in the user dictionary; Hyphenate Across Column off keeps automatic hyphens off the last line of a column or frame; discretionary U+00AD breaks with both composers regardless of automatic hyphenation and replaces pattern points within its word (#1955) | ~ | 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), German 2006 (hyph-de-1996), French (hyph-fr), Italian (hyph-it) 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; discretionary U+00AD breaks with both composers regardless of automatic hyphenation and replaces pattern points within its word (#1955) | ~ | 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 | ✓ | ✓ | ✓ | | diff --git a/packaging/linux/flatpak-bundle.sh b/packaging/linux/flatpak-bundle.sh index dbb9b699e..40301a1fb 100755 --- a/packaging/linux/flatpak-bundle.sh +++ b/packaging/linux/flatpak-bundle.sh @@ -20,10 +20,12 @@ APP_ID=ai.storyteller.designcraft FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo TEST=1 +OCI=0 TARBALL="" while [[ $# -gt 0 ]]; do case "$1" in --no-test) TEST=0; shift ;; + --oci) OCI=1; shift ;; -h | --help) sed -n '2,14p' "$0"; exit 0 ;; -*) echo "unknown argument: $1" >&2; exit 2 ;; *) TARBALL="$1"; shift ;; @@ -61,6 +63,12 @@ OUT="$DIST/$BASENAME.flatpak" # --runtime-repo lets `flatpak install` fetch the freedesktop runtime from Flathub if missing. flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable echo "wrote $OUT" +if [[ "$OCI" = 1 ]]; then + OUT_OCI="$DIST/$BASENAME-oci" + rm -rf "$OUT_OCI" + flatpak build-bundle --oci "$WORK/repo" "$OUT_OCI" "$APP_ID" stable + echo "wrote OCI layout to $OUT_OCI" +fi if [[ "$TEST" = 1 ]]; then flatpak install --user -y --noninteractive --reinstall "$OUT" diff --git a/packaging/linux/flatpak-oci.sh b/packaging/linux/flatpak-oci.sh new file mode 100755 index 000000000..4a500a2b8 --- /dev/null +++ b/packaging/linux/flatpak-oci.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Build designcraft as an OCI container layout and push it to GitHub Container Registry (GHCR) +# for consumption by Flatpak OCI clients (flatpak-oci-specs). +# +# Usage: +# packaging/linux/flatpak-oci.sh [--push] [TARBALL] +# +# Environment variables: +# REGISTRY Target registry image (default: ghcr.io/storytold/designcraft) +# TAG Target image tag (default: $VERSION-$ARCH, also tags stable-$ARCH) +# GITHUB_TOKEN Token for GHCR authentication when --push is used +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" + +PUSH=0 +TARBALL="" +while [[ $# -gt 0 ]]; do + case "$1" in + --push) PUSH=1; shift ;; + -h | --help) sed -n '2,12p' "$0"; exit 0 ;; + -*) echo "unknown argument: $1" >&2; exit 2 ;; + *) TARBALL="$1"; shift ;; + esac +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) ;; + aarch64 | arm64) ARCH=aarch64 ;; + *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; +esac + +BASENAME="designcraft-$VERSION-linux-$ARCH" +OUT_OCI="$DIST/$BASENAME-oci" + +echo "==> Building Flatpak OCI layout for $BASENAME" +"$HERE/flatpak-bundle.sh" --oci --no-test "${TARBALL:-}" + +[[ -d "$OUT_OCI" ]] || { echo "error: $OUT_OCI layout not generated" >&2; exit 1; } + +REGISTRY="${REGISTRY:-ghcr.io/storytold/designcraft}" +TAG="${TAG:-$VERSION-$ARCH}" + +echo "==> OCI layout ready at $OUT_OCI (Target: $REGISTRY:$TAG)" + +if [[ "$PUSH" = 1 ]]; then + command -v skopeo >/dev/null || { echo "error: skopeo is required to push OCI images (apt install skopeo)" >&2; exit 1; } + + AUTH_ARGS=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + AUTH_ARGS=(--dest-creds "${GITHUB_ACTOR:-storytold}:$GITHUB_TOKEN") + fi + + echo "==> Pushing to $REGISTRY:$TAG" + skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:$TAG" + + # Also update architecture-specific stable tag + echo "==> Pushing to $REGISTRY:stable-$ARCH" + skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:stable-$ARCH" + + echo "==> Push complete" +fi 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