use std::cell::RefCell;
use chrono::NaiveDate;
const DEFAULT_LANGUAGE: &str = "en-us";
const SUPPORTED_LANGUAGES: &[&str] = &[
"en-us", "en-gb", "fr", "de", "es", "pt-br", "zh-hans", "ja", "ar",
];
thread_local! {
static ACTIVE_LANGUAGE: RefCell<String> = RefCell::new(DEFAULT_LANGUAGE.to_owned());
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LanguageInfo {
pub code: String,
pub name: String,
pub name_local: String,
pub bidi: bool,
}
#[must_use]
pub fn get_language() -> String {
ACTIVE_LANGUAGE.with(|language| language.borrow().clone())
}
pub fn activate(language: &str) {
ACTIVE_LANGUAGE.with(|active| {
*active.borrow_mut() = language.to_owned();
});
}
pub fn deactivate() {
ACTIVE_LANGUAGE.with(|active| {
*active.borrow_mut() = DEFAULT_LANGUAGE.to_owned();
});
}
#[must_use]
pub fn get_supported_languages() -> Vec<String> {
SUPPORTED_LANGUAGES
.iter()
.map(|language| (*language).to_owned())
.collect()
}
#[must_use]
pub fn number_format(value: f64, decimal_places: usize, use_l10n: bool) -> String {
let plain = format!("{:.*}", decimal_places, value);
if !use_l10n {
return plain;
}
let language = get_language();
let (thousands_separator, decimal_separator) = separators_for_language(&language);
let unsigned = plain.strip_prefix('-').unwrap_or(&plain);
let sign = if plain.starts_with('-') { "-" } else { "" };
let (integer_part, fractional_part) = unsigned.split_once('.').unwrap_or((unsigned, ""));
let mut formatted = format!(
"{sign}{}",
group_integer_part(integer_part, thousands_separator)
);
if decimal_places > 0 {
formatted.push(decimal_separator);
formatted.push_str(fractional_part);
}
formatted
}
#[must_use]
pub fn date_format(date: &str, format_str: &str) -> String {
NaiveDate::parse_from_str(date, "%Y-%m-%d")
.map(|parsed| parsed.format(format_str).to_string())
.unwrap_or_else(|_| date.to_owned())
}
#[must_use]
pub fn get_language_info(code: &str) -> LanguageInfo {
match code.to_ascii_lowercase().as_str() {
"en-us" => LanguageInfo {
code: "en-us".to_owned(),
name: "English (US)".to_owned(),
name_local: "English (US)".to_owned(),
bidi: false,
},
"en-gb" => LanguageInfo {
code: "en-gb".to_owned(),
name: "English (UK)".to_owned(),
name_local: "English (UK)".to_owned(),
bidi: false,
},
"fr" => LanguageInfo {
code: "fr".to_owned(),
name: "French".to_owned(),
name_local: "Français".to_owned(),
bidi: false,
},
"de" => LanguageInfo {
code: "de".to_owned(),
name: "German".to_owned(),
name_local: "Deutsch".to_owned(),
bidi: false,
},
"es" => LanguageInfo {
code: "es".to_owned(),
name: "Spanish".to_owned(),
name_local: "Español".to_owned(),
bidi: false,
},
"pt-br" => LanguageInfo {
code: "pt-br".to_owned(),
name: "Portuguese (Brazil)".to_owned(),
name_local: "Português (Brasil)".to_owned(),
bidi: false,
},
"zh-hans" => LanguageInfo {
code: "zh-hans".to_owned(),
name: "Chinese (Simplified)".to_owned(),
name_local: "简体中文".to_owned(),
bidi: false,
},
"ja" => LanguageInfo {
code: "ja".to_owned(),
name: "Japanese".to_owned(),
name_local: "日本語".to_owned(),
bidi: false,
},
"ar" => LanguageInfo {
code: "ar".to_owned(),
name: "Arabic".to_owned(),
name_local: "العربية".to_owned(),
bidi: true,
},
_ => LanguageInfo {
code: code.to_owned(),
name: code.to_owned(),
name_local: code.to_owned(),
bidi: false,
},
}
}
fn separators_for_language(language: &str) -> (char, char) {
match language.to_ascii_lowercase().as_str() {
"fr" => (' ', ','),
"de" | "es" | "pt-br" => ('.', ','),
"ar" => ('٬', '٫'),
_ => (',', '.'),
}
}
fn group_integer_part(integer_part: &str, separator: char) -> String {
let mut reversed = String::with_capacity(integer_part.len() + integer_part.len() / 3);
for (index, ch) in integer_part.chars().rev().enumerate() {
if index > 0 && index % 3 == 0 {
reversed.push(separator);
}
reversed.push(ch);
}
reversed.chars().rev().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_language_is_returned_when_inactive() {
deactivate();
assert_eq!(get_language(), DEFAULT_LANGUAGE);
}
#[test]
fn activate_updates_current_thread_language() {
deactivate();
activate("fr");
assert_eq!(get_language(), "fr");
deactivate();
assert_eq!(get_language(), DEFAULT_LANGUAGE);
}
#[test]
fn deactivate_restores_default_language() {
activate("de");
deactivate();
assert_eq!(get_language(), DEFAULT_LANGUAGE);
}
#[test]
fn language_state_is_thread_local() {
deactivate();
activate("es");
let other = std::thread::spawn(get_language).join().unwrap();
assert_eq!(get_language(), "es");
assert_eq!(other, DEFAULT_LANGUAGE);
deactivate();
}
#[test]
fn supported_languages_are_hardcoded_common_values() {
let supported = get_supported_languages();
assert!(supported.contains(&DEFAULT_LANGUAGE.to_owned()));
assert!(supported.contains(&"fr".to_owned()));
assert!(supported.contains(&"ja".to_owned()));
}
#[test]
fn test_number_format_with_l10n() {
deactivate();
activate("de");
assert_eq!(number_format(12345.678, 2, true), "12.345,68");
deactivate();
}
#[test]
fn test_number_format_without_l10n() {
activate("de");
assert_eq!(number_format(12345.678, 2, false), "12345.68");
deactivate();
}
#[test]
fn test_language_info_english() {
let info = get_language_info("en-us");
assert_eq!(info.code, "en-us");
assert_eq!(info.name, "English (US)");
assert_eq!(info.name_local, "English (US)");
assert!(!info.bidi);
}
#[test]
fn date_format_reformats_iso_dates() {
assert_eq!(date_format("2024-03-19", "%d/%m/%Y"), "19/03/2024");
}
#[test]
fn unknown_language_info_falls_back_to_code() {
let info = get_language_info("eo");
assert_eq!(info.code, "eo");
assert_eq!(info.name, "eo");
assert_eq!(info.name_local, "eo");
assert!(!info.bidi);
}
}