use icu::collator::CollatorBorrowed;
use icu::decimal::DecimalFormatter;
use icu::locale::Locale;
use icu::plurals::PluralRules;
use once_cell::sync::Lazy;
use std::fmt;
use std::sync::RwLock;
mod i18n_impl;
pub mod messages;
static DEFAULT_LOCALE: Lazy<RwLock<String>> = Lazy::new(|| RwLock::new(detect_system_locale()));
pub fn detect_system_locale() -> String {
let raw = std::env::var("OXCACHE_LANG")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| std::env::var("LC_ALL").ok().filter(|v| !v.is_empty()))
.or_else(|| std::env::var("LC_MESSAGES").ok().filter(|v| !v.is_empty()))
.or_else(|| std::env::var("LANG").ok().filter(|v| !v.is_empty()));
let Some(raw) = raw else {
return String::from("en");
};
let locale = parse_locale_tag(&raw);
if locale == "C" || locale == "POSIX" {
return String::from("en");
}
let lang = locale.split('-').next().unwrap_or(&locale);
if messages::is_supported(lang) {
locale
} else {
String::from("en")
}
}
fn parse_locale_tag(raw: &str) -> String {
let s = raw.split('.').next().unwrap_or(raw);
let s = s.split('@').next().unwrap_or(s);
s.replace('_', "-")
}
pub fn set_default_locale(locale: &str) {
if let Ok(mut guard) = DEFAULT_LOCALE.write() {
*guard = locale.to_string();
}
}
pub fn get_default_locale() -> String {
DEFAULT_LOCALE
.read()
.map(|g| g.clone())
.unwrap_or_else(|_| String::from("en"))
}
#[derive(Debug)]
pub enum I18nError {
InvalidLocale { input: String, reason: String },
InvalidNumber { input: String, reason: String },
DateError(String),
FormatError(String),
}
impl fmt::Display for I18nError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let locale = get_default_locale();
f.write_str(&self.localized_message(&locale))
}
}
impl std::error::Error for I18nError {}
impl I18nError {
pub fn message_id(&self) -> &'static str {
match self {
I18nError::InvalidLocale { .. } => messages::MSG_I18N_INVALID_LOCALE,
I18nError::InvalidNumber { .. } => messages::MSG_I18N_INVALID_NUMBER,
I18nError::DateError(_) => messages::MSG_I18N_DATE_ERROR,
I18nError::FormatError(_) => messages::MSG_I18N_FORMAT_ERROR,
}
}
pub fn localized_message(&self, locale: &str) -> String {
let params: Vec<(&str, String)> = match self {
I18nError::InvalidLocale { input, reason } => {
vec![("input", input.clone()), ("reason", reason.clone())]
}
I18nError::InvalidNumber { input, reason } => {
vec![("input", input.clone()), ("reason", reason.clone())]
}
I18nError::DateError(d) => vec![("detail", d.clone())],
I18nError::FormatError(d) => vec![("detail", d.clone())],
};
let borrowed: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
messages::lookup(locale, self.message_id(), &borrowed)
.unwrap_or_else(|| self.message_id().to_string())
}
}
pub struct CacheI18nFormatter {
locale: Locale,
locale_tag: String,
decimal_formatter: DecimalFormatter,
plural_rules: PluralRules,
collator: CollatorBorrowed<'static>,
}
impl CacheI18nFormatter {
pub fn locale(&self) -> &Locale {
&self.locale
}
pub fn locale_tag(&self) -> &str {
&self.locale_tag
}
pub fn format_message(
&self,
message_id: &str,
params: &[(&str, &str)],
) -> Result<String, I18nError> {
Ok(messages::lookup(&self.locale_tag, message_id, params)
.unwrap_or_else(|| message_id.to_string()))
}
}
#[cfg(test)]
#[cfg(test)]
mod tests;