use std::sync::OnceLock;
use fluent_bundle::concurrent::FluentBundle;
use fluent_bundle::{FluentArgs, FluentResource, FluentValue};
use unic_langid::LanguageIdentifier;
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"),
)],
),
];
static EN_BUNDLE: OnceLock<FluentBundle<FluentResource>> = OnceLock::new();
static ZH_BUNDLE: OnceLock<FluentBundle<FluentResource>> = OnceLock::new();
pub fn t(key: &str, args: &[(&str, String)]) -> String {
let locale = current_locale();
format_from_bundle(locale.language.as_str(), key, args)
.or_else(|| format_from_bundle("en", key, args))
.unwrap_or_else(|| key.to_string())
}
pub fn tr(key: &str) -> String {
t(key, &[])
}
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![];
Some(
bundle
.format_pattern(pattern, Some(&fluent_args), &mut errors)
.to_string(),
)
}
fn build_bundle(langid: LanguageIdentifier, ftl: &str) -> FluentBundle<FluentResource> {
let resource = FluentResource::try_new(ftl.to_string()).unwrap_or_else(|e| e.0);
let mut bundle = FluentBundle::new_concurrent(vec![langid]);
bundle.set_use_isolating(false);
bundle
.add_resource(resource)
.expect("FTL resources should add without conflict");
bundle
}
fn build_en_bundle() -> FluentBundle<FluentResource> {
build_bundle(
"en".parse().expect("'en' is a valid language identifier"),
EN_FTL,
)
}
fn build_zh_bundle() -> FluentBundle<FluentResource> {
build_bundle(
"zh".parse().expect("'zh' is a valid language identifier"),
ZH_FTL,
)
}
const EN_FTL: &str = EMBEDDED_LOCALES[0].1[0].1;
const ZH_FTL: &str = EMBEDDED_LOCALES[1].1[0].1;
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn ftl_keys(ftl: &str) -> BTreeSet<String> {
ftl.lines()
.filter_map(|line| line.split_once('='))
.filter_map(|(head, _)| {
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(|| key.to_string())
})
.collect()
}
#[test]
fn test_key_parity() {
let en = ftl_keys(EN_FTL);
let zh = ftl_keys(ZH_FTL);
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]
fn test_en_bundle_lookups() {
assert_eq!(
format_from_bundle("en", "impact-notice", &[]),
Some("Change impact alert".to_string())
);
assert_eq!(
format_from_bundle("en", "project-not-found", &[("name", "demo".to_string())]),
Some("Project 'demo' not found".to_string())
);
assert_eq!(
format_from_bundle("en", "node-not-found", &[("name", "main".to_string())]),
Some("Node 'main' not found".to_string())
);
}
#[test]
fn test_zh_bundle_lookups() {
assert_eq!(
format_from_bundle("zh", "impact-notice", &[]),
Some("变更影响告警".to_string())
);
assert_eq!(
format_from_bundle("zh", "project-not-found", &[("name", "demo".to_string())]),
Some("项目 'demo' 未找到".to_string())
);
assert_eq!(
format_from_bundle("zh", "index-incremental-triggered", &[]),
Some("触发增量索引".to_string())
);
}
#[test]
fn test_unknown_lang_falls_back_to_en_bundle() {
assert_eq!(
format_from_bundle("ar", "impact-notice", &[]),
Some("Change impact alert".to_string())
);
}
#[test]
fn test_missing_key_returns_key_without_panic() {
assert_eq!(t("nonexistent-key", &[]), "nonexistent-key");
assert_eq!(tr("another-missing-key"), "another-missing-key");
assert_eq!(format_from_bundle("en", "nonexistent-key", &[]), None);
assert_eq!(format_from_bundle("zh", "nonexistent-key", &[]), None);
}
}