From c42fc7ac24eaced821b474571b92fa699d260046 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 07:49:50 -0400 Subject: [PATCH] =?UTF-8?q?LRC-LIB-SMARTCOLL:=20Japanese=20yes/no=20rule?= =?UTF-8?q?=20values=20read=20=E3=81=82=E3=82=8A=20/=20=E3=81=AA=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yes/no rule values used the shared "Yes" / "No" messages, which Japanese translates as the answers はい / いいえ. As the value of a condition ("編集あり … いいえ") that reads oddly; あり / なし ("has" / "has none") is natural. Messages are keyed by their English text, so the same English word can't carry two Japanese translations. A small context lookup fixes that: tr_ctx_in tries "|" first and falls back to the plain message. Only ja.json adds "smart album value|Yes" / "|No"; the shared Yes / No (はい / いいえ) stay as they are elsewhere, and every other language keeps its plain Ja / Sí / Да. A test rejects contextual keys that name no real value, so a typo can't silently fall back. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/locales/ja.json | 4 +++- crates/ui-egui/src/i18n.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index d38fe90f0..773378612 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1741,5 +1741,7 @@ "Portrait (tall)": "縦長", "Square": "正方形", "In Color": "カラー", - "No Label": "ラベルなし" + "No Label": "ラベルなし", + "smart album value|Yes": "あり", + "smart album value|No": "なし" } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 0f08e244b..31ad3da5f 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -164,6 +164,16 @@ pub fn tr(source: &str) -> &str { tr_in(language(), source) } +/// [`tr`] for a message whose translation depends on where it appears: a catalog entry keyed +/// `"|"` wins, else the plain message. A language adds the contextual entry only +/// where its shared translation reads wrongly there (Japanese はい / いいえ as a rule's value). +fn tr_ctx_in<'a>(language: Locale, context: &str, source: &'a str) -> &'a str { + match language.catalog().get(&format!("{context}|{source}")) { + Some(translation) => translation.as_str(), + None => tr_in(language, source), + } +} + /// Catalog values are `'static` (they live in the embedded file), so a translation can be handed /// out for as long as the `'static` key it was looked up by. fn tr_in(language: Locale, source: &str) -> &str { @@ -333,8 +343,11 @@ pub fn bool_text(b: bool) -> &'static str { bool_text_in(language(), b) } +/// The context of a smart-album yes/no value ([`tr_ctx_in`]). +const BOOL_CONTEXT: &str = "smart album value"; + fn bool_text_in(language: Locale, b: bool) -> &'static str { - tr_in(language, lightcraft_catalog::rules::bool_label(b)) + tr_ctx_in(language, BOOL_CONTEXT, lightcraft_catalog::rules::bool_label(b)) } /// A rule summary for display, keeping free-text rule values verbatim. @@ -661,6 +674,21 @@ mod tests { // yes/no fields read Yes / No, never true / false assert_eq!((bool_text_in(Locale::En, true), bool_text_in(Locale::En, false)), ("Yes", "No")); assert_eq!(bool_text_in(Locale::De, false), "Nein"); + // Japanese answers はい / いいえ don't read as a rule's value: its own wording, あり / なし, + // through a context, while the shared Yes / No keep their meaning everywhere else + assert_eq!((bool_text_in(Locale::Ja, true), bool_text_in(Locale::Ja, false)), ("あり", "なし")); + assert_eq!((Locale::Ja.tr("Yes"), Locale::Ja.tr("No")), ("はい", "いいえ")); + assert_eq!(tr_ctx_in(Locale::Ja, "nope", "Yes"), "はい", "no contextual entry: the plain message"); + assert_eq!(tr_ctx_in(Locale::En, BOOL_CONTEXT, "Yes"), "Yes"); + // a contextual entry names a real value (a typo would silently fall back to はい / いいえ) + let values = [lightcraft_catalog::rules::bool_label(true), lightcraft_catalog::rules::bool_label(false)]; + for language in Locale::ALL { + for key in language.catalog().keys() { + if let Some(value) = key.strip_prefix(BOOL_CONTEXT).and_then(|k| k.strip_prefix('|')) { + assert!(values.contains(&value), "{}: {key:?} isn't a yes/no value", language.code()); + } + } + } for language in Locale::ALL.iter().filter(|language| **language != Locale::En) { for label in [lightcraft_catalog::rules::bool_label(true), lightcraft_catalog::rules::bool_label(false)] { assert!(language.catalog().contains_key(label), "{} lacks {label:?}", language.code());