mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
Merge origin/main into spanish-proofing
Resolve the docs/localization-parity.md status-line conflict, keeping both changes.
This commit is contained in:
@@ -4259,11 +4259,34 @@ mod tests {
|
||||
#[test]
|
||||
fn command_search_respects_turkish_i_pairs_and_keeps_canonical_matches() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
// Turkish casing follows the language code; the labels are fixtures with both I pairs.
|
||||
const LABELS: &str =
|
||||
"\tItalic\tİtalik\n\tHome\tGiriş\n\tFont\tYazı Tipi\n\tTable of Contents\tİçindekiler Tablosu\n\tReferences\tBaşvurular\n";
|
||||
let reg = app_with("").session.registry.clone();
|
||||
let italic = reg.get("format.italic").unwrap();
|
||||
let table = reg.get("insert.table").unwrap();
|
||||
let toc = reg.all().iter().find(|c| c.label == "Table of Contents" && c.location == "References › Table of Contents").unwrap();
|
||||
crate::i18n::set_current(crate::i18n::fixture_lang("tr", LABELS));
|
||||
for query in ["italik", "İTALİK", "YAZI TİPİ", "FORMAT.ITALIC", "ITALIC"] {
|
||||
assert!(command_matches(italic, query), "tr: {query}");
|
||||
}
|
||||
for query in ["içindekiler", "TABLE OF CONTENTS"] {
|
||||
assert!(command_matches(toc, query), "tr: {query}");
|
||||
}
|
||||
for query in ["INSERT", "INSERT.TABLE"] {
|
||||
assert!(command_matches(table, query), "tr: {query}");
|
||||
}
|
||||
assert_eq!(crate::i18n::lowercase("Iİıi"), "ıiıi");
|
||||
// The same labels under another language's code use Unicode's default casing.
|
||||
crate::i18n::set_current(crate::i18n::fixture_lang("x-fixture", LABELS));
|
||||
assert!(!command_matches(italic, "italik"), "İ lowercases to i plus a combining dot");
|
||||
assert_eq!(crate::i18n::lowercase("Iİıi"), "ii\u{307}ıi");
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
|
||||
let mut fonts = false;
|
||||
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|
||||
move |ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
crate::i18n::set_current(crate::i18n::Lang::from_pref(&app.ui.language));
|
||||
if fonts {
|
||||
show(app, &ctx);
|
||||
} else {
|
||||
@@ -4274,36 +4297,18 @@ mod tests {
|
||||
app_with("Unchanged document"),
|
||||
);
|
||||
let before = serde_json::to_value(&h.state().session.doc).unwrap();
|
||||
for (language, query, expected) in [
|
||||
("tr", "italik", "İtalik — Giriş › Yazı Tipi"),
|
||||
("tr", "İTALİK", "İtalik — Giriş › Yazı Tipi"),
|
||||
("tr", "içindekiler", "İçindekiler Tablosu — Başvurular › İçindekiler Tablosu"),
|
||||
("tr", "YAZI TİPİ", "İtalik — Giriş › Yazı Tipi"),
|
||||
// Dozens of commands match a lone INSERT and the box shows 14, in registry order: the
|
||||
// witness is an early one; Table is checked below, past the box's limit.
|
||||
("tr", "INSERT", "Bölünemez Boşluk — Ekle › Semboller"),
|
||||
("tr", "INSERT.TABLE", "Tablo — Ekle › Tablolar"),
|
||||
("tr", "FORMAT.ITALIC", "İtalik — Giriş › Yazı Tipi"),
|
||||
("tr", "TABLE OF CONTENTS", "İçindekiler Tablosu — Başvurular › İçindekiler Tablosu"),
|
||||
("en", "ITALIC", "Italic — Home › Font"),
|
||||
("en", "FORMAT.ITALIC", "Italic — Home › Font"),
|
||||
("en", "INSERT.TABLE", "Table — Insert › Tables"),
|
||||
] {
|
||||
h.state_mut().ui.language = language.into();
|
||||
for (query, expected) in
|
||||
[("ITALIC", "Italic — Home › Font"), ("FORMAT.ITALIC", "Italic — Home › Font"), ("INSERT.TABLE", "Table — Insert › Tables")]
|
||||
{
|
||||
h.state_mut().dialog = Some(Dialog::Commands { query: query.into(), swallow: false });
|
||||
h.run_steps(4);
|
||||
assert!(
|
||||
h.query(egui_kittest::kittest::By::new().role(egui::accesskit::Role::Button).label_contains(expected)).is_some(),
|
||||
"{language}: {query}\n{:?}",
|
||||
"{query}\n{:?}",
|
||||
h.query_all_by_role(egui::accesskit::Role::Button).collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(crate::i18n::lowercase("Iİıi"), if language == "tr" { "ıiıi" } else { "ii\u{307}ıi" });
|
||||
}
|
||||
assert_eq!(serde_json::to_value(&h.state().session.doc).unwrap(), before, "search does not change the document");
|
||||
crate::i18n::set_current(crate::i18n::Lang::from_pref("tr"));
|
||||
let reg = h.state().session.registry.clone();
|
||||
assert!(command_matches(reg.get("insert.table").unwrap(), "INSERT"), "tr: INSERT finds Table");
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
}
|
||||
|
||||
/// #2986: a box that keeps the keyboard asks for focus only while it lacks it. `request_focus`
|
||||
|
||||
@@ -1907,8 +1907,9 @@ mod tests {
|
||||
assert_eq!(t.rows.iter().map(|r| r.cells.len()).collect::<Vec<_>>(), [3, 3, 3, 3]);
|
||||
}
|
||||
|
||||
/// The envelope sizes are in the interface language with the region's decimal separator
|
||||
/// (#1128; the untranslated-text audit found `#10 4.13 × 9.5 in` drawn in English).
|
||||
/// The envelope sizes go through the interface language's catalog and use the region's
|
||||
/// decimal separator (#1128; the untranslated-text audit found `#10 4.13 × 9.5 in` drawn in
|
||||
/// English).
|
||||
#[test]
|
||||
fn envelope_sizes_follow_the_interface_language_and_region() {
|
||||
let size = |name: &str| ENVELOPE_SIZES.iter().find(|s| s.0 == name).map(|(n, w, h)| envelope_label(n, *w, *h)).unwrap();
|
||||
@@ -1917,10 +1918,11 @@ mod tests {
|
||||
let mut a = app();
|
||||
a.run("file.options", json!({"region": "it-IT"})).unwrap();
|
||||
crate::options::sync(&a);
|
||||
crate::i18n::set_current(crate::i18n::Lang::from_code("it").unwrap());
|
||||
assert_eq!(size("Envelope #10"), "#10 4,13 × 9,5 pollici");
|
||||
assert_eq!(size("Envelope DL"), "DL 110 × 220 mm");
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
assert_eq!(size("Envelope #10"), "#10 4,13 × 9,5 in");
|
||||
crate::i18n::pseudo::set(true);
|
||||
assert_eq!(size("Envelope #10"), "⟦#10 4,13 × 9,5 in⟧");
|
||||
assert_eq!(size("Envelope DL"), "⟦DL 110 × 220 mm⟧");
|
||||
crate::i18n::pseudo::set(false);
|
||||
a.run("file.options", json!({"region": "en-US"})).unwrap();
|
||||
crate::options::sync(&a);
|
||||
}
|
||||
|
||||
@@ -183,10 +183,11 @@ impl Exempt {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists untranslated interface text per UI state. `cargo test -p wordcraft-ui-egui
|
||||
/// untranslated_interface_text -- --nocapture` prints the report. It runs with the other tests:
|
||||
/// input is synthetic and timed by `TIME`, Read Aloud never starts a speech thread, and
|
||||
/// process-wide names other tests change are gathered every frame.
|
||||
/// Fails on interface text drawn without going through the catalog, listed per UI state, and
|
||||
/// prints the missing-translation report (`report_catalog_gaps`). `cargo xtask i18n-report` runs it
|
||||
/// with `--nocapture`; `cargo xtask test` and `ci` run it that way instead of in the main test run.
|
||||
/// It can also run with the other tests: input is synthetic and timed by `TIME`, Read Aloud never
|
||||
/// starts a speech thread, and process-wide names other tests change are gathered every frame.
|
||||
#[test]
|
||||
fn untranslated_interface_text() {
|
||||
pseudo::set(true);
|
||||
@@ -338,16 +339,31 @@ fn untranslated_interface_text() {
|
||||
states.dedup();
|
||||
report.push_str(&format!("{:?} [{}]\n", pseudo::unmarked(text), states.join(", ")));
|
||||
}
|
||||
assert!(found.is_empty(), "{} interface texts skip translation (wrap them in tl!):\n{report}", found.len());
|
||||
report_catalog_gaps(pseudo::looked_up());
|
||||
assert!(found.is_empty(), "{} interface texts skip translation (wrap them in tl!):\n{report}", found.len());
|
||||
}
|
||||
|
||||
/// Prints, per language, the strings the audit saw looked up (plus the equation gallery's labels,
|
||||
/// shown in menus the audit doesn't open one by one) that its catalog has no entry for. A missing
|
||||
/// translation falls back to English, so it never fails the test; `--nocapture` shows the list.
|
||||
/// Numbered style names go through a template.
|
||||
/// The missing-translation report (`cargo xtask i18n-report`, part of `cargo xtask ci`): prints, per
|
||||
/// language, the interface strings its catalog has no entry for. A missing translation falls back
|
||||
/// to English, so the report never fails. The strings are the ones the audit saw looked up, the
|
||||
/// ribbon tabs, every command's label and ribbon location, the File › Home greetings, every reason
|
||||
/// a command gives for being off (#2678) and the equation gallery's labels (shown in menus the
|
||||
/// audit doesn't open one by one). Numbered style names go through a template.
|
||||
fn report_catalog_gaps(mut keys: std::collections::BTreeSet<String>) {
|
||||
use wordcraft_engine::math_gallery::{BUILT_INS, STRUCTURES, SYMBOL_SETS};
|
||||
let tabs = crate::ribbon::TABS.iter().chain(&["Table Design", "Table Layout"]);
|
||||
keys.extend(tabs.map(|t| t.to_string()));
|
||||
keys.extend(["Good morning", "Good afternoon", "Good evening"].map(String::from));
|
||||
keys.extend(disabled_reasons());
|
||||
// Reasons the UI itself gives (`widgets.rs::tooltip`), which the engine's checks never reach.
|
||||
keys.insert("Open a document first".to_string());
|
||||
for doc in [wordcraft_doc::Document::new(), wordcraft_engine::sample::sample_document()] {
|
||||
let session = wordcraft_engine::Session::new(doc);
|
||||
for spec in session.registry.all() {
|
||||
keys.extend(std::iter::once(spec.label).chain(spec.location.split(" › ")).filter(|s| !s.is_empty()).map(String::from));
|
||||
keys.extend((spec.enabled)(&session).map(String::from));
|
||||
}
|
||||
}
|
||||
keys.extend(BUILT_INS.iter().map(|(_, label, _)| label.to_string()));
|
||||
keys.extend(SYMBOL_SETS.iter().map(|(name, _)| name.to_string()));
|
||||
for g in STRUCTURES {
|
||||
@@ -511,28 +527,3 @@ fn disabled_reasons() -> std::collections::BTreeSet<String> {
|
||||
}
|
||||
reasons
|
||||
}
|
||||
|
||||
/// Ribbon tooltips show why a control is off in the interface's language: every reason has an
|
||||
/// entry in every catalog (#2678).
|
||||
#[test]
|
||||
fn every_disabled_reason_is_translated() {
|
||||
let reasons = disabled_reasons();
|
||||
// The source scan finds every reason the registry gives in a blank and the sample document.
|
||||
for doc in [wordcraft_doc::Document::new(), wordcraft_engine::sample::sample_document()] {
|
||||
let session = wordcraft_engine::Session::new(doc);
|
||||
for spec in session.registry.all() {
|
||||
if let Some(r) = (spec.enabled)(&session) {
|
||||
assert!(reasons.contains(r), "{}: the source scan missed its reason {r:?}", spec.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(reasons.len() > 20, "the source scan found only {reasons:#?}");
|
||||
// Reasons the UI itself gives (`widgets.rs::tooltip`), which the engine's checks never reach.
|
||||
let mut reasons = reasons;
|
||||
reasons.insert("Open a document first".to_string());
|
||||
let mut missing = Vec::new();
|
||||
for l in super::Lang::all().filter(|l| *l != super::Lang::EN) {
|
||||
missing.extend(reasons.iter().filter(|r| !super::has(l, r)).map(|r| format!("{}: {r:?}", l.code())));
|
||||
}
|
||||
assert!(missing.is_empty(), "{} disabled reasons are untranslated:\n{missing:#?}", missing.len());
|
||||
}
|
||||
|
||||
@@ -488,6 +488,14 @@ pub fn fmt(template: &str, args: &[(&str, &str)]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// A test language whose catalog is `source` (catalog format), so tests of lookup code don't
|
||||
/// depend on the bundled catalogs' wording. `code` selects code-specific rules such as Turkish
|
||||
/// casing.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn fixture_lang(code: &'static str, source: &'static str) -> Lang {
|
||||
Lang(Box::leak(Box::new(LangInfo { code, name: "Fixture", source, prefer_hans: false, editing_lang: None, catalog: OnceLock::new() })))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod audit;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -72,22 +72,35 @@ fn settings_resolve_with_fallback() {
|
||||
|
||||
#[test]
|
||||
fn lookups_fall_back_to_english() {
|
||||
let zh = lang("zh-hans");
|
||||
assert_eq!(tr(zh, "File"), "文件");
|
||||
assert_eq!(tr(lang("zh-hant"), "File"), "檔案");
|
||||
assert_eq!(tr(lang("ja"), "File"), "ファイル");
|
||||
assert_eq!(tr(zh, "no such label"), "no such label");
|
||||
let fx = fixture_lang("x-fixture", "\tFile\tFile-FX\n");
|
||||
assert_eq!(tr(fx, "File"), "File-FX");
|
||||
assert_eq!(tr(fx, "no such label"), "no such label");
|
||||
assert_eq!(tr(Lang::EN, "File"), "File");
|
||||
assert!(has(zh, "Save") && !has(Lang::EN, "Save"));
|
||||
assert!(has(fx, "File") && !has(fx, "Save") && !has(Lang::EN, "File"));
|
||||
}
|
||||
|
||||
/// Each registered language looks strings up in its own catalog file.
|
||||
#[test]
|
||||
fn each_language_reads_its_own_catalog() {
|
||||
for l in Lang::all() {
|
||||
for e in parse_entries(l.0.source).0.iter().filter(|e| e.context.is_empty()) {
|
||||
assert_eq!(tr(l, &e.source), e.translation, "{}", l.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_language_is_per_thread() {
|
||||
set_current(lang("ja"));
|
||||
assert_eq!(t("Insert"), "挿入");
|
||||
assert_eq!(location("Home › Font"), "ホーム › フォント");
|
||||
assert_eq!(prefixed("Undo", "Bold"), "元に戻す: 太字");
|
||||
assert_eq!(prefixed("Undo", ""), "元に戻す");
|
||||
set_current(fixture_lang(
|
||||
"x-fixture",
|
||||
"\tInsert\tInsert-FX\n\tHome\tHome-FX\n\tFont\tFont-FX\n\tUndo\tUndo-FX\n\tUndo {action}\tUndo-FX: {action}\n\tBold\tBold-FX\n",
|
||||
));
|
||||
assert_eq!(t("Insert"), "Insert-FX");
|
||||
assert_eq!(location("Home › Font"), "Home-FX › Font-FX");
|
||||
assert_eq!(prefixed("Undo", "Bold"), "Undo-FX: Bold-FX");
|
||||
assert_eq!(prefixed("Undo", ""), "Undo-FX");
|
||||
// An action without a translation keeps its English inside the translated template.
|
||||
assert_eq!(prefixed("Undo", "Typing"), "Undo-FX: Typing");
|
||||
std::thread::spawn(|| assert_eq!(current(), Lang::EN)).join().unwrap();
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(t("Insert"), "Insert");
|
||||
@@ -99,7 +112,9 @@ fn placeholders_fill_without_reinterpreting_values() {
|
||||
let args = [("path", "a-{pages}.docx"), ("pages", "2")];
|
||||
assert_eq!(fmt("{pages}: {path}; {unknown}", &args), "2: a-{pages}.docx; {unknown}");
|
||||
assert_eq!(fmt("text {unfinished", &args), "text {unfinished");
|
||||
assert_eq!(fmt(tr(lang("zh-hans"), "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "第 3 页,共 9 页");
|
||||
// A translation may put the placeholders in another order.
|
||||
let fx = fixture_lang("x-fixture", "\tPage {page} of {pages}\t{pages}: {page}\n");
|
||||
assert_eq!(fmt(tr(fx, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "9: 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,82 +141,9 @@ fn bundled_catalogs_are_well_formed() {
|
||||
assert_eq!(e.translation.trim(), e.translation, "{}: stray whitespace in {:?}", l.code, e.translation);
|
||||
assert!(!e.translation.contains("..."), "{}: use … rather than three dots: {:?}", l.code, e.translation);
|
||||
}
|
||||
assert!(l.code == "en" || entries.len() > 700, "{} has {} entries", l.code, entries.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// The ribbon tabs and every registered command's label and ribbon location are translated, in
|
||||
/// every language, so menus, tooltips and command search never fall back to English.
|
||||
#[test]
|
||||
fn every_tab_and_command_is_translated() {
|
||||
let session = wordcraft_engine::Session::new(wordcraft_doc::Document::new());
|
||||
// Labels that read the same in every language.
|
||||
let universal = |s: &str| !s.chars().any(char::is_alphabetic);
|
||||
let mut missing = Vec::new();
|
||||
for l in Lang::all().filter(|l| *l != Lang::EN) {
|
||||
let tabs = crate::ribbon::TABS.iter().chain(&["Table Design", "Table Layout"]).copied();
|
||||
let commands = session.registry.all().iter().flat_map(|spec| std::iter::once(spec.label).chain(spec.location.split(" › ")));
|
||||
for s in tabs.chain(commands).filter(|s| !s.is_empty() && !universal(s)) {
|
||||
if !has(l, s) {
|
||||
missing.push(format!("{}: {s:?}", l.code()));
|
||||
}
|
||||
}
|
||||
}
|
||||
missing.sort();
|
||||
missing.dedup();
|
||||
assert!(missing.is_empty(), "untranslated: {missing:#?}");
|
||||
}
|
||||
|
||||
/// File › Home opens with a greeting for the time of day (`backstage::home_page`); every
|
||||
/// language has all three, so the page doesn't start in English.
|
||||
#[test]
|
||||
fn the_file_home_greeting_is_translated_in_every_language() {
|
||||
let mut missing = Vec::new();
|
||||
for l in Lang::all().filter(|l| *l != Lang::EN) {
|
||||
for s in ["Good morning", "Good afternoon", "Good evening"] {
|
||||
if !has(l, s) {
|
||||
missing.push(format!("{}: {s:?}", l.code()));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "untranslated: {missing:#?}");
|
||||
assert_eq!(tr(lang("pt-br"), "Good afternoon"), "Boa tarde");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn korean_print_settings_and_printer_feedback_are_translated() {
|
||||
let korean = lang("ko");
|
||||
for source in [
|
||||
"Printer",
|
||||
"Select a printer",
|
||||
"Refresh printers",
|
||||
"Copies",
|
||||
"Two-sided printing",
|
||||
"Printer default",
|
||||
"One-sided",
|
||||
"Two-sided: long edge",
|
||||
"Two-sided: short edge",
|
||||
"Grayscale",
|
||||
"Finding printers…",
|
||||
"No printers found. Configure a system printer or export a PDF.",
|
||||
"Printer discovery failed.",
|
||||
"Submitting print job…",
|
||||
"Print job submitted.",
|
||||
"Print job submitted: {receipt}",
|
||||
"Print failed: the printing worker stopped.",
|
||||
"Print failed: {error}",
|
||||
] {
|
||||
assert!(has(korean, source), "missing Korean print translation: {source}");
|
||||
}
|
||||
assert_eq!(korean.catalog().plain("Printer"), Some("프린터"));
|
||||
assert_eq!(korean.catalog().plain("Copies"), Some("매수"));
|
||||
assert_eq!(korean.catalog().plain("Two-sided printing"), Some("양면 인쇄"));
|
||||
assert_eq!(
|
||||
korean.catalog().plain("No printers found. Configure a system printer or export a PDF."),
|
||||
Some("프린터를 찾을 수 없습니다. 시스템 프린터를 설정하거나 PDF로 내보내세요.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_language_setting_is_a_ui_command_and_persists() {
|
||||
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
|
||||
@@ -243,26 +185,6 @@ fn ukrainian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(uk.name(), "Українська");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
assert_eq!(keys(lang("uk").0.source), keys(lang("zh-hans").0.source));
|
||||
let uk = lang("uk");
|
||||
assert_eq!(tr(uk, "Home"), "Основне");
|
||||
assert_eq!(tr(uk, "Font"), "Шрифт");
|
||||
assert_eq!(tr(uk, "Review"), "Рецензування");
|
||||
assert_eq!(tr(uk, "Save"), "Зберегти");
|
||||
assert_eq!(tr(uk, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(uk, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Експортовано draft-{words}.docx");
|
||||
// Count-neutral wording is grammatical for every Ukrainian integer category, including teens.
|
||||
for count in [0, 1, 2, 5, 11, 12, 21, 22, 25, 111, 121] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(uk, "{words} words"), &[("words", &words)]), format!("Слів: {count}"));
|
||||
assert_eq!(fmt(tr(uk, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Слів: {count}; виділено: 1"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_ukrainian_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -279,20 +201,6 @@ fn bundled_interface_fonts_cover_ukrainian_without_system_fallbacks() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serbian_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let reference = keys(lang("zh-hans").0.source);
|
||||
assert_eq!(keys(lang("sr").0.source), reference);
|
||||
assert_eq!(keys(lang("sr-latn").0.source), reference);
|
||||
let (cyr, lat) = (lang("sr"), lang("sr-latn"));
|
||||
assert_eq!(tr(cyr, "Home"), "Почетак");
|
||||
assert_eq!(tr(lat, "Home"), "Početak");
|
||||
assert_eq!(tr(cyr, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(lat, "Exported {path}"), &[("path", "draft-1.docx")]), "Izvezeno: draft-1.docx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_serbian_cyrillic_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -338,8 +246,6 @@ fn serbian_locales_resolve_by_script() {
|
||||
assert_eq!(lang_from_tag(tag), Some(lat), "{tag}");
|
||||
}
|
||||
assert_eq!(first_supported([UNSUPPORTED, "sr-Latn-RS", "en-US"]), Some(lat));
|
||||
assert_eq!(tr(cyr, "File"), "Фајл");
|
||||
assert_eq!(tr(lat, "File"), "Fajl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -364,26 +270,6 @@ fn brazilian_portuguese_locales_and_saved_preference_work_without_changing_the_d
|
||||
assert_eq!(pt.name(), "Português (Brasil)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brazilian_portuguese_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
assert_eq!(keys(lang("pt-br").0.source), keys(lang("zh-hans").0.source));
|
||||
let pt = lang("pt-br");
|
||||
assert_eq!(tr(pt, "Home"), "Início");
|
||||
assert_eq!(tr(pt, "Font"), "Fonte");
|
||||
assert_eq!(tr(pt, "Review"), "Revisão");
|
||||
assert_eq!(tr(pt, "Save"), "Salvar");
|
||||
assert_eq!(tr(pt, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(pt, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Exportado draft-{words}.docx");
|
||||
// Count-neutral wording is grammatical for every count: `Palavras: 21`, `Palavras: 21; seleção: 1`.
|
||||
for count in [0, 1, 2, 5, 21, 100, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(pt, "{words} words"), &[("words", &words)]), format!("Palavras: {count}"));
|
||||
assert_eq!(fmt(tr(pt, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Palavras: {count}; seleção: 1"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_brazilian_portuguese_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -425,37 +311,6 @@ fn czech_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(cs.name(), "Čeština");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn czech_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
// Keep the original Chinese coverage and include the complete styles, equation gallery,
|
||||
// paper sizes and watermarks from the Russian and Spanish catalogs.
|
||||
assert!(keys(lang("cs").0.source).is_superset(&keys(lang("zh-hans").0.source)));
|
||||
assert!(keys(lang("cs").0.source).is_superset(&keys(lang("ru").0.source)));
|
||||
assert!(keys(lang("cs").0.source).is_superset(&keys(lang("es").0.source)));
|
||||
let cs = lang("cs");
|
||||
assert_eq!(tr(cs, "Home"), "Domů");
|
||||
assert_eq!(tr(cs, "Font"), "Písmo");
|
||||
assert_eq!(tr(cs, "Review"), "Revize");
|
||||
assert_eq!(tr(cs, "Save"), "Uložit");
|
||||
assert_eq!(tr(cs, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(cs, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Exportováno: draft-{words}.docx");
|
||||
// Count-neutral wording is grammatical across all Czech plural categories (1, 2–4, 0/5+).
|
||||
for count in [0, 1, 2, 3, 4, 5, 11, 21, 100, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(cs, "{words} words"), &[("words", &words)]), format!("Počet slov: {count}"));
|
||||
assert_eq!(fmt(tr(cs, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Počet slov: {words}; vybráno: 1"));
|
||||
}
|
||||
set_current(cs);
|
||||
// Built-in names and numbered style families use Czech; user-defined names stay as written.
|
||||
for (style, expected) in [("Normal", "Normální"), ("No Spacing", "Bez mezer"), ("Heading 2", "Nadpis 2"), ("TOC 3", "Obsah 3")] {
|
||||
assert_eq!(builtin_style_name(style), expected);
|
||||
}
|
||||
assert_eq!(builtin_style_name("My Own Style"), "My Own Style");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_czech_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -566,35 +421,6 @@ fn russian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(ru.name(), "Русский");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn russian_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
// Everything the other catalogs translate, plus the preview samples below.
|
||||
assert!(keys(lang("ru").0.source).is_superset(&keys(lang("zh-hans").0.source)));
|
||||
assert!(parse_entries(lang("ru").0.source).1.is_empty());
|
||||
let ru = lang("ru");
|
||||
// Style gallery and Design tab previews show Cyrillic sample text.
|
||||
assert_eq!(tr(ru, "AaBbCcDd"), "АаБбВвГг");
|
||||
assert_eq!(tr(ru, "Title"), "Заголовок");
|
||||
assert_eq!(tr(ru, "Heading 1"), "Заголовок 1");
|
||||
assert!(tr(ru, "Body text in a short paragraph to show spacing.").starts_with("Основной текст"));
|
||||
assert_eq!(tr(ru, "Home"), "Главная");
|
||||
assert_eq!(tr(ru, "Font"), "Шрифт");
|
||||
assert_eq!(tr(ru, "Review"), "Рецензирование");
|
||||
assert_eq!(tr(ru, "Save"), "Сохранить");
|
||||
assert_eq!(tr(ru, "unknown future label"), "unknown future label");
|
||||
// The search hint's example must find the command it names.
|
||||
assert_eq!(tr(ru, "Insert Table"), "Вставить таблицу");
|
||||
assert!(tr(ru, "Type a command, e.g. \"insert table\"").contains("вставить таблицу"));
|
||||
// Count-neutral wording is grammatical for every count: `Слов: 21`, `Слов: 1 из 21`.
|
||||
for count in [0, 1, 2, 5, 21, 100, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(ru, "{words} words"), &[("words", &words)]), format!("Слов: {count}"));
|
||||
assert_eq!(fmt(tr(ru, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Слов: 1 из {count}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_russian_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -621,68 +447,29 @@ fn bundled_interface_fonts_cover_russian_without_system_fallbacks() {
|
||||
fn built_in_style_names_are_translated_for_display_and_user_styles_are_not() {
|
||||
let sheet = wordcraft_doc::StyleSheet::builtin();
|
||||
let name = |id: &str| style_name(sheet.get(id).unwrap());
|
||||
set_current(lang("ru"));
|
||||
assert_eq!(name("Normal"), "Обычный");
|
||||
assert_eq!(name("Title"), "Заголовок");
|
||||
assert_eq!(name("Heading1"), "Заголовок 1");
|
||||
set_current(fixture_lang(
|
||||
"x-fixture",
|
||||
"\tNormal\tNormal-FX\n\tHeading 1\tHeading-one-FX\n\tHeading {n}\tHeading-FX {n}\n\tTOC {n}\tTOC-FX {n}\n\tIndex {n}\tIndex-FX {n}\n",
|
||||
));
|
||||
assert_eq!(name("Normal"), "Normal-FX");
|
||||
// An explicit entry wins over its family's template.
|
||||
assert_eq!(name("Heading1"), "Heading-one-FX");
|
||||
// Numbered families beyond the catalog's explicit entries go through one template.
|
||||
assert_eq!(name("Heading7"), "Заголовок 7");
|
||||
assert_eq!(name("TOC9"), "Оглавление 9");
|
||||
assert_eq!(name("Heading7"), "Heading-FX 7");
|
||||
assert_eq!(name("TOC9"), "TOC-FX 9");
|
||||
assert_eq!(builtin_style_name("Index 2"), "Index-FX 2");
|
||||
// A style the author made keeps its name, even when it matches a built-in one.
|
||||
let mine = wordcraft_doc::Style { name: "Normal".into(), builtin: false, ..Default::default() };
|
||||
assert_eq!(style_name(&mine), "Normal");
|
||||
// A built-in name without a translation stays English.
|
||||
assert_eq!(builtin_style_name("Some Future Style"), "Some Future Style");
|
||||
assert_eq!(builtin_style_name("Heading x"), "Heading x");
|
||||
// Every built-in paragraph and character style shows in Russian. Table styles (`Grid Table 4 –
|
||||
// Blue`) appear only as tooltips under Table Design and stay English for now.
|
||||
let untranslated: Vec<&str> = sheet
|
||||
.styles
|
||||
.iter()
|
||||
.filter(|s| s.kind != wordcraft_doc::StyleKind::Table && style_name(s) == s.name && s.name.chars().any(char::is_alphabetic))
|
||||
.map(|s| s.name.as_str())
|
||||
.collect();
|
||||
assert!(untranslated.is_empty(), "built-in styles without a Russian name: {untranslated:?}");
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(name("Heading7"), "Heading 7");
|
||||
// The document itself never changes.
|
||||
assert_eq!(sheet.get("Normal").unwrap().name, "Normal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spanish_built_in_style_names_are_translated() {
|
||||
set_current(lang("es"));
|
||||
for (source, expected) in [
|
||||
("Book Title", "Título de libro"),
|
||||
("Caption", "Leyenda"),
|
||||
("Emphasis", "Énfasis"),
|
||||
("Endnote Reference", "Referencia de nota al final"),
|
||||
("Endnote Text", "Texto de nota al final"),
|
||||
("Footnote Reference", "Referencia de nota al pie"),
|
||||
("Footnote Text", "Texto de nota al pie"),
|
||||
("Hyperlink", "Hipervínculo"),
|
||||
("Intense Emphasis", "Énfasis intenso"),
|
||||
("Intense Quote", "Cita intensa"),
|
||||
("Intense Reference", "Referencia intensa"),
|
||||
("List Paragraph", "Párrafo de lista"),
|
||||
("Quote", "Cita"),
|
||||
("Strong", "Fuerte"),
|
||||
("Subtitle", "Subtítulo"),
|
||||
("Subtle Emphasis", "Énfasis sutil"),
|
||||
("Subtle Reference", "Referencia sutil"),
|
||||
("TOC Heading", "Título de tabla de contenido"),
|
||||
] {
|
||||
assert_eq!(builtin_style_name(source), expected, "{source}");
|
||||
}
|
||||
for level in 4..=9 {
|
||||
assert_eq!(builtin_style_name(&format!("Heading {level}")), format!("Título {level}"));
|
||||
}
|
||||
for level in 1..=9 {
|
||||
assert_eq!(builtin_style_name(&format!("TOC {level}")), format!("Tabla de contenido {level}"));
|
||||
}
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_entries_override_only_their_context_and_unknown_contexts_are_rejected() {
|
||||
let (c, errors) = Catalog::parse("\tTitle\tЗаголовок\npreview\tTitle\tТитул\n\tFont\tШрифт\nnope\tFont\tX\n");
|
||||
@@ -693,11 +480,12 @@ fn context_entries_override_only_their_context_and_unknown_contexts_are_rejected
|
||||
// Without a context entry, a context lookup uses the plain one.
|
||||
assert_eq!(c.in_context("preview", "Font"), Some("Шрифт"));
|
||||
assert_eq!(c.in_context("preview", "Missing"), None);
|
||||
let ru = lang("ru");
|
||||
assert_eq!(tr(ru, "Title"), "Заголовок");
|
||||
assert_eq!(trc(ru, "preview", "Title"), "Пример");
|
||||
let fx = fixture_lang("x-fixture", "\tTitle\tTitle-FX\npreview\tTitle\tSample-FX\n");
|
||||
assert_eq!(tr(fx, "Title"), "Title-FX");
|
||||
assert_eq!(trc(fx, "preview", "Title"), "Sample-FX");
|
||||
assert_eq!(trc(fx, "template", "Title"), "Title-FX");
|
||||
assert_eq!(trc(Lang::EN, "preview", "Title"), "Title");
|
||||
assert_eq!(trc(lang("ja"), "preview", "Unknown"), "Unknown");
|
||||
assert_eq!(trc(fx, "preview", "Unknown"), "Unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -720,28 +508,6 @@ fn estonian_locales_and_saved_preference_keep_document_content() {
|
||||
assert_eq!(Lang::from_pref(&restored.language), et);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn estonian_covers_the_interface_catalog_and_keeps_count_labels_neutral() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let et = lang("et");
|
||||
// Estonian carries the interface catalog; Spanish and Turkish add the equation gallery and
|
||||
// built-in style names on top.
|
||||
assert!(keys(et.0.source).is_subset(&keys(lang("es").0.source)));
|
||||
assert!(keys(et.0.source).is_subset(&keys(lang("tr").0.source)));
|
||||
assert!(keys(lang("zh-hans").0.source).is_subset(&keys(et.0.source)));
|
||||
assert_eq!(tr(et, "Home"), "Avaleht");
|
||||
assert_eq!(tr(et, "Save"), "Salvesta");
|
||||
assert_eq!(tr(et, "Spelling & Grammar"), "Õigekiri ja grammatika");
|
||||
assert_eq!(tr(et, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(et, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Eksporditud: draft-{words}.docx");
|
||||
for count in [0, 1, 2, 11, 21, 101] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(et, "{words} words"), &[("words", &words)]), format!("Sõnu: {count}"));
|
||||
assert_eq!(fmt(tr(et, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Sõnu: {count}; valitud: 1"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_estonian_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -779,51 +545,6 @@ fn turkish_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(tr.name(), "Türkçe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turkish_covers_the_entire_existing_interface_catalog_and_preserves_templates() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
// Full parity with the Russian catalog: covers all UI strings, styles, math gallery, and presets.
|
||||
assert_eq!(keys(lang("tr").0.source), keys(lang("ru").0.source));
|
||||
assert!(keys(lang("es").0.source).is_subset(&keys(lang("tr").0.source)));
|
||||
assert!(keys(lang("zh-hans").0.source).is_subset(&keys(lang("tr").0.source)));
|
||||
let tr = lang("tr");
|
||||
for (source, expected) in [
|
||||
("Home", "Giriş"),
|
||||
("Font", "Yazı Tipi"),
|
||||
("Review", "İncele"),
|
||||
("Italic", "İtalik"),
|
||||
("Table of Contents", "İçindekiler Tablosu"),
|
||||
("Track Changes", "Değişiklikleri İzle"),
|
||||
("Save", "Kaydet"),
|
||||
("UPPERCASE", "BÜYÜK HARF"),
|
||||
("lowercase", "küçük harf"),
|
||||
("Square Root", "Karekök"),
|
||||
("Euler's identity", "Euler Özdeşliği"),
|
||||
("Default Paragraph Font", "Varsayılan Paragraf Yazı Tipi"),
|
||||
("CONFIDENTIAL", "GİZLİ"),
|
||||
("WordCraft", "WordCraft"),
|
||||
("unknown future label", "unknown future label"),
|
||||
] {
|
||||
assert_eq!(super::tr(tr, source), expected);
|
||||
}
|
||||
assert_eq!(fmt(super::tr(tr, "Exported {path}"), &[("path", "taslak-{words}.docx")]), "Dışa aktarıldı: taslak-{words}.docx");
|
||||
assert_eq!(fmt(super::tr(tr, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Sayfa 3 / 9");
|
||||
// The Font dialog uppercases its preview sample for All Caps, so the sample has no lowercase i.
|
||||
assert_eq!(super::tr(tr, "AaBbYyZz").to_uppercase(), "AABBYYZZ ĞĞİIŞŞ");
|
||||
// Turkish count nouns remain singular for both one and other, including zero and large counts.
|
||||
for count in [0, 1, 2, 5, 11, 21, 100, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(super::tr(tr, "{words} words"), &[("words", &words)]), format!("{count} sözcük"));
|
||||
assert_eq!(fmt(super::tr(tr, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("{count} sözcükten 1 seçili"));
|
||||
}
|
||||
set_current(tr);
|
||||
assert_eq!(location("Home › Font"), "Giriş › Yazı Tipi");
|
||||
assert_eq!(prefixed("Undo", "Italic"), "Geri Al: İtalik");
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(super::tr(Lang::EN, "Italic"), "Italic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_turkish_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -863,48 +584,6 @@ fn hungarian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(app.ui.language, AUTO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hungarian_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
// Every key of the most complete catalog: built-in style names, equation gallery labels, paper
|
||||
// sizes and watermarks included.
|
||||
assert!(keys(lang("hu").0.source).is_superset(&keys(lang("ru").0.source)));
|
||||
assert!(keys(lang("hu").0.source).is_superset(&keys(lang("es").0.source)));
|
||||
let hu = lang("hu");
|
||||
for (source, expected) in [
|
||||
("Home", "Kezdőlap"),
|
||||
("Font", "Betűtípus"),
|
||||
("Review", "Véleményezés"),
|
||||
("Header", "Élőfej"),
|
||||
("Track Changes", "Változáskövetés"),
|
||||
("Save", "Mentés"),
|
||||
("Redo", "Újra"),
|
||||
("UPPERCASE", "NAGYBETŰS"),
|
||||
("WordCraft", "WordCraft"),
|
||||
("unknown future label", "unknown future label"),
|
||||
] {
|
||||
assert_eq!(super::tr(hu, source), expected);
|
||||
}
|
||||
assert_eq!(fmt(super::tr(hu, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Oldal: 3/9");
|
||||
// A noun after a number stays singular in Hungarian, whatever the count.
|
||||
for count in [0, 1, 2, 21, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(super::tr(hu, "{words} words"), &[("words", &words)]), format!("{count} szó"));
|
||||
}
|
||||
// The Font dialog uppercases its preview sample for All Caps.
|
||||
assert_eq!(super::tr(hu, "AaBbYyZz").to_uppercase(), "AABBYYZZ ŐŐŰŰ");
|
||||
set_current(hu);
|
||||
// Built-in styles show translated names; numbered families go through their template.
|
||||
for (style, expected) in [("Normal", "Normál"), ("No Spacing", "Nincs térköz"), ("Heading 2", "Címsor 2"), ("TOC 3", "Tartalomjegyzék 3")] {
|
||||
assert_eq!(builtin_style_name(style), expected);
|
||||
}
|
||||
assert_eq!(builtin_style_name("My Own Style"), "My Own Style");
|
||||
assert_eq!(location("Home › Font"), "Kezdőlap › Betűtípus");
|
||||
assert_eq!(prefixed("Undo", "Bold"), "Visszavonás: Félkövér");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_hungarian_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -932,10 +611,6 @@ fn french_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(first_supported([UNSUPPORTED, "fr-CA", "en-US"]), Some(fr));
|
||||
assert_eq!(normalize_pref("FR"), Some("fr"));
|
||||
assert_eq!(fr.name(), "Français");
|
||||
assert_eq!(tr(fr, "Home"), "Accueil");
|
||||
assert_eq!(tr(fr, "Save"), "Enregistrer");
|
||||
assert_eq!(fmt(tr(fr, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Page 3 sur 9");
|
||||
assert_eq!(tr(fr, "unknown future label"), "unknown future label");
|
||||
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_engine::sample::sample_document()), Default::default());
|
||||
let before = serde_json::to_value(&app.session.doc).unwrap();
|
||||
let result = app.run("ui.language", serde_json::json!({"value": "FR"})).unwrap();
|
||||
@@ -998,51 +673,6 @@ fn polish_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(pl.name(), "Polski");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polish_covers_every_key_of_every_other_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let polish = keys(lang("pl").0.source);
|
||||
// Italian and Hungarian still carry "Style: APA", which the interface no longer shows;
|
||||
// Slovenian carries the union of every catalog's keys.
|
||||
// Hungarian and Bulgarian also name the citation styles, and Bulgarian has a few one-line
|
||||
// pieces of gallery labels. Polish matches every other catalog, including the built-in style
|
||||
// names, citation styles and equation gallery. French carries newer strings (paper sizes,
|
||||
// bookmarks, printing) the Polish catalog hasn't caught up with yet.
|
||||
for other in Lang::all().filter(|l| *l != Lang::EN && !matches!(l.code(), "pl" | "it" | "hu" | "bg" | "vi" | "sv" | "hr" | "fr" | "ro" | "sl")) {
|
||||
let mut missing: Vec<String> = keys(other.0.source).difference(&polish).cloned().collect();
|
||||
missing.sort();
|
||||
assert!(missing.is_empty(), "pl lacks {}'s {missing:?}", other.code());
|
||||
}
|
||||
for code in ["it", "hu"] {
|
||||
let missing: Vec<String> = keys(lang(code).0.source).difference(&polish).cloned().collect();
|
||||
assert_eq!(missing, ["Style: APA"], "pl vs {code}");
|
||||
}
|
||||
let pl = lang("pl");
|
||||
assert_eq!(tr(pl, "File"), "Plik");
|
||||
assert_eq!(tr(pl, "Home"), "Start");
|
||||
assert_eq!(tr(pl, "Insert"), "Wstaw");
|
||||
assert_eq!(tr(pl, "Review"), "Recenzja");
|
||||
assert_eq!(tr(pl, "Save"), "Zapisz");
|
||||
assert_eq!(tr(pl, "Spelling & Grammar"), "Pisownia i gramatyka");
|
||||
assert_eq!(tr(pl, "Polish"), "Polski");
|
||||
assert_eq!(tr(pl, "GOST (name sort)"), "GOST (sortowanie według nazwiska)");
|
||||
assert_eq!(tr(pl, "ISO 690 (author-date)"), "ISO 690 (autor–data)");
|
||||
assert_eq!(tr(pl, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(pl, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Wyeksportowano: draft-{words}.docx");
|
||||
assert_eq!(fmt(tr(pl, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Strona 3 z 9");
|
||||
// Count-neutral wording reads grammatically for every Polish number form (1, 2–4, 5+, 12–14, 22).
|
||||
for count in [0, 1, 2, 4, 5, 12, 14, 22, 25, 101, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(pl, "{words} words"), &[("words", &words)]), format!("Słowa: {count}"));
|
||||
assert_eq!(fmt(tr(pl, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Słowa: 1 z {count}"));
|
||||
}
|
||||
set_current(pl);
|
||||
assert_eq!(location("Review › Proofing"), "Recenzja › Sprawdzanie");
|
||||
assert_eq!(prefixed("Undo", "Bold"), "Cofnij: Pogrubienie");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_polish_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1076,13 +706,15 @@ fn a_polish_interface_writes_new_documents_in_polish() {
|
||||
assert!(!app.session.dirty);
|
||||
app.run("file.new", serde_json::json!({})).unwrap();
|
||||
assert_eq!(app.session.doc.styles.default_chr.lang.as_deref(), Some("pl-PL"));
|
||||
// Typing Polish then shows no spelling marks; the status bar names the language.
|
||||
// Typing Polish then shows no spelling marks; the status bar names the language, translated.
|
||||
app.session.run("text.insert", &serde_json::json!({"text": "Zażółć gęślą jaźń"})).unwrap();
|
||||
assert_eq!(app.session.run("review.issues", &serde_json::json!({})).unwrap(), serde_json::json!([]));
|
||||
set_current(lang("pl"));
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "Polski");
|
||||
// The app's frames set the interface language; name it in English first.
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "Polish");
|
||||
super::pseudo::set(true);
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "⟦Polish⟧", "the language name goes through the catalog");
|
||||
super::pseudo::set(false);
|
||||
// Back to English: documents already written keep their language.
|
||||
app.run("ui.language", serde_json::json!({"value": "en"})).unwrap();
|
||||
frame(&mut app);
|
||||
@@ -1102,10 +734,12 @@ fn a_czech_interface_writes_new_documents_and_plain_text_in_czech() {
|
||||
assert!(!app.session.dirty);
|
||||
app.run("file.new", serde_json::json!({})).unwrap();
|
||||
assert_eq!(app.session.doc.styles.default_chr.lang.as_deref(), Some("cs-CZ"));
|
||||
set_current(lang("cs"));
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "Čeština");
|
||||
// The app's frames set the interface language; name it in English first.
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "Czech");
|
||||
super::pseudo::set(true);
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "⟦Czech⟧", "the language name goes through the catalog");
|
||||
super::pseudo::set(false);
|
||||
for path in ["poznamka.txt", "poznamka.md"] {
|
||||
// Base64 of our synthetic text, "Město"; file.open takes bytes without a dialog.
|
||||
app.session.run("file.open", &serde_json::json!({"path": path, "data": "TcSbc3Rv"})).unwrap();
|
||||
@@ -1127,14 +761,16 @@ fn an_italian_interface_writes_new_documents_in_italian() {
|
||||
frame(&mut app);
|
||||
assert_eq!(app.session.editing_lang.as_deref(), Some("it-IT"));
|
||||
assert_eq!(app.session.doc.styles.default_chr.lang.as_deref(), Some("it-IT"), "the untouched first document follows");
|
||||
// Typing Italian then shows no English misspellings; the status bar names the language.
|
||||
// Typing Italian then shows no English misspellings; the status bar names the language, translated.
|
||||
app.session.run("text.insert", &serde_json::json!({"text": "Città vecchie e caffè"})).unwrap();
|
||||
let issues = app.session.run("review.issues", &serde_json::json!({})).unwrap();
|
||||
assert!(issues.as_array().unwrap().iter().all(|i| i["lang"] == "it"), "{issues}");
|
||||
set_current(lang("it"));
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "Italiano");
|
||||
// The app's frames set the interface language; name it in English first.
|
||||
set_current(Lang::EN);
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "Italian");
|
||||
super::pseudo::set(true);
|
||||
assert_eq!(crate::ribbon::caret_language(&app), "⟦Italian⟧", "the language name goes through the catalog");
|
||||
super::pseudo::set(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1163,25 +799,6 @@ fn german_locales_and_saved_preference_keep_document_content() {
|
||||
assert_eq!(Lang::from_pref(&restored.language), de);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn german_covers_the_interface_catalog_and_keeps_count_labels_neutral() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let de = lang("de");
|
||||
assert_eq!(keys(de.0.source), keys(lang("zh-hans").0.source));
|
||||
assert_eq!(tr(de, "Home"), "Start");
|
||||
assert_eq!(tr(de, "Save"), "Speichern");
|
||||
assert_eq!(tr(de, "Spelling & Grammar"), "Rechtschreibung und Grammatik");
|
||||
assert_eq!(tr(de, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(de, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Exportiert: draft-{words}.docx");
|
||||
// Count-neutral wording is grammatical for every count: `Wörter: 21`, `Wörter: 21; ausgewählt: 1`.
|
||||
for count in [0, 1, 2, 11, 21, 101] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(de, "{words} words"), &[("words", &words)]), format!("Wörter: {count}"));
|
||||
assert_eq!(fmt(tr(de, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Wörter: {count}; ausgewählt: 1"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_german_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1214,31 +831,6 @@ fn italian_locales_and_saved_preference_keep_document_content() {
|
||||
assert_eq!(Lang::from_pref(&restored.language), it);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn italian_covers_the_entire_existing_interface_catalog_and_keeps_count_labels_neutral() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let it = lang("it");
|
||||
for other in ["es", "tr", "et", "zh-hans"] {
|
||||
assert!(keys(lang(other).0.source).is_subset(&keys(it.0.source)), "{other} has keys Italian lacks");
|
||||
}
|
||||
assert_eq!(tr(it, "Good morning"), "Buongiorno");
|
||||
assert_eq!(tr(it, "Save"), "Salva");
|
||||
assert_eq!(tr(it, "Insert"), "Inserisci");
|
||||
assert_eq!(tr(it, "Spelling & Grammar"), "Ortografia e grammatica");
|
||||
assert_eq!(tr(it, "unknown future label"), "unknown future label");
|
||||
// The Review tab's group and its toggle keep different names.
|
||||
assert_ne!(tr(it, "Changes"), tr(it, "Track Changes"));
|
||||
// Italian nouns agree with the number, so count labels put the noun first and read right at 1.
|
||||
for count in [0, 1, 2, 11, 21, 101] {
|
||||
let n = count.to_string();
|
||||
assert_eq!(fmt(tr(it, "{words} words"), &[("words", &n)]), format!("Parole: {count}"));
|
||||
assert_eq!(fmt(tr(it, "{selected} of {words} words"), &[("selected", "1"), ("words", &n)]), format!("Parole: {count}; selezionate: 1"));
|
||||
assert_eq!(fmt(tr(it, "{n} lines"), &[("n", &n)]), format!("Righe: {count}"));
|
||||
assert_eq!(fmt(tr(it, "{count} items highlighted"), &[("count", &n)]), format!("Elementi evidenziati: {count}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_italian_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1271,41 +863,6 @@ fn bulgarian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(bg.name(), "Български");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulgarian_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
// Every key the interface looks up (Spanish carries them all), and no catalog errors.
|
||||
assert!(keys(lang("bg").0.source).is_superset(&keys(lang("es").0.source)));
|
||||
assert!(parse_entries(lang("bg").0.source).1.is_empty());
|
||||
let bg = lang("bg");
|
||||
for (source, expected) in [
|
||||
("Home", "Начало"),
|
||||
("Insert", "Вмъкване"),
|
||||
("Font", "Шрифт"),
|
||||
("Paragraph", "Абзац"),
|
||||
("Review", "Преглед"),
|
||||
("Save", "Запазване"),
|
||||
("Heading 1", "Заглавие 1"),
|
||||
("Hyphenation", "Сричкопренасяне"),
|
||||
("Track Changes", "Проследяване на промените"),
|
||||
("WordCraft", "WordCraft"),
|
||||
("unknown future label", "unknown future label"),
|
||||
] {
|
||||
assert_eq!(tr(bg, source), expected, "{source}");
|
||||
}
|
||||
// The search hint's example must find the command it names.
|
||||
assert_eq!(tr(bg, "Insert Table"), "Вмъкване на таблица");
|
||||
assert!(tr(bg, "Type a command, e.g. \"insert table\"").contains("вмъкване на таблица"));
|
||||
assert_eq!(fmt(tr(bg, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Страница 3 от 9");
|
||||
// Count-neutral wording is grammatical for every count: `Думи: 21`, `Думи: 1 от 21`.
|
||||
for count in [0, 1, 2, 5, 21, 100, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(bg, "{words} words"), &[("words", &words)]), format!("Думи: {count}"));
|
||||
assert_eq!(fmt(tr(bg, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Думи: 1 от {count}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_bulgarian_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1348,49 +905,6 @@ fn croatian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(hr.name(), "Hrvatski");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn croatian_covers_the_entire_existing_interface_catalog_and_keeps_count_labels_neutral() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let hr = lang("hr");
|
||||
// Every key of every other catalog, and no catalog errors.
|
||||
for other in Lang::all().filter(|l| *l != Lang::EN && l.code() != "hr") {
|
||||
let mut missing: Vec<String> = keys(other.0.source).difference(&keys(hr.0.source)).cloned().collect();
|
||||
missing.sort();
|
||||
// Italian and Hungarian still carry "Style: APA", which the interface no longer shows.
|
||||
missing.retain(|k| k != "Style: APA" || !matches!(other.code(), "it" | "hu"));
|
||||
assert!(missing.is_empty(), "hr lacks {}'s {missing:?}", other.code());
|
||||
}
|
||||
assert!(parse_entries(hr.0.source).1.is_empty());
|
||||
for (source, expected) in [
|
||||
("File", "Datoteka"),
|
||||
("Home", "Početna"),
|
||||
("Insert", "Umetni"),
|
||||
("Review", "Pregled"),
|
||||
("Save", "Spremi"),
|
||||
("Heading 1", "Naslov 1"),
|
||||
("Spelling & Grammar", "Pravopis i gramatika"),
|
||||
("Hyphenation", "Rastavljanje riječi"),
|
||||
("WordCraft", "WordCraft"),
|
||||
("unknown future label", "unknown future label"),
|
||||
] {
|
||||
assert_eq!(tr(hr, source), expected, "{source}");
|
||||
}
|
||||
assert_eq!(fmt(tr(hr, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Stranica 3 od 9");
|
||||
// The letter template is "Pismo"; the paper size of the same English name stays "Letter".
|
||||
assert_eq!(trc(hr, "template", "Letter"), "Pismo");
|
||||
assert_eq!(tr(hr, "Letter"), "Letter");
|
||||
// Croatian nouns take three forms after numerals (1, 2–4, 5+), so counts read `Riječi: 21`.
|
||||
for count in [0, 1, 2, 4, 5, 12, 14, 21, 22, 25, 101, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(hr, "{words} words"), &[("words", &words)]), format!("Riječi: {count}"));
|
||||
assert_eq!(fmt(tr(hr, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Riječi: 1 od {count}"));
|
||||
}
|
||||
set_current(hr);
|
||||
assert_eq!(location("Review › Proofing"), "Pregled › Jezična provjera");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_croatian_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1433,42 +947,6 @@ fn swedish_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(sv.name(), "Svenska");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swedish_covers_the_entire_existing_interface_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
// Every key the interface looks up (Spanish carries them all), and no catalog errors.
|
||||
assert!(keys(lang("sv").0.source).is_superset(&keys(lang("es").0.source)));
|
||||
assert!(parse_entries(lang("sv").0.source).1.is_empty());
|
||||
let sv = lang("sv");
|
||||
for (source, expected) in [
|
||||
("File", "Arkiv"),
|
||||
("Home", "Start"),
|
||||
("Insert", "Infoga"),
|
||||
("Font", "Typsnitt"),
|
||||
("Paragraph", "Stycke"),
|
||||
("Review", "Granska"),
|
||||
("Save", "Spara"),
|
||||
("Heading 1", "Rubrik 1"),
|
||||
("Hyphenation", "Avstavning"),
|
||||
("Track Changes", "Spåra ändringar"),
|
||||
("WordCraft", "WordCraft"),
|
||||
("unknown future label", "unknown future label"),
|
||||
] {
|
||||
assert_eq!(tr(sv, source), expected, "{source}");
|
||||
}
|
||||
// The search hint's example must find the command it names.
|
||||
assert_eq!(tr(sv, "Insert Table"), "Infoga tabell");
|
||||
assert!(tr(sv, "Type a command, e.g. \"insert table\"").contains(&tr(sv, "Insert Table").to_lowercase()));
|
||||
assert_eq!(fmt(tr(sv, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Sida 3 av 9");
|
||||
// `ord` is the same in the singular and the plural, so the counts read right for every number.
|
||||
for count in [0, 1, 2, 5, 21, 100, 1000] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(sv, "{words} words"), &[("words", &words)]), format!("{count} ord"));
|
||||
assert_eq!(fmt(tr(sv, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("1 av {count} ord"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_swedish_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1512,46 +990,6 @@ fn vietnamese_locales_and_saved_preference_work_without_changing_the_document()
|
||||
assert_eq!(vi.name(), "Tiếng Việt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vietnamese_covers_existing_catalog_keys_with_an_explicit_preview_fallback() {
|
||||
use std::collections::HashSet;
|
||||
let vi = lang("vi");
|
||||
let (entries, errors) = parse_entries(vi.0.source);
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
let vi_keys: HashSet<_> = entries.into_iter().map(|e| (e.context, e.source)).collect();
|
||||
// The short plain title fits the preview too; this is the only deliberate context fallback.
|
||||
let preview_title = ("preview".to_string(), "Title".to_string());
|
||||
assert!(vi_keys.contains(&(String::new(), "Title".to_string())));
|
||||
assert_eq!(trc(vi, "preview", "Title"), "Tiêu đề");
|
||||
assert_eq!(trc(vi, "preview", "Title"), tr(vi, "Title"));
|
||||
// Compare complete (context, source) keys so a contextual entry cannot hide a missing plain
|
||||
// translation, or silently satisfy another context's key.
|
||||
for other in Lang::all().filter(|l| *l != Lang::EN && l.code() != "vi") {
|
||||
let (entries, errors) = parse_entries(other.0.source);
|
||||
assert!(errors.is_empty(), "{}: {errors:?}", other.code());
|
||||
let keys: HashSet<_> = entries.into_iter().map(|e| (e.context, e.source)).collect();
|
||||
let mut missing: Vec<_> = keys.difference(&vi_keys).filter(|key| **key != preview_title).cloned().collect();
|
||||
missing.sort();
|
||||
assert!(missing.is_empty(), "vi lacks {}'s {missing:?}", other.code());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vietnamese_translations_preserve_templates_and_count_labels() {
|
||||
let vi = lang("vi");
|
||||
assert_eq!(tr(vi, "Home"), "Trang chủ");
|
||||
assert_eq!(tr(vi, "Save"), "Lưu");
|
||||
assert_eq!(tr(vi, "Spelling & Grammar"), "Chính tả & Ngữ pháp");
|
||||
assert_eq!(tr(vi, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(vi, "Exported {path}"), &[("path", "draft-{words}.docx")]), "Đã xuất draft-{words}.docx");
|
||||
// Count-neutral wording (`Từ: 21`): a Vietnamese noun stays unchanged after a number.
|
||||
for count in [0, 1, 2, 11, 21, 101] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(vi, "{words} words"), &[("words", &words)]), format!("Từ: {count}"));
|
||||
assert_eq!(fmt(tr(vi, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Từ: 1 trên {count}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_vietnamese_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1581,9 +1019,14 @@ fn bundled_interface_fonts_cover_vietnamese_letters() {
|
||||
/// #3317: the spooler's English receipt is not spliced into a translated sentence.
|
||||
#[test]
|
||||
fn spooled_print_receipt_is_translated() {
|
||||
let german = lang("de");
|
||||
let msg = crate::printing::feedback(&crate::printing::Feedback::Submitted("spooled 2 sheet(s)".into()), |s| tr(german, s).to_owned());
|
||||
assert_eq!(msg, "Druckauftrag gesendet: 2 Blatt an den Spooler übergeben");
|
||||
use crate::printing::{Feedback, feedback};
|
||||
let (c, errors) = Catalog::parse(
|
||||
"\tPrint job submitted: spooled {count} sheet(s)\tSpooled-FX: {count}\n\tPrint job submitted: {receipt}\tSubmitted-FX: {receipt}\n",
|
||||
);
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
let receipt = || Feedback::Submitted("spooled 2 sheet(s)".into());
|
||||
assert_eq!(feedback(&receipt(), |s| c.plain(s).unwrap_or(s).to_owned()), "Spooled-FX: 2");
|
||||
assert_eq!(feedback(&receipt(), str::to_owned), "Print job submitted: spooled 2 sheet(s)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1607,28 +1050,6 @@ fn romanian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert!(!ro.uses_cjk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn romanian_covers_every_key_of_every_other_catalog_and_keeps_placeholders() {
|
||||
use std::collections::HashSet;
|
||||
let ro = lang("ro");
|
||||
let (entries, errors) = parse_entries(ro.0.source);
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
let ro_keys: HashSet<_> = entries.iter().map(|e| (e.context.clone(), e.source.clone())).collect();
|
||||
for other in Lang::all().filter(|l| *l != Lang::EN && l.code() != "ro") {
|
||||
let (other_entries, other_errors) = parse_entries(other.0.source);
|
||||
assert!(other_errors.is_empty(), "{}: {other_errors:?}", other.code());
|
||||
let mut missing: Vec<_> = other_entries.into_iter().map(|e| (e.context, e.source)).filter(|k| !ro_keys.contains(k)).collect();
|
||||
missing.sort();
|
||||
// Italian and Hungarian still carry "Style: APA", which the interface no longer shows.
|
||||
missing.retain(|(_, k)| k != "Style: APA" || !matches!(other.code(), "it" | "hu"));
|
||||
assert!(missing.is_empty(), "ro lacks {}'s {missing:?}", other.code());
|
||||
}
|
||||
assert_eq!(tr(ro, "Home"), "Pornire");
|
||||
assert_eq!(tr(ro, "Save"), "Salvare");
|
||||
assert_eq!(tr(ro, "unknown future label"), "unknown future label");
|
||||
assert_eq!(fmt(tr(ro, "Exported {path}"), &[("path", "draft.docx")]), "Exportat: draft.docx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_romanian_without_system_fallbacks() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1666,51 +1087,6 @@ fn slovenian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
assert_eq!(Lang::from_pref(&restored.language), sl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slovenian_covers_every_key_of_every_other_catalog() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let slovenian = keys(lang("sl").0.source);
|
||||
assert!(parse_entries(lang("sl").0.source).1.is_empty());
|
||||
for other in Lang::all().filter(|l| *l != Lang::EN && l.code() != "sl") {
|
||||
let mut missing: Vec<String> = keys(other.0.source).difference(&slovenian).cloned().collect();
|
||||
missing.sort();
|
||||
// Italian and Hungarian still carry "Style: APA", which the interface no longer shows.
|
||||
missing.retain(|k| k != "Style: APA" || !matches!(other.code(), "it" | "hu"));
|
||||
assert!(missing.is_empty(), "sl lacks {}'s {missing:?}", other.code());
|
||||
}
|
||||
let sl = lang("sl");
|
||||
for (source, expected) in [
|
||||
("File", "Datoteka"),
|
||||
("Home", "Osnovno"),
|
||||
("Insert", "Vstavljanje"),
|
||||
("Layout", "Postavitev"),
|
||||
("References", "Sklici"),
|
||||
("Review", "Pregled"),
|
||||
("View", "Pogled"),
|
||||
("Save", "Shrani"),
|
||||
("Bold", "Krepko"),
|
||||
("Italic", "Ležeče"),
|
||||
("Underline", "Podčrtano"),
|
||||
("Track Changes", "Sledenje spremembam"),
|
||||
("Insert Citation", "Vstavi citat"),
|
||||
("Citations", "Citati"),
|
||||
("unknown future label", "unknown future label"),
|
||||
] {
|
||||
assert_eq!(tr(sl, source), expected, "{source}");
|
||||
}
|
||||
// The search hint's example must find the command it names.
|
||||
assert_eq!(tr(sl, "Insert Table"), "Vstavi tabelo");
|
||||
assert!(tr(sl, "Type a command, e.g. \"insert table\"").contains("vstavi tabelo"));
|
||||
assert_eq!(fmt(tr(sl, "Page {page} of {pages}"), &[("page", "3"), ("pages", "9")]), "Stran 3 od 9");
|
||||
// Count-neutral wording is grammatical for every count (Slovenian has singular, dual and plural).
|
||||
for count in [0, 1, 2, 3, 5, 101, 102] {
|
||||
let words = count.to_string();
|
||||
assert_eq!(fmt(tr(sl, "{words} words"), &[("words", &words)]), format!("Besede: {count}"));
|
||||
assert_eq!(fmt(tr(sl, "{selected} of {words} words"), &[("selected", "1"), ("words", &words)]), format!("Besede: 1 od {count}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_slovenian_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
@@ -1723,17 +1099,3 @@ fn bundled_interface_fonts_cover_slovenian_letters() {
|
||||
}
|
||||
assert!(!lang("sl").uses_cjk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slovenian_has_no_keys_outside_the_other_catalogs() {
|
||||
use std::collections::HashSet;
|
||||
let keys = |source| parse_entries(source).0.into_iter().map(|e| e.source).collect::<HashSet<_>>();
|
||||
let slovenian = keys(lang("sl").0.source);
|
||||
let mut union = HashSet::new();
|
||||
for other in Lang::all().filter(|l| *l != Lang::EN && l.code() != "sl") {
|
||||
union.extend(keys(other.0.source));
|
||||
}
|
||||
let mut extra: Vec<String> = slovenian.difference(&union).cloned().collect();
|
||||
extra.sort();
|
||||
assert!(extra.is_empty(), "sl has {} keys no other catalog has: {extra:?}", extra.len());
|
||||
}
|
||||
|
||||
@@ -912,10 +912,6 @@ mod tests {
|
||||
.expect("navigation label is painted");
|
||||
let bounds = text.visual_bounding_rect();
|
||||
assert!(row.contains_rect(bounds.intersect(clip)), "{language} {percent}% {selected}: {label} paints outside {row:?}");
|
||||
if language == "ru" && id == "accessibility" {
|
||||
assert_eq!(label, "Доступность");
|
||||
assert!(clip.contains_rect(bounds), "{language} {percent}% {selected}: {label} must fit its row");
|
||||
}
|
||||
if language == "en" && ["general", "display", "save"].contains(&id) {
|
||||
assert!(clip.contains_rect(bounds), "short labels must remain fully visible");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hungarian interface
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** trivial (the interface audit lists missing Hungarian entries instead of failing; previously minor (Hungarian interface catalog and regression checks)) · **Target:** WordCraft 0.5.0
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** trivial (interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously trivial: the interface audit lists missing Hungarian entries instead of failing; previously minor: Hungarian interface catalog and regression checks) · **Target:** WordCraft 0.5.0
|
||||
|
||||
Choose **File › Options › Interface language › Magyar**, or follow a Hungarian system display
|
||||
language (`hu-HU`, `hu_HU.UTF-8`). The catalog code is `hu`; `ui.language` with `{"value":"hu"}`
|
||||
@@ -59,12 +59,9 @@ acute capitals.
|
||||
## Verification and remaining scope
|
||||
|
||||
`cargo test -p wordcraft-ui-egui --lib i18n::tests` covers Hungarian locale tags and preference
|
||||
order, saving and restoring `hu`, unchanged document content, every key of the Russian catalog,
|
||||
built-in style names, placeholders, count wording, the font sample's capitals, English fallback,
|
||||
and the glyphs `ÁáÉéÍíÓóÖöŐőÚúÜüŰű` in the bundled Inter (Regular, Medium, SemiBold) and JetBrains Mono. The shared catalog checks
|
||||
(validation, every tab and command translated) include Hungarian automatically, and the on-demand
|
||||
interface audit (`untranslated_interface_text`) lists Hungarian strings it looked up that the
|
||||
catalog lacks.
|
||||
order, saving and restoring `hu`, unchanged document content and the glyphs `ÁáÉéÍíÓóÖöŐőÚúÜüŰű` in
|
||||
the bundled Inter (Regular, Medium, SemiBold) and JetBrains Mono. The shared checks (catalog validation and the missing-translation report, `cargo xtask i18n-report`)
|
||||
include Hungarian automatically.
|
||||
|
||||
Spelling, grammar and hyphenation stay English-only: this is an interface translation, not
|
||||
Hungarian proofing. A native-speaker review of the catalog is welcome.
|
||||
@@ -73,5 +70,7 @@ Hungarian proofing. A native-speaker review of the catalog is welcome.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | trivial | Lookup, context, template, style-name and command-search tests use inline fixture catalogs or the pseudo-language; tests that asserted one language's catalog wording removed |
|
||||
| 2026-10-23 | trivial | Interface-language tests: a missing translation falls back to English and never fails a test; `cargo xtask i18n-report` (part of `cargo xtask ci`) prints the strings each language lacks; single-language coverage tests removed |
|
||||
| 2026-10-23 | trivial | The interface audit lists Hungarian strings the catalog lacks; a missing entry doesn't fail the audit |
|
||||
| 2026-10-11 | minor | Hungarian catalog (2,419 entries: built-in style names, equation gallery, citation styles), conventions, terminology, scope and regression checks |
|
||||
|
||||
+21
-18
@@ -1,6 +1,6 @@
|
||||
# Localization parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (Spanish proofing: es-ES, es-MX, es-419 and es-UY from the installed Hunspell dictionaries, Uruguayan `vos` forms, opening `¿`/`¡` checks, #3360, #3553; previously trivial: interface audit: missing catalog entries are reported per language and don't fail the audit; previously minor: disabled-control reasons in ribbon hints translated in every catalog, #2678; previously trivial: Slovenian interface catalog: every key of every other catalog, 2,614 entries; previously minor: Romanian interface catalog; previously trivial: the File › Home greeting is translated in every catalog; previously trivial: Japanese row: ruby (Phonetic Guide) is supported and HTML export/import keeps it, #1681; previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously minor: English dialects: en-GB, en-CA, en-AU and the other British-spelling Englishes proof their own spellings and flag the other dialect's, #1281, #1045; previously minor: how to install the Hunspell dictionaries on Windows, macOS and Linux: `docs/spelling-dictionaries.md`; previously minor: Read Aloud speaks each sentence in its own language, #1843; previously minor: Indonesian proofing: id-ID spelling from the installed Hunspell dictionary, dictionary-driven suggestions, generic checks with Indonesian messages, #498; previously trivial: print-area strings translated in every catalog; previously trivial: Italian: the interface audit checks the Italian catalog, `Privacy Settings` wording; previously trivial: Italian grammar: `po'`, `qual è`, `È`, `un`/`un'` rules; more elisions and truncations in spelling; previously trivial: Czech interface catalog completed, 2,447 entries; previously trivial: interface audit runs in CI again: size labels translated, 20 strings in every catalog, Read Aloud error race fixed, #1128; previously trivial: Review › Language reaches every language again — Set Proofing Language… and Set As Default, #860; previously minor: Italian proofing: it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar with Italian messages; previously minor: Portuguese proofing; previously trivial: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Hungarian interface added; previously trivial: sorting and table numbers follow the text's language, #836, #837; previously minor: Polish interface; Polish proofing — spelling, grammar, hyphenation — and a proofing language per run; previously minor: Turkish interface added; previously major: first version; catalogs measured from `crates/ui-egui/src/i18n`, Word's languages from its bundle listing) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (Spanish proofing: es-ES, es-MX, es-419 and es-UY from the installed Hunspell dictionaries, Uruguayan `vos` forms, opening `¿`/`¡` checks, #3360, #3553; previously trivial: interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously trivial: interface audit: missing catalog entries are reported per language and don't fail the audit; previously minor: disabled-control reasons in ribbon hints translated in every catalog, #2678; previously trivial: Slovenian interface catalog: every key of every other catalog, 2,614 entries; previously minor: Romanian interface catalog; previously trivial: the File › Home greeting is translated in every catalog; previously trivial: Japanese row: ruby (Phonetic Guide) is supported and HTML export/import keeps it, #1681; previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously minor: English dialects: en-GB, en-CA, en-AU and the other British-spelling Englishes proof their own spellings and flag the other dialect's, #1281, #1045; previously minor: how to install the Hunspell dictionaries on Windows, macOS and Linux: `docs/spelling-dictionaries.md`; previously minor: Read Aloud speaks each sentence in its own language, #1843; previously minor: Indonesian proofing: id-ID spelling from the installed Hunspell dictionary, dictionary-driven suggestions, generic checks with Indonesian messages, #498; previously trivial: print-area strings translated in every catalog; previously trivial: Italian: the interface audit checks the Italian catalog, `Privacy Settings` wording; previously trivial: Italian grammar: `po'`, `qual è`, `È`, `un`/`un'` rules; more elisions and truncations in spelling; previously trivial: Czech interface catalog completed, 2,447 entries; previously trivial: interface audit runs in CI again: size labels translated, 20 strings in every catalog, Read Aloud error race fixed, #1128; previously trivial: Review › Language reaches every language again — Set Proofing Language… and Set As Default, #860; previously minor: Italian proofing: it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar with Italian messages; previously minor: Portuguese proofing; previously trivial: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Hungarian interface added; previously trivial: sorting and table numbers follow the text's language, #836, #837; previously minor: Polish interface; Polish proofing — spelling, grammar, hyphenation — and a proofing language per run; previously minor: Turkish interface added; previously major: first version; catalogs measured from `crates/ui-egui/src/i18n`, Word's languages from its bundle listing) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
Interface languages, script support and proofing per language. How translation works (catalog
|
||||
format, adding a language, clean-room rule): `crates/ui-egui/src/i18n/mod.rs`.
|
||||
@@ -21,8 +21,15 @@ native-speaker review (human).
|
||||
|
||||
- **27 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, Korean `ko` (#1216), `uk`, `es`, `pt-br`,
|
||||
Serbian `sr`/`sr-latn` (#250), Estonian `et`, Turkish `tr`, Russian `ru`, Italian `it` (#594), German `de` (#574), Polish `pl`, Czech `cs`, Hungarian `hu`, French `fr`, Bulgarian `bg`, Vietnamese `vi` (#1434), Swedish `sv` (#1219), Croatian `hr` (#1271), Romanian `ro`, Slovenian `sl`, Dutch `nl` and Danish `da` (#2722). Catalogs have 2,061–2,674 entries
|
||||
each; Polish has every key of every other catalog, including the built-in style names and the equation gallery, except the citation style names only Hungarian, Bulgarian, Swedish, Romanian and Slovenian translate so far (a test checks it). A test (`every_tab_and_command_is_translated`) enforces that every ribbon tab, command
|
||||
label and ribbon location is translated in every language.
|
||||
each; Polish has every key of every other catalog, including the built-in style names and the equation gallery, except the citation style names only Hungarian, Bulgarian, Swedish, Romanian and Slovenian translate so far. A missing translation falls back to English and never fails a test or
|
||||
build. `cargo xtask i18n-report`, part of `cargo xtask ci`, prints per language the interface
|
||||
strings its catalog lacks: everything the interface audit saw looked up, the ribbon tabs, command
|
||||
labels and ribbon locations, the File › Home greetings, disabled-control reasons and the equation
|
||||
gallery. The catalog checks apply to every language alike: format (the file parses, no duplicate
|
||||
keys, placeholders match the English, whitespace, ellipses). Font tests check that the bundled faces have a glyph for each catalog's letters.
|
||||
Tests of the lookup code (English fallback, contexts, placeholder templates, built-in style names,
|
||||
the per-thread language, command search) use small inline catalogs or the pseudo-language, so
|
||||
rewording a translation never fails a test.
|
||||
`pl`, `pl-PL` and `pl_PL.UTF-8` system locales pick Polish automatically.
|
||||
- Of the 675 English strings a scan extracts from `tl!(…)` calls and command labels/locations, each
|
||||
catalog covers **661 (98%)**. Some newer dialog and pane strings (Equation tab tooltips, Zotero,
|
||||
@@ -76,8 +83,7 @@ entries, the same keys as the Russian catalog. Command search folds the Turkish
|
||||
finds Yazı Tipi) while English labels and ASCII command ids keep their usual matching. Turkish
|
||||
proofing is not included; terminology and checks: [`localization-tr.md`](localization-tr.md).
|
||||
Bulgarian (`bg-BG` system locales, `{"value":"bg"}`) has 2,419 entries: every key of the reference
|
||||
catalog, the equation gallery's labels and everything the interface audit finds (the audit checks
|
||||
Bulgarian as it does Russian). Counts use count-neutral wording (`Думи: 21`). Bulgarian text is
|
||||
catalog, the equation gallery's labels and everything the interface audit finds. Counts use count-neutral wording (`Думи: 21`). Bulgarian text is
|
||||
hyphenated by the Bulgarian syllable rules (#826); Bulgarian spelling and grammar are not included.
|
||||
Slovenian (`sl-SI` system locales, `{"value":"sl"}`) has 2,614 entries: every key of every other
|
||||
catalog. Counts use count-neutral wording (`Besede: 21`); terminology: [`localization-sl.md`](localization-sl.md).
|
||||
@@ -88,9 +94,8 @@ the Russian catalog, built-in style names shown translated, plus the citation st
|
||||
|
||||
Czech follows `cs-CZ` system locales and `{"value":"cs"}`, with 2,447 entries: every key of the
|
||||
Russian and Spanish catalogs, including built-in style names and numbered families, the equation
|
||||
gallery, watermarks, paper sizes and preview tiles. The Czech coverage test
|
||||
enforces both reference catalogs and retains the Chinese catalog coverage; counts keep the existing
|
||||
count-neutral wording (`Počet slov: 21`). Czech proofing is not included.
|
||||
gallery, watermarks, paper sizes and preview tiles. Counts use count-neutral wording
|
||||
(`Počet slov: 21`). Czech proofing is not included.
|
||||
|
||||
When no saved proofing-language default exists, Polish, Portuguese and Czech interfaces write
|
||||
new documents and opened TXT/Markdown text in their proofing languages (`pl-PL`, `pt-BR`, `cs-CZ`).
|
||||
@@ -98,18 +103,16 @@ A Spanish interface writes them in the region's Spanish: `es-` plus the system r
|
||||
region's language is Spanish (`es-UY` on a Uruguayan system, `es-MX` on a Mexican one), else
|
||||
Spain's `es-ES` (#3360).
|
||||
|
||||
**Interface audit (runs in CI).** `untranslated_interface_text` lays the app out in a pseudo-language
|
||||
(every tab, File page, dialog, pane, the right-click menu, and a hover and a click on every ribbon
|
||||
control) and fails on visible text that skipped the catalog. It then prints, per language, the
|
||||
strings it looked up that the catalog lacks (`--nocapture`); a missing entry falls back to English
|
||||
and doesn't fail the audit. Proper names (citation styles such as APA or
|
||||
**Interface audit (runs in CI).** `cargo xtask i18n-report` runs `untranslated_interface_text`,
|
||||
which lays the app out in a pseudo-language (every tab, File page, dialog, pane, the right-click
|
||||
menu, and a hover and a click on every ribbon control) and fails on visible text that skipped the
|
||||
catalog. It then prints the missing-translation report described above, which never fails. Proper names (citation styles such as APA or
|
||||
GOST, LaTeX, Unicode) and `bad parameters:` errors, which name a command's JSON parameters for
|
||||
scripts and MCP, stay English. Size labels (Envelopes, Labels, Layout › Size) are translated
|
||||
patterns (`{name} {height} × {width} in`) with the region's decimal separator; paper names
|
||||
other than envelopes (Letter, Legal, A4, B5 (JIS)) stay as they are (#1128).
|
||||
`every_disabled_reason_is_translated` reads every command's `enabled` check from the engine's
|
||||
command sources (the reason a ribbon hint gives for a greyed-out control, #1086) and fails if any
|
||||
catalog lacks one (#2678).
|
||||
The report's disabled-control reasons (the reason a ribbon hint gives for a greyed-out control,
|
||||
#1086, #2678) are read from every command's `enabled` check in the engine's command sources.
|
||||
|
||||
Set Proofing Language › Set As Default keeps the chosen language in user preferences across restarts (#2882), taking precedence over the interface language for blank documents and plain-text/Markdown files. Existing DOCX and built-in English templates keep their own language. Ordinary selection-language changes do not change this preference; older settings keep following the interface language. A rejected command, including a final protected-region check, retains the previous saved default. This does not install dictionaries or add proofing support.
|
||||
|
||||
@@ -147,8 +150,6 @@ Italian (`it`, #594) follows `it`, `it-IT` and `it-CH` system locales. Its catal
|
||||
the others plus 51 strings that are wrapped in `tl!` but were in no catalog yet (File › Info
|
||||
properties, the File › Home greeting, list levels, Equation tab gallery and tooltips). Ribbon tabs,
|
||||
groups and command names use the names Italian Word users know (feature names, as `AGENTS.md`
|
||||
allows); all other text is written from the meaning of the English. The interface audit also
|
||||
checks the Italian catalog (2026-10-14). Italian proofing is in the proofing table above.
|
||||
allows); all other text is written from the meaning of the English. Italian proofing: see the table above.
|
||||
|
||||
A native speaker reviewed the catalog on 2026-10-14 (wording fixes, consistent terms).
|
||||
@@ -218,6 +219,8 @@ WordCraft keeps 2,54 cm).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | trivial | Lookup, context, template, style-name and command-search tests use inline fixture catalogs or the pseudo-language; tests that asserted one language's catalog wording removed |
|
||||
| 2026-10-23 | trivial | Interface-language tests: a missing translation falls back to English and never fails a test; `cargo xtask i18n-report` (part of `cargo xtask ci`) prints the strings each language lacks; single-language coverage tests removed |
|
||||
| 2026-10-23 | trivial | Interface audit: strings a catalog lacks are printed per language, for every language, instead of failing for Russian, Hungarian, Turkish and Bulgarian |
|
||||
| 2026-10-22 | minor | Spanish proofing (#3360, #3553): es-ES, es-MX, es-419 and es-UY each read their installed Hunspell dictionary (RLA-ES, not bundled), Czech's dictionary-driven suggestions, Uruguayan `vos` forms (imperatives included), repeated-word, spacing and opening-mark checks with Spanish messages; Review › Language, the status bar, Set Proofing Language and File › Options › Proofing list the four varieties, and a Spanish interface writes new documents in the region's Spanish; estimates unchanged |
|
||||
| 2026-10-21 | trivial | Japanese row: ruby (Phonetic Guide) is supported over its base text and HTML export/import keeps it (#1681); the row had listed ruby as missing |
|
||||
|
||||
+13
-5
@@ -1,6 +1,6 @@
|
||||
# Slovenian interface
|
||||
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** trivial (catalog completed to every key of every other catalog, 2,614 entries) · **Target:** WordCraft 0.7.0
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-23 · **Change:** trivial (interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously trivial: catalog completed to every key of every other catalog, 2,614 entries) · **Target:** WordCraft 0.7.0
|
||||
|
||||
Choose **File › Options › Interface language › Slovenščina**, or follow a Slovenian system display
|
||||
language (`sl-SI`, `sl_SI.UTF-8`). The catalog code is `sl`; `ui.language` with `{"value":"sl"}`
|
||||
@@ -57,7 +57,15 @@ resources were consulted.
|
||||
|
||||
## Verification and remaining scope
|
||||
|
||||
Tests in `crates/ui-egui/src/i18n/tests.rs` check locale selection, the saved preference, that the
|
||||
catalog covers every key of every other catalog and has no keys outside them, key terms, the search
|
||||
hint example and the bundled fonts' coverage of č, š, ž. Slovenian spelling, grammar and hyphenation
|
||||
are not included. Native speaker review is welcome.
|
||||
Tests in `crates/ui-egui/src/i18n/tests.rs` check locale selection, the saved preference and the
|
||||
bundled fonts' coverage of č, š, ž. The shared checks (catalog validation and the missing-translation report, `cargo xtask i18n-report`)
|
||||
include Slovenian automatically. Slovenian spelling, grammar and hyphenation are not included. Native speaker review
|
||||
is welcome.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | trivial | Lookup, context, template, style-name and command-search tests use inline fixture catalogs or the pseudo-language; tests that asserted one language's catalog wording removed |
|
||||
| 2026-10-23 | trivial | Interface-language tests: a missing translation falls back to English and never fails a test; `cargo xtask i18n-report` (part of `cargo xtask ci`) prints the strings each language lacks; single-language coverage tests removed |
|
||||
| 2026-10-15 | trivial | Catalog completed to every key of every other catalog (2,614 entries) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Swedish interface
|
||||
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-23 · **Change:** trivial (the interface audit lists missing Swedish entries instead of failing; previously minor (Swedish interface catalog and regression checks)) · **Target:** WordCraft 0.6.1
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-23 · **Change:** trivial (interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously trivial: the interface audit lists missing Swedish entries instead of failing; previously minor: Swedish interface catalog and regression checks) · **Target:** WordCraft 0.6.1
|
||||
|
||||
Choose **File › Options › Interface language › Svenska**, or follow a Swedish system display
|
||||
language (`sv-SE`, `sv_SE.UTF-8`, and `sv-FI` for Swedish in Finland). The catalog code is `sv`;
|
||||
@@ -79,13 +79,10 @@ quotes); they are translated as written and still act on English text.
|
||||
## Verification and remaining scope
|
||||
|
||||
`cargo test -p wordcraft-ui-egui --lib i18n::tests` covers Swedish locale tags (`sv`, `sv-SE`,
|
||||
`sv_SE.UTF-8`, `SV-se`, `sv-FI`) and preference order, saving and restoring `sv`, unchanged
|
||||
document content, every key of the reference catalog, no catalog errors, the search hint's example
|
||||
(`infoga tabell`), page and count wording, English fallback, and the glyphs `ÅÄÖåäöÉé` plus every
|
||||
other accented letter the catalog uses in the bundled Inter (Regular, Medium, SemiBold) and
|
||||
JetBrains Mono. The shared catalog checks (validation, every tab and command translated) include
|
||||
Swedish automatically, and the interface audit (`untranslated_interface_text`) lists Swedish
|
||||
strings it looked up that the catalog lacks.
|
||||
`sv_SE.UTF-8`, `SV-se`, `sv-FI`) and preference order, saving and restoring `sv`, unchanged document
|
||||
content and the glyphs `ÅÄÖåäöÉé` plus every other accented letter the catalog uses in the bundled
|
||||
Inter (Regular, Medium, SemiBold) and JetBrains Mono. The shared checks (catalog validation and the missing-translation report, `cargo xtask i18n-report`)
|
||||
include Swedish automatically.
|
||||
|
||||
Spelling, grammar and hyphenation stay English-only: this is an interface translation, not
|
||||
Swedish proofing.
|
||||
@@ -94,5 +91,7 @@ Swedish proofing.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | trivial | Lookup, context, template, style-name and command-search tests use inline fixture catalogs or the pseudo-language; tests that asserted one language's catalog wording removed |
|
||||
| 2026-10-23 | trivial | Interface-language tests: a missing translation falls back to English and never fails a test; `cargo xtask i18n-report` (part of `cargo xtask ci`) prints the strings each language lacks; single-language coverage tests removed |
|
||||
| 2026-10-23 | trivial | The interface audit lists Swedish strings the catalog lacks; a missing entry doesn't fail the audit |
|
||||
| 2026-10-13 | minor | Swedish catalog (2,504 entries), conventions, terminology, known compromises and regression checks |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Turkish interface
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Turkish interface catalog completed, 2,325 entries, #774; previously minor: Turkish interface catalog and regression checks) · **Target:** WordCraft 0.5.0
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** trivial (interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously minor: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Turkish interface catalog and regression checks) · **Target:** WordCraft 0.5.0
|
||||
|
||||
Choose **File › Options › Interface language › Türkçe**, or follow a Turkish system display language
|
||||
(`tr-TR`, `tr-CY`, `tr_TR.UTF-8`). The catalog code is `tr`; `ui.language` with `{"value":"tr"}`
|
||||
@@ -46,13 +46,11 @@ still work. Other interface languages and document search are unchanged.
|
||||
## Verification and remaining scope
|
||||
|
||||
`cargo test -p wordcraft-ui-egui --lib i18n::tests` covers Turkish locale tags and preference order,
|
||||
saving and restoring `tr`, unknown preferences, unchanged document content, full key parity with
|
||||
the Russian catalog (and superset of Spanish and Chinese), placeholders, count wording, the font
|
||||
sample's capitals, English fallback, and the glyphs `ÇçĞğİıÖöŞşÜü` in the bundled Inter (Regular,
|
||||
Medium, SemiBold) and JetBrains Mono. A dialog test renders real command-search results in Turkish
|
||||
and English, including dotted and dotless I and ASCII command ids. The shared catalog checks
|
||||
(validation, every tab and command translated, and interface audit strings) include Turkish
|
||||
automatically.
|
||||
saving and restoring `tr`, unknown preferences, unchanged document content and the glyphs
|
||||
`ÇçĞğİıÖöŞşÜü` in the bundled Inter (Regular, Medium, SemiBold) and JetBrains Mono. A command-search
|
||||
test checks dotted and dotless I under Turkish casing with fixture labels, and that English labels
|
||||
and ASCII command ids still match. The shared checks (catalog validation and the missing-translation report, `cargo xtask i18n-report`)
|
||||
include Turkish automatically.
|
||||
|
||||
Turkish proofing (`crates/proof/src/tr`) provides spelling against installed Hunspell `tr_TR`
|
||||
dictionaries, spelling suggestions tuned for Turkish diacritics and detached particles, rule-based grammar
|
||||
@@ -62,6 +60,8 @@ checks (reduplications, conjunction commas, capitalization), and syllable hyphen
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | trivial | Lookup, context, template, style-name and command-search tests use inline fixture catalogs or the pseudo-language; tests that asserted one language's catalog wording removed |
|
||||
| 2026-10-23 | trivial | Interface-language tests: a missing translation falls back to English and never fails a test; `cargo xtask i18n-report` (part of `cargo xtask ci`) prints the strings each language lacks; single-language coverage tests removed |
|
||||
| 2026-10-13 | minor | Turkish proofing added: spelling, suggestions, grammar rules and TDK syllable hyphenation |
|
||||
| 2026-10-11 | minor | Turkish catalog completed (2,325 entries, #774), covering all math structures, built-in styles, presets and preview tiles |
|
||||
| 2026-10-11 | minor | Turkish catalog (1,307 entries), terminology, scope and regression checks |
|
||||
|
||||
+29
-11
@@ -21,8 +21,11 @@ commands:
|
||||
assets check that every icon/image/font/asset is attributed in ATTRIBUTION.md
|
||||
layers enforce the crate dependency layering (plan/architecture.md §3)
|
||||
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates (one call)
|
||||
test workspace tests (cargo nextest when installed, else cargo test), then doctests
|
||||
ci fmt --check, clippy -D warnings, test, doctest, assets, layers, wasm
|
||||
test workspace tests (cargo nextest when installed, else cargo test), the interface
|
||||
audit with its missing-translation report, then doctests
|
||||
i18n-report the interface audit: fails on text that skips the catalog, and prints the
|
||||
strings each language has no translation for (never a failure)
|
||||
ci fmt --check, clippy -D warnings, test, i18n-report, doctest, assets, layers, wasm
|
||||
(stops at first failure)
|
||||
corpus [--download]
|
||||
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
|
||||
@@ -42,7 +45,8 @@ fn main() -> ExitCode {
|
||||
Some("assets") => assets::run(&root()),
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("wasm") => cmd_wasm(),
|
||||
Some("test") => cmd_test().and_then(|()| cmd_doctest()),
|
||||
Some("test") => cmd_test().and_then(|()| cmd_i18n_report()).and_then(|()| cmd_doctest()),
|
||||
Some("i18n-report") => cmd_i18n_report(),
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
|
||||
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
|
||||
@@ -165,18 +169,31 @@ fn nextest_available() -> bool {
|
||||
cargo().args(["nextest", "--version"]).stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success())
|
||||
}
|
||||
|
||||
/// The interface audit, which `cmd_i18n_report` runs on its own so its report is printed.
|
||||
const I18N_AUDIT: &str = "i18n::audit::untranslated_interface_text";
|
||||
const I18N_AUDIT_FILTER: &str = "package(wordcraft-ui-egui) & test(=i18n::audit::untranslated_interface_text)";
|
||||
|
||||
fn cmd_test() -> Result<(), String> {
|
||||
test_run(&["-E", &format!("not ({I18N_AUDIT_FILTER})")], &["--skip", I18N_AUDIT])
|
||||
}
|
||||
|
||||
/// Missing translations never fail the build: the report lists them in the log.
|
||||
fn cmd_i18n_report() -> Result<(), String> {
|
||||
test_run(&["-E", I18N_AUDIT_FILTER, "--no-capture"], &["--exact", I18N_AUDIT, "--nocapture"])
|
||||
}
|
||||
|
||||
/// A test run over `TEST_TARGETS`, with nextest arguments or, without nextest, libtest arguments.
|
||||
/// Every run builds the same targets, so a second run reuses the first one's build.
|
||||
fn test_run(nextest: &[&str], libtest: &[&str]) -> Result<(), String> {
|
||||
let mut c = cargo();
|
||||
let tool = if nextest_available() {
|
||||
c.args(["nextest", "run"]);
|
||||
"nextest run"
|
||||
if nextest_available() {
|
||||
c.args(["nextest", "run"]).args(TEST_TARGETS).args(nextest);
|
||||
run(c, &format!("cargo nextest run {} {}", TEST_TARGETS.join(" "), nextest.join(" ")))
|
||||
} else {
|
||||
eprintln!("note: cargo-nextest is not installed, running cargo test (cargo install cargo-nextest --locked)");
|
||||
c.arg("test");
|
||||
"test"
|
||||
};
|
||||
c.args(TEST_TARGETS);
|
||||
run(c, &format!("cargo {tool} {}", TEST_TARGETS.join(" ")))
|
||||
c.arg("test").args(TEST_TARGETS).arg("--").args(libtest);
|
||||
run(c, &format!("cargo test {} -- {}", TEST_TARGETS.join(" "), libtest.join(" ")))
|
||||
}
|
||||
}
|
||||
|
||||
/// nextest does not run doctests.
|
||||
@@ -206,6 +223,7 @@ fn cmd_ci() -> Result<(), String> {
|
||||
}),
|
||||
),
|
||||
("test", Box::new(cmd_test)),
|
||||
("i18n-report", Box::new(cmd_i18n_report)),
|
||||
("doctest", Box::new(cmd_doctest)),
|
||||
("assets", Box::new(|| assets::run(&root()))),
|
||||
("layers", Box::new(cmd_layers)),
|
||||
|
||||
Reference in New Issue
Block a user