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")); }