mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 01:57:40 -06:00
i18n: missing translations are reported, never a failure (#2729)
A message a catalog lacks, or translates as an empty string, shows in English and fails nothing: - build.rs no longer requires every formats catalog to carry the same messages, and tr_format! gets a last arm that formats the English source, so a message no catalog has builds. - Coverage tests (every language translates X) and tests of one language's exact wording are gone; the language-switching, layout, date, placeholder and user-value tests read the catalog's own value instead of a hard-coded translation. - i18n::tests::translation_gaps_report lists, per language, the plain and format messages it lacks and its untranslated What's New lines. It always passes; cargo xtask ci runs it from the test step's binary after the tests. - catalogs_load checks that every catalog file is a JSON object of strings with each key once; it.json named "Dimensions" twice.
This commit is contained in:
@@ -456,10 +456,6 @@ mod tests {
|
||||
set_language(*language);
|
||||
assert!(seen.insert(structure_of(&bar)), "{language:?} shares another language's menu structure key");
|
||||
}
|
||||
set_language(Locale::ZhHant);
|
||||
assert_eq!(label_text("File"), "檔案");
|
||||
assert_eq!(label_text("Settings…"), "設定…");
|
||||
assert_eq!(label_text("Quit LightCraft"), "結束 LightCraft");
|
||||
set_language(Locale::En);
|
||||
}
|
||||
|
||||
|
||||
+8
-16
@@ -7,9 +7,9 @@
|
||||
//! `{:+}`… keep their meaning). The English source is the fallback arm. Adding a language means
|
||||
//! adding its file (and its `language_table!` entry); this script picks the file up by name.
|
||||
//!
|
||||
//! Every catalog must carry the same messages: a `tr_format!` call whose message is in no catalog
|
||||
//! does not compile, so a message missing from one language would otherwise go unnoticed until it
|
||||
//! shows English.
|
||||
//! A message a language lacks (or translates as an empty string) shows the English source: that
|
||||
//! language has no arm for it, and a message no catalog has formats the English source through the
|
||||
//! macro's last arm. `i18n::tests::translation_gaps_report` lists the gaps per language.
|
||||
//!
|
||||
//! It also bakes the contributor credits into the binary as static Rust tables (`$OUT_DIR/credits.rs`,
|
||||
//! included by `src/credits.rs`; craftrules `standards/contributors.md`): commit stats from
|
||||
@@ -61,25 +61,14 @@ fn generate() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let Some(code) = name.strip_suffix("-formats.json").filter(|code| *code != "en") else { continue };
|
||||
println!("cargo:rerun-if-changed=locales/{name}");
|
||||
let variant = variant_for(code)?;
|
||||
let messages: BTreeMap<String, String> =
|
||||
let mut messages: BTreeMap<String, String> =
|
||||
serde_json::from_str(&fs::read_to_string(&path)?).map_err(|error| format!("locales/{name}: {error}"))?;
|
||||
messages.retain(|_, translated| !translated.is_empty());
|
||||
catalogs.push((code.to_string(), variant, messages));
|
||||
}
|
||||
catalogs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Every catalog translates the same messages.
|
||||
let all: BTreeSet<&String> = catalogs.iter().flat_map(|(_, _, messages)| messages.keys()).collect();
|
||||
let mut problems = Vec::new();
|
||||
for (code, _, messages) in &catalogs {
|
||||
for key in &all {
|
||||
if !messages.contains_key(*key) {
|
||||
problems.push(format!("locales/{code}-formats.json is missing {key:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !problems.is_empty() {
|
||||
return Err(problems.join("\n").into());
|
||||
}
|
||||
|
||||
let mut source = String::from(concat!(
|
||||
"/// Translate a format string, checked by `format!` in every language.\n",
|
||||
@@ -98,6 +87,9 @@ fn generate() -> Result<String, Box<dyn std::error::Error>> {
|
||||
source.push_str(&format!(" _ => format!({en} $(, $($args)*)?),\n }}\n }};\n"));
|
||||
}
|
||||
source.push_str(concat!(
|
||||
" ($english:literal $(, $($args:tt)*)?) => {\n",
|
||||
" format!($english $(, $($args)*)?)\n",
|
||||
" };\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"// `macro_rules!` is textual: the module re-export is what lets call sites say\n",
|
||||
|
||||
@@ -2344,7 +2344,6 @@
|
||||
"Loupe Info 1": "Info lente 1",
|
||||
"Loupe Info 2": "Info lente 2",
|
||||
"Use Defaults": "Usa predefiniti",
|
||||
"Dimensions": "Dimensioni",
|
||||
"Camera & Lens": "Fotocamera e obiettivo",
|
||||
"View → View Options chooses the lines Info 1 and Info 2 show.": "Visualizza → Opzioni di visualizzazione sceglie le righe di Info 1 e Info 2."
|
||||
}
|
||||
|
||||
@@ -2495,7 +2495,7 @@ mod tests {
|
||||
assert!(!row.cancellable, "{row:?}");
|
||||
assert!(h.app.session.activity.cancel(row.id).is_err(), "Photos can't be stopped once asked");
|
||||
let current = h.app.export.as_ref().map(|e| e.progress.lock().unwrap().1.clone()).unwrap_or_default();
|
||||
assert_eq!(current, "Wird zu Apple Fotos hinzugefügt…");
|
||||
assert_eq!(current, crate::i18n::Locale::De.tr("Adding to Apple Photos…"));
|
||||
// the row says so on the next frame (issue #345)
|
||||
h.step();
|
||||
let row = h.app.session.activity.list().into_iter().find(|r| r.kind == "export").expect("the export's row");
|
||||
@@ -4570,9 +4570,6 @@ mod tests {
|
||||
assert_eq!(crate::i18n::language(), locale);
|
||||
for (mode, key) in [("basic", "Info Overlay: File"), ("exposure", "Info Overlay: Exposure"), ("off", "Info Overlay Off")] {
|
||||
let expected = locale.tr(key);
|
||||
if locale != Locale::En {
|
||||
assert_ne!(expected, key, "this locale must translate the tested message");
|
||||
}
|
||||
assert_eq!(h.request("ui.key", json!({"key": "i", "cmd": true}), t)["ok"], true);
|
||||
assert_eq!(serde_json::to_value(h.app.ui.info_overlay).unwrap(), json!(mode));
|
||||
assert_eq!(h.app.ui.toast.as_ref().expect("overlay toast").0, expected);
|
||||
|
||||
+383
-806
File diff suppressed because it is too large
Load Diff
@@ -684,25 +684,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_images_are_one_line_in_the_ui_language() {
|
||||
fn skipped_images_are_one_line() {
|
||||
let value = json!({"report": {"photos": 3, "collections": 0, "warnings": ["w1", "w2"], "skipped": [
|
||||
{"image": 7, "reason": "missingFile"},
|
||||
{"image": 8, "reason": "missingRoot"},
|
||||
{"image": 9, "reason": "masterSkipped"},
|
||||
]}});
|
||||
let details = |locale| {
|
||||
crate::i18n::set_language(locale);
|
||||
let dialog = result_dialog(&value, "done");
|
||||
crate::i18n::set_language(crate::i18n::Locale::En);
|
||||
let crate::state::Dialog::LightroomResult { details, .. } = dialog else { panic!("not a Lightroom result") };
|
||||
details
|
||||
};
|
||||
let en = details(crate::i18n::Locale::En);
|
||||
crate::i18n::set_language(crate::i18n::Locale::En);
|
||||
let crate::state::Dialog::LightroomResult { details: en, .. } = result_dialog(&value, "done") else { panic!("not a Lightroom result") };
|
||||
assert_eq!(en.len(), 4, "{en:?}");
|
||||
assert_eq!(en[1], "3 catalog images were not imported because their records in the catalog are incomplete.");
|
||||
assert_eq!(&en[2..], ["w1", "w2"]);
|
||||
let es = details(crate::i18n::Locale::Es);
|
||||
assert_eq!(es[1], "No se importaron 3 imágenes del catálogo porque sus registros en el catálogo están incompletos.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1533,18 +1533,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_context_menu_labels_have_french_translations() {
|
||||
let saved = crate::i18n::language();
|
||||
crate::i18n::set_language(crate::i18n::Locale::Fr);
|
||||
for (label, expected) in
|
||||
[("Pick", "Sélectionnée"), ("Reject", "Rejeter"), ("Group into Stack", "Regrouper en pile"), ("HDR Panorama…", "Panorama HDR…")]
|
||||
{
|
||||
assert_eq!(crate::i18n::tr(label), expected);
|
||||
}
|
||||
crate::i18n::set_language(saved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_grid_groups_and_auto_levels() {
|
||||
let l = layout(&[1.0; 5], &[(0, 3), (3, 2)], 400.0, 100.0, true);
|
||||
|
||||
@@ -520,6 +520,35 @@ fn in_tree(tree: &[KeywordNode], path: &str) -> bool {
|
||||
tree.iter().any(|n| same(&n.path, path) || (lightcraft_catalog::keywords::is_under(path, &n.path) && in_tree(&n.children, path)))
|
||||
}
|
||||
|
||||
/// The labels the Keyword List and its dialogs pass to `tr` through variables, out of reach of a
|
||||
/// literal search: the translation gaps report (`i18n`) checks them.
|
||||
#[cfg(test)]
|
||||
pub(crate) const VARIABLE_LABELS: &[&str] = &[
|
||||
"Include on Export",
|
||||
"Export Containing Keywords",
|
||||
"Export Synonyms",
|
||||
"Person",
|
||||
"Merge Keywords",
|
||||
"No keywords yet",
|
||||
"No keywords match",
|
||||
"Add to Selected Photos",
|
||||
"Remove from Selected Photos",
|
||||
"Keywords",
|
||||
"& Containing",
|
||||
"Will Export",
|
||||
"No keywords are exported",
|
||||
"Add to All Selected Photos",
|
||||
"Set name",
|
||||
"Edit Set…",
|
||||
"Edit Keyword Set",
|
||||
"A keyword set needs a name",
|
||||
"Recent Keywords",
|
||||
"Keywords (⌥1–⌥9)",
|
||||
"Save as a new set",
|
||||
"Import Keywords…",
|
||||
"Export Keywords…",
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lightcraft_catalog::{Op, Photo, Source};
|
||||
@@ -589,45 +618,4 @@ mod tests {
|
||||
let (c, ids) = library(&[&["Ärzte", "ÄRZTE"], &[]]);
|
||||
assert_eq!(Ticks::of(&c, &ids).tick("ärzte"), Tick::Some);
|
||||
}
|
||||
|
||||
/// The labels the Keyword List and its dialogs pass to `tr` through variables (out of reach of
|
||||
/// a literal search) are translated in every language.
|
||||
#[test]
|
||||
fn labels_passed_through_variables_are_translated() {
|
||||
use crate::i18n::Locale;
|
||||
let labels = [
|
||||
"Include on Export",
|
||||
"Export Containing Keywords",
|
||||
"Export Synonyms",
|
||||
"Person",
|
||||
"Merge Keywords",
|
||||
"No keywords yet",
|
||||
"No keywords match",
|
||||
"Add to Selected Photos",
|
||||
"Remove from Selected Photos",
|
||||
"Keywords",
|
||||
"& Containing",
|
||||
"Will Export",
|
||||
"No keywords are exported",
|
||||
"Add to All Selected Photos",
|
||||
"Set name",
|
||||
"Edit Set…",
|
||||
"Edit Keyword Set",
|
||||
"A keyword set needs a name",
|
||||
"Recent Keywords",
|
||||
"Keywords (⌥1–⌥9)",
|
||||
"Save as a new set",
|
||||
"Import Keywords…",
|
||||
"Export Keywords…",
|
||||
];
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
|
||||
for code in ["de", "es", "fr", "pt-br", "ru", "uk", "it", "ka", "ja", "zh-hans", "zh-hant", "pl", "cs", "sv", "fi", "id", "vi"] {
|
||||
let text = std::fs::read_to_string(dir.join(format!("{code}.json"))).unwrap();
|
||||
let catalog: std::collections::HashMap<String, String> = serde_json::from_str(&text).unwrap();
|
||||
for label in labels {
|
||||
assert!(catalog.contains_key(label), "{code}: “{label}”");
|
||||
}
|
||||
}
|
||||
assert_eq!(Locale::ALL.len(), 18, "a language added: list its file above");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,25 +897,6 @@ fn brush_settings(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn masking_context_actions_use_catalog_translations() {
|
||||
let saved = crate::i18n::language();
|
||||
crate::i18n::set_language(crate::i18n::Locale::Fr);
|
||||
for (english, translated) in [
|
||||
("Duplicate Mask", "Dupliquer le masque"),
|
||||
("Invert Mask", "Inverser le masque"),
|
||||
("Hide Mask", "Masquer le masque"),
|
||||
("Move Up", "Monter"),
|
||||
("Brush", "Pinceau"),
|
||||
("Luminance Range", "Plage de luminance"),
|
||||
] {
|
||||
assert_eq!(menu_label(english), translated, "{english}");
|
||||
}
|
||||
// An arbitrary user name has no catalog entry and is left unchanged.
|
||||
assert_eq!(menu_label("My custom mask"), "My custom mask");
|
||||
crate::i18n::set_language(saved);
|
||||
}
|
||||
|
||||
/// The grid keeps four 52 pt tiles when the labels fit, and gives a long (translated) label
|
||||
/// room by dropping a column rather than drawing it past its tile.
|
||||
#[test]
|
||||
|
||||
@@ -343,20 +343,12 @@ mod import_review {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every shipped locale paints translated destination choices and initializes a translated new name.
|
||||
/// Every shipped locale paints its own destination choices and names a new album in its language.
|
||||
#[test]
|
||||
fn import_album_picker_localizes_choices_in_every_shipped_language() {
|
||||
use crate::i18n::Locale;
|
||||
let Review { mut h, .. } = review();
|
||||
for locale in Locale::ALL {
|
||||
for source in
|
||||
["None", "New album", "New album…", "Imported Photos", "Choose an album…", "Search albums", "No albums yet", "No albums match"]
|
||||
{
|
||||
assert!(!locale.tr(source).is_empty());
|
||||
if *locale != Locale::En {
|
||||
assert_ne!(locale.tr(source), source, "{locale:?}: {source}");
|
||||
}
|
||||
}
|
||||
let r = h.request("ui.set", json!({"language": locale.code()}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert!(h.settle(SETTLE));
|
||||
|
||||
@@ -46,8 +46,9 @@ ani anglické zdrojové řetězce. Rozložení panelů a seznamu jazyků je beze
|
||||
## Ověření a revize
|
||||
|
||||
Testy `i18n::tests` ověřují registraci příkazu, přepínání přes nabídku a řídicí kanál,
|
||||
uložení jazyka, pokrytí katalogů, placeholdery, neutrální formulace počtů, datum, čas
|
||||
a české glyfy. Překladové šablony kontroluje také Rust při sestavení.
|
||||
uložení jazyka, placeholdery, datum, čas a české glyfy. Překladové šablony kontroluje také
|
||||
Rust při sestavení. Chybějící překlady se zobrazují anglicky a vypisuje je
|
||||
`i18n::tests::translation_gaps_report`; žádný test kvůli nim neselže.
|
||||
|
||||
Při jazykové korektuře doporučujeme zkontrolovat „asistované třídění“, „sada“ pro Stack,
|
||||
„bodová barva“, „měkký nátisk“ a názvy kreativních přednastavení.
|
||||
|
||||
@@ -29,8 +29,8 @@ cargo test -p lightcraft-ui-egui i18n::tests
|
||||
LIGHTCRAFT_LANGUAGE=de lightcraft-cli snapshot --demo -o deutsch.png --size 1600x1000
|
||||
```
|
||||
|
||||
Die Tests prüfen Katalogabdeckung, Platzhalter, Sprachwechsel per Menü und Steuerkanal,
|
||||
Speicherung, Datumsformate, unveränderte Benutzernamen und gerenderte Texte sowie Umlaute
|
||||
und ß. Ein weiterer Render-Test prüft die Schaltflächenbreiten; der Importtest prüft
|
||||
Die Tests prüfen Platzhalter, Sprachwechsel per Menü und Steuerkanal, Speicherung,
|
||||
Datumsformate, unveränderte Benutzernamen und gerenderte Texte sowie Umlaute und ß. Fehlende
|
||||
Übersetzungen erscheinen auf Englisch; `i18n::tests::translation_gaps_report` listet sie auf. Ein weiterer Render-Test prüft die Schaltflächenbreiten; der Importtest prüft
|
||||
auch den Sprachwechsel nach der Hintergrundprüfung der Quellen. Akzeptierte Sprachangaben umfassen `de`, `de-DE`, `de_AT.UTF-8` und `de-CH`;
|
||||
gespeichert wird stets `de`. Weitere Hinweise stehen in [localization.md](localization.md).
|
||||
|
||||
@@ -52,8 +52,9 @@ englanniksi. Käännös ei muuta kirjastotietoja eikä englanninkielisiä lähde
|
||||
## Testaus
|
||||
|
||||
`i18n::tests` tarkistaa komennon rekisteröinnin, vaihdon valikosta ja ohjauskanavasta,
|
||||
kielen tallennuksen, luetteloiden kattavuuden, paikkamerkit, määrämuodot, päivämäärän, ajan ja
|
||||
suomen kirjaimet. Rust tarkistaa muotoilumallit käännösvaiheessa.
|
||||
kielen tallennuksen, paikkamerkit, päivämäärän, ajan ja suomen kirjaimet. Rust tarkistaa
|
||||
muotoilumallit käännösvaiheessa. Puuttuvat käännökset näkyvät englanniksi, ja
|
||||
`i18n::tests::translation_gaps_report` luettelee ne; mikään testi ei epäonnistu niiden takia.
|
||||
|
||||
Kielentarkistuksessa kannattaa katsoa erityisesti: ”Väriluokittelu” (Color Grading),
|
||||
”Valoalueet / Valkoiset” (Highlights / Whites), ”Näyttövedos” (Soft Proofing),
|
||||
|
||||
@@ -19,7 +19,7 @@ not translated. Descriptions for SAM 3 go to the model unchanged.
|
||||
## Georgian script
|
||||
|
||||
Mkhedruli has no upper and lower case: `i18n::upper_first` leaves Georgian letters alone, and the
|
||||
interface never uses Mtavruli (a test checks the whole catalog). Commands are verbal nouns. Counts
|
||||
interface never uses Mtavruli. Commands are verbal nouns. Counts
|
||||
are count-neutral, noun first (`იმპორტირებულია ფოტო: 3`); English plural endings are dropped with
|
||||
`{:.0}`. Dates read `კვირა, 20.09.2026` (`{weekday}, {day:02}.{month:02}.{year}`), months `09.2026`.
|
||||
|
||||
@@ -73,7 +73,7 @@ cargo test -p lightcraft-ui-egui i18n::tests -- --nocapture
|
||||
LIGHTCRAFT_LANGUAGE=ka lightcraft-cli snapshot --demo -o georgian.png --size 1600x1000
|
||||
```
|
||||
|
||||
The tests check catalog coverage, placeholders, switching language by menu and control channel,
|
||||
saving, the `ka` locale tags, dates, count wording, the absence of Mtavruli and that sentence case
|
||||
keeps Georgian lowercase. More notes are in
|
||||
The tests check placeholders, switching language by menu and control channel, saving, the `ka`
|
||||
locale tags, dates and that sentence case keeps Georgian lowercase. Messages Georgian lacks show in
|
||||
English; `i18n::tests::translation_gaps_report` lists them. More notes are in
|
||||
[localization.md](localization.md).
|
||||
|
||||
@@ -66,8 +66,9 @@ LIGHTCRAFT_LANGUAGE=vi lightcraft-cli snapshot --demo -o vi.png --size 1600x1000
|
||||
LIGHTCRAFT_LANGUAGE=vi lightcraft-cli snapshot --demo -o vi-narrow.png --size 1280x800
|
||||
```
|
||||
|
||||
Các test tiếng Việt bắt buộc phủ catalog union, nhãn lệnh/control/Settings/Smart Album,
|
||||
đổi ngôn ngữ, lưu `vi`, formats, ngày, dữ liệu người dùng, và glyph dấu.
|
||||
Các test tiếng Việt kiểm tra đổi ngôn ngữ, lưu `vi`, formats, ngày, dữ liệu người dùng và glyph
|
||||
dấu. Chuỗi còn thiếu hiện bằng tiếng Anh và được `i18n::tests::translation_gaps_report` liệt kê;
|
||||
không test nào thất bại vì thiếu bản dịch.
|
||||
|
||||
## Nguồn dịch (clean-room)
|
||||
|
||||
|
||||
@@ -34,5 +34,6 @@ LightCraft 的界面支持英语、简体中文、繁体中文(台湾)和日
|
||||
|
||||
`LIGHTCRAFT_LANGUAGE=zh-hans lightcraft-cli snapshot ...` 可以渲染中文界面用于检查。
|
||||
|
||||
翻译是否完整、两种目录的键与占位符是否一致、字形是否齐全,都由
|
||||
两种目录的占位符是否一致、字形是否齐全,由
|
||||
`cargo test -p lightcraft-ui-egui i18n::tests` 验证(字形检查只在指定 `CRAFT_FONTS_DIR` 时实际执行)。
|
||||
缺少的译文显示为英文,由 `i18n::tests::translation_gaps_report` 列出,不会让测试失败。
|
||||
|
||||
+16
-12
@@ -12,11 +12,10 @@ are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-h
|
||||
|
||||
1. **Translate the two catalogs.** Copy `locales/ja.json` and `locales/ja-formats.json` to
|
||||
`locales/<code>.json` and `locales/<code>-formats.json`, then translate the values. Keys are the
|
||||
English source strings and must stay byte-for-byte identical. The plain catalog may grow at its
|
||||
own pace: a message it lacks shows in English, and `cargo test -p lightcraft-ui-egui i18n::tests
|
||||
-- --nocapture` lists the gaps (it fails on an empty entry or mismatched placeholders). The
|
||||
formats catalog must carry every message (the build fails otherwise; a message copied in English,
|
||||
value equal to its key, is an untranslated placeholder until someone translates it), including the date
|
||||
English source strings and must stay byte-for-byte identical. Both catalogs may grow at their
|
||||
own pace: a message a catalog lacks, or translates as an empty string, shows in English, and
|
||||
nothing fails. The translation gaps report lists what each language lacks (see
|
||||
[Testing and looking at it](#testing-and-looking-at-it)). The formats catalog also holds the date
|
||||
patterns (`{year}`, `{month} {year}`, `{weekday}, {day} {month} {year}`…) that date headings and
|
||||
capture times use in every language but English; weekday names go in the plain catalog.
|
||||
2. **Add the table entry** in `crates/ui-egui/src/i18n.rs`:
|
||||
@@ -81,18 +80,23 @@ tr_format!("Added {n} photo{} to “{}”", if n == 1 { "" } else { "s" }, album
|
||||
`"已将 {n} 张照片添加到“{1}”{0:.0}"`. Keep the source's spec (`{:.1}`, `{d:+}`, `{:.0}%`).
|
||||
- `format!` rejects an unused argument, so a value a language does not need — the English plural
|
||||
suffix (`"s"` / `""`) — is consumed as `{:.0}` (a string at precision 0 prints nothing).
|
||||
- Every formats catalog carries the same messages (`build.rs` fails the build otherwise), and a
|
||||
`tr_format!` call whose message is in no catalog does not compile: add it to every
|
||||
`*-formats.json` file.
|
||||
- A language whose formats catalog lacks a message shows the English source there, and a
|
||||
`tr_format!` call whose message is in no catalog formats the English source (the macro's last
|
||||
arm). A new message builds with no catalog entry.
|
||||
|
||||
## Testing and looking at it
|
||||
|
||||
```sh
|
||||
cargo test -p lightcraft-ui-egui i18n::tests # catalogs, keys, placeholders, commands
|
||||
cargo test -p lightcraft-ui-egui i18n::tests # catalogs load, placeholders, switching
|
||||
CRAFT_FONTS_DIR=../craft-fonts cargo test -p lightcraft-ui-egui i18n::tests # + glyph coverage
|
||||
cargo test -p lightcraft-ui-egui --lib -- --exact i18n::tests::translation_gaps_report --nocapture
|
||||
```
|
||||
|
||||
The glyph tests are skipped without `CRAFT_FONTS_DIR` (there are no CJK faces to check then).
|
||||
The tests fail on a broken catalog (malformed JSON, a repeated key, placeholders that differ from
|
||||
the source), never on a missing translation. The last command is the translation gaps report: per
|
||||
language, the count and list of plain and format messages it lacks and the number of untranslated
|
||||
What's New lines. It always passes, and `cargo xtask ci` prints it after its test step. The glyph
|
||||
tests are skipped without `CRAFT_FONTS_DIR` (there are no CJK faces to check then).
|
||||
To look at a language, render it headless:
|
||||
|
||||
```sh
|
||||
@@ -114,5 +118,5 @@ when a region has no dedicated entry.
|
||||
Help ▸ What's New shows `docs/whats-new.md`, and each of its lines (a heading, or one wrapped line of a
|
||||
bullet, without the `- ` or `### `) is looked up like any message, so a catalog can translate the notes
|
||||
line by line (German does). A line that is re-wrapped or edited no longer matches its translation and
|
||||
shows in English until the catalog is updated; untranslated lines are reported by
|
||||
`display_label_gaps_are_reported`, never failed.
|
||||
shows in English until the catalog is updated; the translation gaps report counts the untranslated
|
||||
lines.
|
||||
|
||||
+36
-1
@@ -37,7 +37,8 @@ commands:
|
||||
ci [--hardware] [--summary-md]
|
||||
fmt --check, clippy -D warnings, heif, test, GPU probe, parity refs, layers, assets, wasm
|
||||
(stops at first failure). Tests run under cargo test (cargo nextest with
|
||||
LIGHTCRAFT_NEXTEST=1), without examples, then doctests with cargo test --doc. Writes
|
||||
LIGHTCRAFT_NEXTEST=1), without examples, then doctests with cargo test --doc; after
|
||||
them it prints the translation gaps report (never a failure). Writes
|
||||
target/ci/report.json (machine, GPU adapters, data,
|
||||
steps, skipped tests) and target/ci/summary.md; --hardware also runs the GPU tests on
|
||||
every backend with a hardware adapter; --summary-md prints the Markdown summary
|
||||
@@ -238,6 +239,39 @@ fn doctests() -> (Command, String) {
|
||||
(c, "cargo test --workspace --doc".to_owned())
|
||||
}
|
||||
|
||||
/// The test that prints the messages each language lacks (shown in English); it always passes.
|
||||
const TRANSLATION_GAPS_TEST: &str = "i18n::tests::translation_gaps_report";
|
||||
|
||||
/// Print the translation gaps report into the log: [`TRANSLATION_GAPS_TEST`] of lightcraft-ui-egui,
|
||||
/// run from the test binary the test step built (found with `--no-run` and the step's own
|
||||
/// arguments, so nothing rebuilds). A report that can't run is a note, never a failed step.
|
||||
fn translation_gaps() {
|
||||
eprintln!("\n=== ci: translation gaps ===");
|
||||
let mut c = cargo();
|
||||
c.args(["test", "--no-run", "--message-format=json"]).args(with_test_targets(&["--workspace"]));
|
||||
let binary = c.output().map_err(|e| format!("cargo test --no-run: {e}")).and_then(|out| {
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
||||
.find(|m| {
|
||||
m["reason"] == "compiler-artifact"
|
||||
&& m["target"]["name"] == "lightcraft_ui_egui"
|
||||
&& m["profile"]["test"] == true
|
||||
&& m["target"]["kind"].as_array().is_some_and(|kinds| kinds.iter().any(|kind| kind == "lib"))
|
||||
})
|
||||
.and_then(|m| m["executable"].as_str().map(PathBuf::from))
|
||||
.ok_or_else(|| format!("no lightcraft-ui-egui test binary in the build (cargo exited with {})", out.status))
|
||||
});
|
||||
let ran = binary.and_then(|binary| {
|
||||
let mut c = Command::new(&binary);
|
||||
c.args(["--exact", TRANSLATION_GAPS_TEST, "--nocapture", "--quiet"]);
|
||||
run(c, &format!("{} --exact {TRANSLATION_GAPS_TEST} --nocapture --quiet", binary.display()))
|
||||
});
|
||||
if let Err(e) = ran {
|
||||
eprintln!("note: the translation gaps report did not run: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(mut cmd: Command, what: &str) -> Result<(), String> {
|
||||
eprintln!("$ {what}");
|
||||
let status = cmd.status().map_err(|e| format!("{what}: failed to spawn: {e}"))?;
|
||||
@@ -431,6 +465,7 @@ fn cmd_ci(args: &[&str]) -> Result<(), String> {
|
||||
if *name == "test" {
|
||||
// right after the workspace tests, before parity and wasm build other feature sets
|
||||
probe();
|
||||
translation_gaps();
|
||||
}
|
||||
if let Err(e) = r {
|
||||
failure = Some((*name, e));
|
||||
|
||||
Reference in New Issue
Block a user