use std::sync::OnceLock;
use fluent_bundle::concurrent::FluentBundle;
use fluent_bundle::{FluentArgs, FluentResource, FluentValue};
use unic_langid::LanguageIdentifier;
pub(crate) const EN_FTL: &str = include_str!("../../locales/en/messages.ftl");
pub(crate) const ZH_FTL: &str = include_str!("../../locales/zh/messages.ftl");
pub fn translate(key: &str, args: &[(&str, String)]) -> String {
let lang = super::locale::current_locale()
.language
.as_str()
.to_string();
translate_for(&lang, key, args)
}
pub fn translate_en(key: &str, args: &[(&str, String)]) -> String {
format_from_bundle("en", key, args).unwrap_or_else(|| key.to_string())
}
pub fn translate_for(lang: &str, key: &str, args: &[(&str, String)]) -> String {
format_from_bundle(lang, key, args)
.or_else(|| format_from_bundle("en", key, args))
.unwrap_or_else(|| key.to_string())
}
pub fn t(key: &str, args: &[(&str, String)]) -> String {
translate(key, args)
}
pub fn t_simple(key: &str) -> String {
translate(key, &[])
}
static EN_BUNDLE: OnceLock<FluentBundle<FluentResource>> = OnceLock::new();
static ZH_BUNDLE: OnceLock<FluentBundle<FluentResource>> = OnceLock::new();
fn format_from_bundle(lang: &str, key: &str, args: &[(&str, String)]) -> Option<String> {
let bundle = match lang {
"zh" => ZH_BUNDLE.get_or_init(build_zh_bundle),
_ => EN_BUNDLE.get_or_init(build_en_bundle),
};
let msg = bundle.get_message(key)?;
let pattern = msg.value()?;
let mut fluent_args = FluentArgs::new();
for (name, value) in args {
fluent_args.set(*name, FluentValue::from(value.clone()));
}
let mut errors = vec![];
let result = bundle.format_pattern(pattern, Some(&fluent_args), &mut errors);
Some(result.to_string())
}
fn build_en_bundle() -> FluentBundle<FluentResource> {
let resource = FluentResource::try_new(EN_FTL.to_string()).unwrap_or_else(|e| e.0);
let langid: LanguageIdentifier = "en".parse().expect("'en' is a valid language identifier");
let mut bundle = FluentBundle::new_concurrent(vec![langid]);
bundle.set_use_isolating(false);
bundle
.add_resource(resource)
.expect("EN resources should add without conflict");
bundle
}
fn build_zh_bundle() -> FluentBundle<FluentResource> {
let resource = FluentResource::try_new(ZH_FTL.to_string()).unwrap_or_else(|e| e.0);
let langid: LanguageIdentifier = "zh".parse().expect("'zh' is a valid language identifier");
let mut bundle = FluentBundle::new_concurrent(vec![langid]);
bundle.set_use_isolating(false);
bundle
.add_resource(resource)
.expect("ZH resources should add without conflict");
bundle
}
#[cfg(test)]
mod tests {
use super::*;
fn ftl_keys(ftl: &str) -> std::collections::BTreeSet<String> {
ftl.lines()
.filter_map(|line| line.split_once(" = "))
.filter(|(key, _)| !key.contains(' '))
.map(|(key, _)| key.trim().to_string())
.collect()
}
#[test]
fn test_key_parity_en_zh() {
let en = ftl_keys(EN_FTL);
let zh = ftl_keys(ZH_FTL);
assert!(!en.is_empty(), "EN catalog must not be empty");
let missing_in_zh: Vec<_> = en.difference(&zh).collect();
let missing_in_en: Vec<_> = zh.difference(&en).collect();
assert!(
missing_in_zh.is_empty() && missing_in_en.is_empty(),
"en/zh key sets diverge: missing_in_zh={missing_in_zh:?} missing_in_en={missing_in_en:?}"
);
}
#[test]
fn test_locales_dir_matches_embedded() {
let manifest = env!("CARGO_MANIFEST_DIR");
let en_disk = std::fs::read_to_string(format!("{manifest}/locales/en/messages.ftl"))
.expect("locales/en/messages.ftl must exist on disk");
let zh_disk = std::fs::read_to_string(format!("{manifest}/locales/zh/messages.ftl"))
.expect("locales/zh/messages.ftl must exist on disk");
assert_eq!(en_disk, EN_FTL, "disk en catalog drifted from embedded");
assert_eq!(zh_disk, ZH_FTL, "disk zh catalog drifted from embedded");
}
#[test]
fn test_fluent_en_simple() {
assert_eq!(
format_from_bundle("en", "rate-limit-exceeded", &[]),
Some("Rate limit exceeded".to_string())
);
assert_eq!(
format_from_bundle("en", "error-config", &[("message", "x".to_string())]),
Some("Configuration error: x".to_string())
);
}
#[test]
fn test_fluent_zh_simple() {
assert_eq!(
format_from_bundle("zh", "rate-limit-exceeded", &[]),
Some("超出速率限制".to_string())
);
assert_eq!(
format_from_bundle(
"zh",
"error-storage-not-found",
&[("message", "k".to_string())]
),
Some("未找到: k".to_string())
);
}
#[test]
fn test_unknown_lang_falls_back_to_en_bundle() {
assert_eq!(
format_from_bundle("ar", "rate-limit-exceeded", &[]),
Some("Rate limit exceeded".to_string())
);
assert_eq!(
format_from_bundle("ja", "error-config", &[("message", "x".to_string())]),
Some("Configuration error: x".to_string())
);
}
#[test]
fn test_missing_key_returns_none() {
assert_eq!(format_from_bundle("en", "nonexistent-key", &[]), None);
assert_eq!(translate("nonexistent-key", &[]), "nonexistent-key");
assert_eq!(translate_en("nonexistent-key", &[]), "nonexistent-key");
assert_eq!(
translate_for("zh", "nonexistent-key", &[]),
"nonexistent-key"
);
}
#[test]
fn test_translate_for_explicit_lang() {
assert_eq!(
translate_for("zh", "error-limit", &[("message", "x".to_string())]),
"限流错误: x"
);
assert_eq!(
translate_for("fr", "error-limit", &[("message", "x".to_string())]),
"Rate limit error: x",
"unsupported language must terminate in en"
);
}
}