233 lines
7.9 KiB
Rust
233 lines
7.9 KiB
Rust
pub use rust_i18n::t;
|
|
|
|
/// Available UI locale codes. Separate from dictionary language support.
|
|
pub const SUPPORTED_UI_LOCALES: &[&str] = &[
|
|
"en", "de", "es", "fr", "it", "pt", "nl", "sv", "da", "nb", "fi", "pl", "cs", "ro", "hr",
|
|
"hu", "lt", "lv", "sl", "et", "tr",
|
|
];
|
|
|
|
pub fn set_ui_locale(locale: &str) {
|
|
let effective = if SUPPORTED_UI_LOCALES.contains(&locale) {
|
|
locale
|
|
} else {
|
|
"en"
|
|
};
|
|
rust_i18n::set_locale(effective);
|
|
}
|
|
|
|
/// Retrieve the set of all translation keys for a given locale.
|
|
/// Used by the catalog parity test to verify every key exists in every locale.
|
|
#[cfg(test)]
|
|
fn collect_yaml_keys(value: &serde_yaml::Value, prefix: &str, keys: &mut std::collections::BTreeSet<String>) {
|
|
match value {
|
|
serde_yaml::Value::Mapping(map) => {
|
|
for (k, v) in map {
|
|
let key_str = k.as_str().unwrap_or("");
|
|
let full = if prefix.is_empty() {
|
|
key_str.to_string()
|
|
} else {
|
|
format!("{prefix}.{key_str}")
|
|
};
|
|
collect_yaml_keys(v, &full, keys);
|
|
}
|
|
}
|
|
_ => {
|
|
keys.insert(prefix.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Translate a LanguageLayoutValidationError for display in the UI.
|
|
pub fn localized_language_layout_error(
|
|
err: &crate::l10n::language_pack::LanguageLayoutValidationError,
|
|
) -> String {
|
|
use crate::l10n::language_pack::LanguageLayoutValidationError::*;
|
|
match err {
|
|
UnknownLanguage(key) => t!("errors.unknown_language", key = key).to_string(),
|
|
UnknownLayout(key) => t!("errors.unknown_layout", key = key).to_string(),
|
|
UnsupportedLanguageLayoutPair {
|
|
language_key,
|
|
layout_key,
|
|
} => t!(
|
|
"errors.unsupported_pair",
|
|
language = language_key,
|
|
layout = layout_key
|
|
)
|
|
.to_string(),
|
|
LanguageBlockedBySupportLevel(key) => {
|
|
t!("errors.language_blocked", key = key).to_string()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::BTreeSet;
|
|
|
|
fn locale_keys(locale: &str) -> BTreeSet<String> {
|
|
let path = format!("locales/{locale}.yml");
|
|
let content = std::fs::read_to_string(&path)
|
|
.unwrap_or_else(|e| panic!("Failed to read {path}: {e}"));
|
|
let root: serde_yaml::Value = serde_yaml::from_str(&content)
|
|
.unwrap_or_else(|e| panic!("Failed to parse {path}: {e}"));
|
|
let mut keys = BTreeSet::new();
|
|
collect_yaml_keys(&root, "", &mut keys);
|
|
keys
|
|
}
|
|
|
|
#[test]
|
|
fn catalog_parity_all_locales() {
|
|
let en = locale_keys("en");
|
|
let mut errors = Vec::new();
|
|
|
|
for locale in SUPPORTED_UI_LOCALES {
|
|
if *locale == "en" {
|
|
continue;
|
|
}
|
|
let other = locale_keys(locale);
|
|
let missing: Vec<_> = en.difference(&other).collect();
|
|
let extra: Vec<_> = other.difference(&en).collect();
|
|
|
|
if !missing.is_empty() {
|
|
errors.push(format!(
|
|
"Keys in en.yml missing from {locale}.yml:\n {}",
|
|
missing
|
|
.iter()
|
|
.map(|k| k.as_str())
|
|
.collect::<Vec<_>>()
|
|
.join("\n ")
|
|
));
|
|
}
|
|
if !extra.is_empty() {
|
|
errors.push(format!(
|
|
"Keys in {locale}.yml not present in en.yml:\n {}",
|
|
extra
|
|
.iter()
|
|
.map(|k| k.as_str())
|
|
.collect::<Vec<_>>()
|
|
.join("\n ")
|
|
));
|
|
}
|
|
}
|
|
|
|
assert!(errors.is_empty(), "Catalog parity errors:\n{}", errors.join("\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn placeholder_parity_all_locales() {
|
|
let en_content = std::fs::read_to_string("locales/en.yml").unwrap();
|
|
let en_root: serde_yaml::Value = serde_yaml::from_str(&en_content).unwrap();
|
|
let mut en_map = std::collections::BTreeMap::new();
|
|
collect_leaf_values(&en_root, "", &mut en_map);
|
|
|
|
let placeholder_re = regex::Regex::new(r"%\{(\w+)\}").unwrap();
|
|
let mut all_mismatches = Vec::new();
|
|
|
|
for locale in SUPPORTED_UI_LOCALES {
|
|
if *locale == "en" {
|
|
continue;
|
|
}
|
|
let other_content = std::fs::read_to_string(format!("locales/{locale}.yml")).unwrap();
|
|
let other_root: serde_yaml::Value = serde_yaml::from_str(&other_content).unwrap();
|
|
let mut other_map = std::collections::BTreeMap::new();
|
|
collect_leaf_values(&other_root, "", &mut other_map);
|
|
|
|
for (key, en_val) in &en_map {
|
|
if let Some(other_val) = other_map.get(key) {
|
|
let en_placeholders: BTreeSet<_> = placeholder_re
|
|
.captures_iter(en_val)
|
|
.map(|c| c[1].to_string())
|
|
.collect();
|
|
let other_placeholders: BTreeSet<_> = placeholder_re
|
|
.captures_iter(other_val)
|
|
.map(|c| c[1].to_string())
|
|
.collect();
|
|
if en_placeholders != other_placeholders {
|
|
all_mismatches.push(format!(
|
|
" {locale}/{key}: en={en_placeholders:?} {locale}={other_placeholders:?}"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
all_mismatches.is_empty(),
|
|
"Placeholder mismatches:\n{}",
|
|
all_mismatches.join("\n")
|
|
);
|
|
}
|
|
|
|
fn collect_leaf_values(
|
|
value: &serde_yaml::Value,
|
|
prefix: &str,
|
|
map: &mut std::collections::BTreeMap<String, String>,
|
|
) {
|
|
match value {
|
|
serde_yaml::Value::Mapping(m) => {
|
|
for (k, v) in m {
|
|
let key_str = k.as_str().unwrap_or("");
|
|
let full = if prefix.is_empty() {
|
|
key_str.to_string()
|
|
} else {
|
|
format!("{prefix}.{key_str}")
|
|
};
|
|
collect_leaf_values(v, &full, map);
|
|
}
|
|
}
|
|
serde_yaml::Value::String(s) => {
|
|
map.insert(prefix.to_string(), s.clone());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_locale_english_produces_english() {
|
|
set_ui_locale("en");
|
|
let text = t!("menu.subtitle").to_string();
|
|
assert_eq!(text, "Terminal Typing Tutor");
|
|
}
|
|
|
|
#[test]
|
|
fn set_locale_german_produces_german() {
|
|
// Use the explicit locale parameter to avoid race conditions with
|
|
// parallel tests that share the global locale state.
|
|
let text = t!("menu.subtitle", locale = "de").to_string();
|
|
assert_eq!(text, "Terminal-Tipptrainer");
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_locale_falls_back_to_english() {
|
|
set_ui_locale("zz");
|
|
// After setting unsupported locale, the effective locale is "en"
|
|
let text = t!("menu.subtitle", locale = "en").to_string();
|
|
assert_eq!(text, "Terminal Typing Tutor");
|
|
}
|
|
|
|
#[test]
|
|
fn branch_name_translated_de() {
|
|
let text = t!("skill_tree.branch_primary_letters", locale = "de").to_string();
|
|
assert_eq!(text, "Grundbuchstaben");
|
|
}
|
|
|
|
#[test]
|
|
fn level_name_translated_de() {
|
|
let text = t!("skill_tree.level_frequency_order", locale = "de").to_string();
|
|
assert_eq!(text, "Haeufigkeitsfolge");
|
|
}
|
|
|
|
#[test]
|
|
fn passage_all_translated_de() {
|
|
let text = t!("select.passage_all", locale = "de").to_string();
|
|
assert_eq!(text, "Alle (Eingebaut + alle Buecher)");
|
|
}
|
|
|
|
#[test]
|
|
fn progress_overall_translated_de() {
|
|
let text = t!("progress.overall_key_progress", locale = "de").to_string();
|
|
assert_eq!(text, "Gesamter Tastenfortschritt");
|
|
}
|
|
}
|