use std::sync::{LazyLock, RwLock};
use fluent_templates::static_loader;
pub use unic_langid::LanguageIdentifier;
use unic_langid::langid;
pub const DEFAULT_LOCALE: &str = "en";
static_loader! {
pub static LOCALES = {
locales: "./locales",
fallback_language: "en",
};
}
static LOCALE: LazyLock<RwLock<LanguageIdentifier>> = LazyLock::new(|| RwLock::new(langid!("en")));
pub fn set_locale(locale: &str) {
let lang_id: LanguageIdentifier = locale.parse().unwrap_or_else(|_| langid!("en"));
if let Ok(mut guard) = LOCALE.write() {
*guard = lang_id;
} else {
log::warn!("locale lock poisoned; locale not updated");
}
}
pub fn current_locale_id() -> LanguageIdentifier {
LOCALE
.read()
.map(|guard| guard.clone())
.unwrap_or_else(|_| langid!("en"))
}
#[macro_export]
macro_rules! t {
($id:expr) => {{
use ::fluent_templates::Loader as _;
let lang = $crate::locale::current_locale_id();
$crate::locale::LOCALES.lookup(&lang, $id)
}};
($id:expr, $($key:literal => $val:expr),+) => {{
use ::fluent_templates::Loader as _;
let lang = $crate::locale::current_locale_id();
let mut args: ::std::collections::HashMap<
::std::borrow::Cow<'static, str>,
::fluent_templates::fluent_bundle::FluentValue<'_>,
> = ::std::collections::HashMap::new();
$(
args.insert(
::std::borrow::Cow::Borrowed($key),
::fluent_templates::fluent_bundle::FluentValue::from($val.to_string()),
);
)+
$crate::locale::LOCALES.lookup_with_args(&lang, $id, &args)
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_locale_id_defaults_to_en() {
let lang = current_locale_id();
let _ = lang.to_string();
}
#[test]
fn t_macro_returns_english_for_button_yes() {
set_locale("en");
let label = t!("button-yes");
assert_eq!(label, "Yes");
}
#[test]
fn t_macro_returns_english_for_button_no() {
set_locale("en");
let label = t!("button-no");
assert_eq!(label, "No");
}
#[test]
fn t_macro_returns_english_for_button_screen_help() {
set_locale("en");
let help = t!("button-screen-help");
assert!(help.contains("←/→"), "expected arrow in help: {help}");
assert!(help.contains("Enter"), "expected Enter in help: {help}");
assert!(help.contains("Esc"), "expected Esc in help: {help}");
}
#[test]
fn t_macro_with_variable_substitutes_value() {
set_locale("en");
let question = t!("manifest-path-question", "manifest" => "Cargo.toml");
assert!(
question.contains("Cargo.toml"),
"expected manifest name in question: {question}"
);
}
#[test]
fn set_locale_invalid_falls_back_to_default() {
set_locale("not-a-valid!!!");
let lang = current_locale_id();
assert_eq!(lang.to_string(), DEFAULT_LOCALE);
set_locale("en");
}
#[test]
fn set_locale_and_lookup_returns_translation() {
set_locale("en");
let label = t!("button-major");
assert_eq!(label, "Major");
}
}