From 1d459796ad67d3f49330fd9a622ad097e41b1546 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 00:28:25 -0800 Subject: [PATCH] i18n: term checks match whole words and the Info row's Tracking label The Leading check now matches each word of the Leading field as a whole word in languages that separate words with spaces, so a longer word such as "liderlik" no longer stands in for "lider". Chinese, Japanese and other unspaced scripts keep substring matching. The Info row check now looks for the whole Tracking name in the label in front of {tracking}, after taking out a Kerning or Leading name that contains it. A word of the name found in the Size label, or a Kerning label that contains the Tracking name, no longer passes. The Czech Auto Leading row uses the singular "proklad", as the Leading field does. --- crates/ui-egui/src/i18n/cs.tsv | 2 +- crates/ui-egui/src/i18n/tests.rs | 54 +++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 67d5709b77..3d287457e5 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5657,7 +5657,7 @@ mark-style Japanese Japonské Word Spacing: Mezery mezi slovy: Letter Spacing: Mezery mezi písmeny: Glyph Scaling: Změna velikosti glyfů: - Auto Leading: Automatické proklady: + Auto Leading: Automatický proklad: Single Word Justification: Zarovnání jednoho slova: Full Justify Zarovnat do bloku Minimum must not exceed Desired, nor Desired Maximum. Minimum nesmí překročit požadovanou hodnotu ani požadovaná hodnota maximum. diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index bb2de38d61..2fe5cb4aac 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -524,9 +524,36 @@ fn zh_hans_is_simplified_and_mainland() { assert!(bad.is_empty(), "in zh-hans.tsv:\n{}", bad.join("\n")); } +/// Whether `text` holds `term`. In a script that separates words with spaces the term must stand as +/// whole words, so "liderlik" does not hold "lider"; in Chinese, Japanese, Thai, Lao, Khmer and +/// Myanmar text any occurrence counts. +fn contains_term(text: &str, term: &str) -> bool { + let unspaced = |c: char| matches!(c as u32, 0x0E00..=0x0EFF | 0x1000..=0x109F | 0x1780..=0x17FF | 0x3040..=0x30FF | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF); + if term.chars().any(unspaced) { + return text.contains(term); + } + text.match_indices(term).any(|(at, m)| { + !text[..at].chars().next_back().is_some_and(char::is_alphanumeric) && !text[at + m.len()..].chars().next().is_some_and(char::is_alphanumeric) + }) +} + +#[test] +fn contains_term_matches_whole_words_where_words_are_spaced() { + assert!(contains_term("tepeden tepeye lider", "lider")); + assert!(contains_term("boyut/lider:", "lider")); + assert!(contains_term("réinitialiser l’approche", "approche")); + assert!(!contains_term("tepeden tepeye liderlik", "lider")); + assert!(!contains_term("automatické proklady:", "proklad")); + assert!(contains_term("kích thước: giãn chữ:", "giãn chữ")); + assert!(!contains_term("kích thước chữ: giãn:", "giãn chữ")); + assert!(contains_term("自動行送り:", "行送り")); + assert!(contains_term("大小:字距微調:", "字距")); +} + /// Each catalog names leading with one term: every word of the Character panel's Leading field -/// (the text before " (", ignoring case) appears in the Auto Leading, Size/Leading, Top-to-Top -/// Leading and Bottom-to-Bottom Leading rows outside their placeholders. +/// (the text before " (", ignoring case) appears as a whole word (see [`contains_term`]) in the +/// Auto Leading, Size/Leading, Top-to-Top Leading and Bottom-to-Bottom Leading rows outside their +/// placeholders. /// A catalog without the Leading row, or without one of the others, is skipped for that check. #[test] fn leading_term_agrees_within_each_catalog() { @@ -540,7 +567,7 @@ fn leading_term_agrees_within_each_catalog() { for src in ["Auto Leading:", "Size/Leading", "Top-to-Top Leading", "Bottom-to-Bottom Leading"] { if let Some(text) = row(src) { let bare = placeholders(&text).iter().fold(text.clone(), |t, p| t.replace(&format!("{{{p}}}"), " ")); - for word in leading.split_whitespace().filter(|w| !bare.contains(*w)) { + for word in leading.split_whitespace().filter(|w| !contains_term(&bare, w)) { bad.push(format!("{}: {src:?} → {text:?} lacks {word:?} from the Leading field {leading:?}", l.code)); } } @@ -576,10 +603,12 @@ fn no_translated_word_mixes_latin_cyrillic_and_greek_letters() { } /// Each catalog gives the Character panel's Kerning, Leading and Tracking fields three different -/// names, every word of the Tracking and Kerning names appears in the Reset Tracking and Kerning -/// row, and every word of the Tracking name appears in the Info panel's type line outside its -/// placeholders (ignoring case). Each word is matched once, the longer name first, so a name -/// that contains the other name does not count for both. +/// names, and every word of the Tracking and Kerning names appears in the Reset Tracking and +/// Kerning row (ignoring case). Each word is matched once, the longer name first, so a name that +/// contains the other name does not count for both. The Info panel's type line labels +/// `{tracking}` with the whole Tracking name: the text between the placeholder before it and +/// `{tracking}` holds the name (see [`contains_term`]) once any Kerning or Leading name that +/// contains it has been taken out. /// A catalog without one of these rows is skipped for the checks that need it. #[test] fn character_panel_terms_agree_within_each_catalog() { @@ -613,7 +642,16 @@ fn character_panel_terms_agree_within_each_catalog() { } }; uses("Reset Tracking and Kerning", &[&tracking, &kerning]); - uses("Size: {size} Tracking: {tracking}", &[&tracking]); + if let (Some(text), Some(tracking)) = (row("Size: {size} Tracking: {tracking}"), &tracking) { + let before = text.split("{tracking}").next().unwrap_or_default(); + let mut label = before.rsplit('}').next().unwrap_or_default().to_string(); + for other in [&kerning, &leading].into_iter().flatten().filter(|o| *o != tracking && o.contains(tracking.as_str())) { + label = label.replace(other.as_str(), " "); + } + if !contains_term(&label, tracking) { + bad.push(format!("{}: the Info row {text:?} labels {{tracking}} {label:?}, not the field {tracking:?}", l.code)); + } + } } assert!(bad.is_empty(), "Character panel terms that disagree:\n{}", bad.join("\n")); }