use super::locale::current_locale;
const EMBEDDED_LOCALES: &[(&str, &[(&str, &str)])] = &[
(
"en",
&[(
"messages.ftl",
include_str!("../../locales/en/messages.ftl"),
)],
),
(
"zh",
&[(
"messages.ftl",
include_str!("../../locales/zh/messages.ftl"),
)],
),
];
fn ftl_entries(ftl: &str) -> impl Iterator<Item = (&str, &str)> {
ftl.lines().filter_map(|line| {
let (head, tail) = line.split_once('=')?;
let key = head.trim();
let is_key = !key.is_empty()
&& !key.starts_with('#')
&& key
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
is_key.then_some((key, tail.trim()))
})
}
pub fn init() {
super::locale::init();
for (locale, files) in EMBEDDED_LOCALES {
for (_, ftl) in *files {
for (key, value) in ftl_entries(ftl) {
sdforge::i18n::register_translation(locale, key, value);
}
}
}
sdforge::i18n::set_locale(¤t_locale().to_string());
}
pub fn t(key: &str, args: &[(&str, String)]) -> String {
sdforge::i18n::t(key, args)
}
pub fn tr(key: &str) -> String {
t(key, &[])
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn ftl_keys(ftl: &str) -> BTreeSet<String> {
ftl_entries(ftl).map(|(key, _)| key.to_string()).collect()
}
fn embedded_ftl(locale: &str) -> &'static str {
EMBEDDED_LOCALES
.iter()
.find(|(l, _)| *l == locale)
.unwrap_or_else(|| panic!("EMBEDDED_LOCALES missing locale '{locale}'"))
.1[0]
.1
}
#[test]
fn test_key_parity() {
let en = ftl_keys(embedded_ftl("en"));
let zh = ftl_keys(embedded_ftl("zh"));
assert!(!en.is_empty(), "EN catalog must not be empty");
assert_eq!(en, zh, "EN/ZH key sets must be identical");
}
#[test]
fn embedded_locales_cover_locales_dir() {
let locales_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
for entry in std::fs::read_dir(&locales_dir).expect("locales dir exists") {
let path = entry.expect("locale entry").path();
if !path.is_dir() {
continue;
}
let locale = path
.file_name()
.and_then(|n| n.to_str())
.expect("locale dir name");
let files = EMBEDDED_LOCALES
.iter()
.find(|(l, _)| *l == locale)
.unwrap_or_else(|| panic!("EMBEDDED_LOCALES missing locale dir '{locale}'"))
.1;
for ftl_entry in std::fs::read_dir(&path).expect("locale dir readable") {
let ftl_path = ftl_entry.expect("ftl entry").path();
if ftl_path.extension().and_then(|e| e.to_str()) != Some("ftl") {
continue;
}
let stem = ftl_path
.file_stem()
.and_then(|s| s.to_str())
.expect("ftl stem");
assert!(
files
.iter()
.any(|(name, _)| name.strip_suffix(".ftl") == Some(stem)),
"locale '{locale}': '{stem}.ftl' exists on disk but is not registered \
in EMBEDDED_LOCALES"
);
}
}
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn test_en_bundle_lookups() {
init();
assert_eq!(
sdforge::i18n::translate_for("en", "impact-notice", &[]),
"Change impact alert".to_string()
);
assert_eq!(
sdforge::i18n::translate_for(
"en",
"project-not-found",
&[("name", "demo".to_string())]
),
"Project 'demo' not found".to_string()
);
assert_eq!(
sdforge::i18n::translate_for("en", "node-not-found", &[("name", "main".to_string())]),
"Node 'main' not found".to_string()
);
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn test_zh_bundle_lookups() {
init();
assert_eq!(
sdforge::i18n::translate_for("zh", "impact-notice", &[]),
"变更影响告警".to_string()
);
assert_eq!(
sdforge::i18n::translate_for(
"zh",
"project-not-found",
&[("name", "demo".to_string())]
),
"项目 'demo' 未找到".to_string()
);
assert_eq!(
sdforge::i18n::translate_for("zh", "index-incremental-triggered", &[]),
"触发增量索引".to_string()
);
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn test_missing_key_returns_key_without_panic() {
assert_eq!(t("nonexistent-key", &[]), "nonexistent-key");
assert_eq!(tr("another-missing-key"), "another-missing-key");
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn init_registers_all_embedded_entries_in_sdforge() {
init();
for (locale, files) in EMBEDDED_LOCALES {
for (_, ftl) in *files {
for (key, value) in ftl_entries(ftl) {
assert_eq!(
sdforge::i18n::translate_for(locale, key, &[]),
value,
"sdforge registry must serve embedded ({locale}, {key}) verbatim"
);
}
}
}
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn init_wires_detected_locale_and_translates_via_sdforge() {
crate::i18n::set_locale("zh").expect("zh supported");
init();
assert_eq!(
sdforge::i18n::get_locale(),
"zh",
"init() must call sdforge set_locale with the detected locale"
);
assert_eq!(
t("project-not-found", &[("name", "demo".to_string())]),
"项目 'demo' 未找到",
"zh must translate through the sdforge registry"
);
assert_eq!(tr("impact-notice"), "变更影响告警");
crate::i18n::set_locale("en").expect("en supported");
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn explicit_unsupported_locale_falls_back_to_en() {
init();
assert_eq!(
sdforge::i18n::translate_for(
"ar",
"project-not-found",
&[("name", "demo".to_string())]
),
"Project 'demo' not found"
);
assert_eq!(
sdforge::i18n::translate_for("en", "impact-notice", &[]),
"Change impact alert"
);
}
#[test]
#[serial_test::serial(i18n_catalog)]
fn missing_key_returns_bare_key_before_and_after_init() {
assert_eq!(t("nonexistent-key", &[]), "nonexistent-key");
assert_eq!(tr("another-missing-key"), "another-missing-key");
init();
assert_eq!(t("nonexistent-key", &[]), "nonexistent-key");
assert_eq!(tr("another-missing-key"), "another-missing-key");
}
}