collective

Browse / github.com/storytold/designcraft / patch

Hyphenation: French and Italian, and words with an elision

Hyphenation: French and Italian, and words with an elision ## Gap InDesign hyphenates text in the language set on it (Character panel › Language), with a dictionary for each of about 40 languages. DesignCraft had patterns for English, Spanish, Portuguese, Norwegian and Greek only. French and Italian text wasn't hyphenated at all; a test asserted it, and gap 6 in `docs/gaps.md` listed it. Open PRs already add German (#1254), Dutch (#1147), Russian (#465), Hungarian (#1484) and Esperanto (#1168). This PR doesn't touch those languages. ## Change - **French and Italian patterns.** `designcraft-compose` enables hypher's `french` and `italian` features.

Status
open
Score
0.00
Adopters
0 other operators
Author
moppeeer (imported from pull request #1566, open)
Base
d4c1322d97bf0ea28cfbaf35a626f5dfc75428f0
Size
+92 −14 in 8 files
Review
pending
Shared
2026-10-28
Id
7c489f1bd85021f7e6cd4e91

How it went

No other operator has reported using it yet.

Diff

raw · NOTICE ROADMAP.md crates/compose/Cargo.toml crates/compose/src/hyphen/mod.rs crates/compose/src/tests.rs docs/gaps.md docs/localization-parity.md docs/typography-parity.md

diff --git a/NOTICE b/NOTICE--- a/NOTICE+++ b/NOTICE@@ -68,6 +68,34 @@ list (author, source, license) is in ASSETS.md.   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.+- French hyphenation patterns, embedded through the hypher crate (feature "french"):+  hyph-fr.tex 2.13 from hyph-utf8, https://github.com/hyphenation/tex-hyphen.+  Copyright (C) 1994-2002 Daniel Flipo, Bernard Gaulle, 2016 Arthur Reutenauer. 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.+- Italian hyphenation patterns, embedded through the hypher crate (feature "italian"):+  hyph-it.tex 4.9 from hyph-utf8, https://github.com/hyphenation/tex-hyphen.+  Copyright (C) 2008-2011 Claudio Beccari. Available under the LPPL (1.3 or later) or the MIT+  License; taken here under the 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. - Norwegian hyphenation patterns, embedded through the hypher crate (feature "norwegian"):   hyph-no.tex from hyph-utf8, https://github.com/hyphenation/tex-hyphen.   Copyright (C) Rune Kleveland and Ole Michael Selberg. Copying and distribution of this file,diff --git a/ROADMAP.md b/ROADMAP.md--- a/ROADMAP.md+++ b/ROADMAP.md@@ -369,6 +369,7 @@ Newest first. - **2026-10-22** · Hyphenation Settings, as in InDesign: the Paragraph panel menu's Hyphenation… (`app.hyphenationDialog`) sets the selected paragraphs' Hyphenate, Words with at Least, After First, Before Last, Hyphen Limit, Hyphenation Zone, the Better Spacing ↔ Fewer Hyphens slider (InDesign's 0–10 steps), Hyphenate Capitalized Words, Last Word and Across Column, with Preview, as one undo step; Paragraph Style Options › Hyphenation has the same form. The panel menu lists its items in InDesign's order. Replicated from a hyphenation tutorial: the same text in Minion Pro breaks into the same lines as InDesign's with hyphenation on, off, and at Fewer Hyphens. No readiness percentage change. - **2026-10-22** · Hyphenate Across Column (Hyphenation Settings; IDML HyphenateAcrossColumns) is applied: with it off, a paragraph whose line ending in an automatic hyphen would end a column or frame is re-composed without that hyphenation point, so the word moves on whole, as in InDesign. Opened and imported files keep their own setting, so their layouts don't change. No readiness percentage change. - **2026-10-22** · The Paragraph panel is laid out as InDesign's: the nine alignment buttons in groups; indent, spacing and drop cap fields by their icons in two columns, with Space Between Paragraphs Using Same Style (Ignore or a space); Shading and Border with the Paragraph Borders and Shading button and colour; Hyphenate with Do Not Align / Align to Baseline Grid. Its panel menu follows InDesign's order: Paragraph and Single-line Composer, Only Align First Line to Grid, Balance Ragged Lines, Optical Margin Alignment, Justification…, Keep Options…, Span Columns…, Hyphenation…, Drop Caps and Nested Styles…, GREP Styles… (these three now open for the selected paragraphs), Paragraph Rules…, Paragraph Borders and Shading…, Bullets and Numbering…, Restart/Continue Numbering, Define Lists… and the paragraph direction. Convert Bullets and Numbering to Text is listed but not available yet. No readiness percentage change.+- **2026-10-22** · French and Italian text is hyphenated, with hyph-utf8's patterns through hypher (hyph-fr 2.13 and hyph-it 4.9, MIT): for the Language set in the Character panel (French, French: Canadian, French: Swiss, Italian, or fr / it locale codes). A word with an elision hyphenates after its apostrophe (l'im-pri-me-rie, dell'im-pa-gi-na-zio-ne), in every patterned language. Gap 6 shrinks. No readiness percentage change. - **2026-10-22** · A ruler guide dragged with the Selection tools, out of a ruler, on its own or with other selected guides, snaps along its axis as objects do (#1221): to other ruler guides, margins, columns, bleed, page and object edges and centers (with the smart-guide mark) and the document grid, within the snap zone and following Snap to Guides and Smart Guides. Shift snaps to the ruler's ticks and Cmd suspends snapping. The preview line, the position readout and the drop use the same snapped position. No readiness percentage change. - **2026-10-22** · macOS: the app menu has a Language submenu after Settings… with each interface language in its own name and the current one checked, as in the suite's other apps; Preferences › General › Interface Language stays. No readiness percentage change. - **2026-10-22** · macOS menu bar: the menus keep working after the interface language or Show CJK Features changes (a rebuilt menu bar lost every click, Quit included), and its key equivalents are the shortcuts in effect, so a shortcut moved in the Keyboard Shortcuts editor leaves its old menu item. No readiness percentage change.diff --git a/crates/compose/Cargo.toml b/crates/compose/Cargo.toml--- a/crates/compose/Cargo.toml+++ b/crates/compose/Cargo.toml@@ -21,7 +21,7 @@ log = { workspace = true } regex = { workspace = true } fancy-regex = { workspace = true } miniz_oxide = "0.8"-hypher = { version = "0.1.8", default-features = false, features = ["alloc", "greek", "portuguese", "spanish", "norwegian"] }+hypher = { version = "0.1.8", default-features = false, features = ["alloc", "french", "greek", "italian", "portuguese", "spanish", "norwegian"] }  [dev-dependencies] designcraft-fonts = { workspace = true, features = ["testing"] }diff --git a/crates/compose/src/hyphen/mod.rs b/crates/compose/src/hyphen/mod.rs--- a/crates/compose/src/hyphen/mod.rs+++ b/crates/compose/src/hyphen/mod.rs@@ -5,7 +5,10 @@ //! `hyph-no.tex` from hyph-utf8 (Rune Kleveland, Ole Michael Selberg), permissive (see NOTICE); //! Greek is `hyph-el-monoton.tex` 5.0 from hyph-utf8 (Dimitrios Filippou), LPPL or MIT, taken //! under MIT. Those are the **monotonic** patterns, which is what `hypher` maps `el` to, so-//! polytonic text hyphenates by them as well.+//! 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+//! (Claudio Beccari), LPPL or MIT, taken under MIT. A word with an elision (`l'imprimerie`)+//! hyphenates after its apostrophe. //! //! English lookup order for each word: //! 1. the **dictionary** ([`Dictionary::en_us`]) — the public-domain Moby Hyphenator II word list@@ -119,8 +122,11 @@ fn core_breaks(core: &[char], lang: Lang) -> (Vec<usize>, Source) {         return (vec![], Source::None);     }     if let Lang::Patterns(l) = lang {-        if core.contains(&'\'') {-            return (vec![], Source::None);+        // An elision (French "l'", Italian "dell'"): the word after the apostrophe hyphenates,+        // with no break up to it.+        if let Some(ap) = core.iter().rposition(|&c| c == '\'') {+            let (pts, src) = if ap + 1 < n { core_breaks(&core[ap + 1..], lang) } else { (vec![], Source::None) };+            return (pts.into_iter().map(|p| p + ap + 1).collect(), src);         }         let s: String = core.iter().collect();         let mut out = Vec::new();@@ -393,7 +399,7 @@ mod tests {         assert_eq!(h("desesperación"), "de-ses-pe-ra-ción");         assert_eq!(h("teatro"), "tea-tro", "a vowel run stays whole");         assert_eq!(h("calle"), "ca-lle", "ll stays whole");-        assert!(Lang::for_language("French").is_none(), "only languages built in");+        assert!(Lang::for_language("Swedish").is_none(), "only languages built in");     }      #[test]@@ -435,6 +441,41 @@ mod tests {         assert_eq!(h("beveger"), "be-ve-ger");     } +    /// A word with an elision ("l'", "dell'") hyphenates as the word after it.+    fn marked(w: &str, l: Lang) -> String {+        let pts = hyphen_points_in(w, &Limits::default(), l);+        w.chars().enumerate().map(|(i, c)| if pts.contains(&i) { format!("-{c}") } else { c.to_string() }).collect()+    }++    #[test]+    fn french_uses_the_hyph_fr_patterns() {+        let fr = Lang::for_language("French").unwrap();+        for name in ["fr", "fr_FR", "French: Canadian", "French: Swiss"] {+            assert_eq!(Lang::for_language(name), Some(fr), "{name}");+        }+        assert_eq!(marked("hyphénation", fr), "hy-phé-na-tion");+        assert_eq!(marked("typographie", fr), "ty-po-gra-phie");+        assert_eq!(marked("Développement", fr), "Dé-ve-lop-pe-ment");+        assert_eq!(marked("anticonstitutionnellement", fr), "an-ti-cons-ti-tu-tion-nel-le-ment");+        assert_eq!(marked("paragraphe", fr), "pa-ra-graphe", "no break before a final mute syllable of two letters");+        assert_eq!(marked("l'imprimerie", fr), "l'im-pri-me-rie", "after an elision");+        assert_eq!(marked("l’imprimerie", fr), "l’im-pri-me-rie", "a typographer's apostrophe too");+    }++    #[test]+    fn italian_uses_the_hyph_it_patterns() {+        let it = Lang::for_language("Italian").unwrap();+        for name in ["it", "it_IT"] {+            assert_eq!(Lang::for_language(name), Some(it), "{name}");+        }+        assert_ne!(it, Lang::for_language("French").unwrap());+        assert_eq!(marked("sillabazione", it), "sil-la-ba-zio-ne");+        assert_eq!(marked("tipografia", it), "ti-po-gra-fia");+        assert_eq!(marked("Repubblica", it), "Re-pub-bli-ca");+        assert_eq!(marked("impaginazione", it), "im-pa-gi-na-zio-ne");+        assert_eq!(marked("dell'impaginazione", it), "dell'im-pa-gi-na-zio-ne", "after an elision");+    }+     #[test]     fn heuristic_fallback() {         let c: Vec<char> = "happen".chars().collect();diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs--- a/crates/compose/src/tests.rs+++ b/crates/compose/src/tests.rs@@ -2728,11 +2728,18 @@ fn only_english_text_gets_english_hyphenation() {     let hyphenated = |d: &Document, sid| all_lines(&compose_story(d, sid, &ComposeOptions::default())).iter().filter(|l| l.hyphenated).count();     let (d, sid, _) = doc_with(text, narrow, ParaAttrs::default());     assert!(hyphenated(&d, sid) > 0, "English hyphenates");-    let mut d2 = d.clone();-    let st = d2.story_mut(sid).unwrap();-    let n = st.len();-    st.format_chars(0..n, |f| f.over.language = Some("French".into()));-    assert_eq!(hyphenated(&d2, sid), 0, "French isn't hyphenated with English rules");+    // A language without bundled patterns isn't hyphenated with English rules.+    let in_language = |lang: &str| {+        let mut d2 = d.clone();+        let st = d2.story_mut(sid).unwrap();+        let n = st.len();+        st.format_chars(0..n, |f| f.over.language = Some(lang.into()));+        hyphenated(&d2, sid)+    };+    assert_eq!(in_language("Swedish"), 0, "Swedish isn't hyphenated with English rules");+    // French and Italian hyphenate with their own patterns.+    assert!(in_language("French") > 0, "French hyphenates (hyph-fr)");+    assert!(in_language("Italian") > 0, "Italian hyphenates (hyph-it)"); }  // A text frame's bottom inset constrains its last baseline. The full font descentdiff --git a/docs/gaps.md b/docs/gaps.md--- a/docs/gaps.md+++ b/docs/gaps.md@@ -155,7 +155,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · - **Estimate:** 25–40 h.  ### 13. Hyphenation and spelling languages (T, L)-- **Missing:** hyphenation patterns for every language but English, Spanish, Portuguese, Norwegian and Greek; spelling for+- **Missing:** hyphenation patterns for every language but English, Spanish, Portuguese, Norwegian, Greek, French and Italian; 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--- a/docs/localization-parity.md+++ b/docs/localization-parity.md@@ -36,7 +36,7 @@ come as Middle East editions with an English or French interface and right-to-le | Spanish | es | ✓ | 1,438 (83%) | menus, panels, most dialogs; no help | Latin ✓; Spanish hyphenation ✓ (#222) | no | partial | 5–8 | | Hindi | hi | ✗ | 0 (0%) | none | Devanagari shaped by harfrust in documents but untested; no Hindi hyphenation or spelling | no | none | 20–30 | | Arabic | ar | Middle East edition (English/French UI) | 1,459 (85%) | menus, panels, most dialogs; no help | RTL UI text (`rtl.rs`: bidi + shaping); Arabic composition, kashida, digits, RTL stories and tables ✓ (arabic-typography.md); list bullets and multi-column RTL open (#100) | no | partial | 15–25 |-| French | fr | ✓ | 1,438 (83%) | menus, panels, most dialogs; no help | Latin ✓; no French hyphenation | no | partial | 6–10 |+| French | fr | ✓ | 1,438 (83%) | menus, panels, most dialogs; no help | Latin ✓; French hyphenation ✓ (hyph-fr) | no | partial | 6–10 | | Portuguese (Brazil) | pt-br | ✓ (pt) | 1,057 (61%) | menus, panels, some dialogs; no help | Latin ✓; Portuguese hyphenation ✓ | no | partial | 8–12 | | Indonesian | id | ✗ | 0 (0%) | none | Latin ✓ | no | none | 12–18 | | Japanese | ja | ✓ | 1,644 (95%) | whole interface (#272), vertical-type commands; no help | vertical text, kinsoku, aki, ruby, warichu ✓ (partial depth, cjk.md); canvas IME ✓, not checked by hand (#322); Japanese UI and Mincho fonts from craft-fonts | no | partial | 6–10 (+ IME checks, shared with zh) |@@ -68,7 +68,7 @@ eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, ## Document languages  InDesign 2026 hyphenates and spell-checks ~40 languages (Hunspell and Duden dictionaries).-DesignCraft hyphenates **English, Spanish, Portuguese and Norwegian** and spell-checks **English** only. Text language+DesignCraft hyphenates **English, French, 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). @@ -94,3 +94,4 @@ locale shaping. See [typography-parity.md](typography-parity.md). | 2026-10-10 | major | Created: catalogs measured (1,724 strings), the twelve key languages plus Italian and Ukrainian | | 2026-10-11 | minor | Czech (cs) added: 1,722 strings, plural categories handled | | 2026-10-13 | minor | Polish interface added (1,717 strings) |+| 2026-10-22 | minor | French and Italian hyphenation (hyph-fr, hyph-it): the French row and Document languages updated |diff --git a/docs/typography-parity.md b/docs/typography-parity.md--- a/docs/typography-parity.md+++ b/docs/typography-parity.md@@ -36,7 +36,7 @@ Fidelity: **unmeasured** unless stated. | Single-line Composer | greedy | greedy with the same ranges | ✓ | | | World-Ready composers | Paragraph / Single-line World-Ready | RTL, Arabic and bidi in the main composer | ~ | Indic scripts (Devanagari etc.) shape through harfrust but are untested | | Justification | word/letter spacing, glyph scaling min/desired/max, single-word, last line | all of these | ✓ | a justified line with a tab justifies after the tab (#284); set per paragraph in Type › Justification… and the Paragraph panel menu (#531) |-| Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list), Spanish (Bezos), Portuguese (hyph-pt), Norwegian (hyph-no) and Greek (hyph-el-monoton); zone, limits, exceptions in the user dictionary; Hyphenate Across Column off keeps automatic hyphens off the last line of a column or frame | ~ | other languages don't hyphenate (#87 reported "doesn't work") |+| Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list), Spanish (Bezos), Portuguese (hyph-pt), Norwegian (hyph-no), Greek (hyph-el-monoton), French (hyph-fr) and Italian (hyph-it), hyphenating after an elision (l'im-pri-me-rie); zone, limits, exceptions in the user dictionary; Hyphenate Across Column off keeps automatic hyphens off the last line of a column or frame | ~ | other languages don't hyphenate (#87 reported "doesn't work") | | Spelling | per-language dictionaries | English (Moby) + document/user dictionary | ~ | | | Kerning | Metrics, Optical, manual, none | Metrics, Optical (outline-based: pairs pulled towards the font's stem gap, `crates/compose/src/optical.rs`), manual, none | ~ | Optical is an approximation of Adobe's algorithm (#181), not a match: imported display text can still break differently | | Tracking, scale, skew, baseline shift | ✓ | ✓ | ✓ | |