use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::sync::OnceLock;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Locale {
PtBr,
#[default]
En,
Es,
Fr,
De,
It,
Ru,
Zh,
Ja,
Ko,
Hi,
Ar,
Id,
Tr,
Nl,
Pl,
}
const SOURCES: [(Locale, &str); 16] = [
(Locale::En, include_str!("locales/en.json")),
(Locale::PtBr, include_str!("locales/pt-BR.json")),
(Locale::Es, include_str!("locales/es.json")),
(Locale::Fr, include_str!("locales/fr.json")),
(Locale::De, include_str!("locales/de.json")),
(Locale::It, include_str!("locales/it.json")),
(Locale::Ru, include_str!("locales/ru.json")),
(Locale::Zh, include_str!("locales/zh.json")),
(Locale::Ja, include_str!("locales/ja.json")),
(Locale::Ko, include_str!("locales/ko.json")),
(Locale::Hi, include_str!("locales/hi.json")),
(Locale::Ar, include_str!("locales/ar.json")),
(Locale::Id, include_str!("locales/id.json")),
(Locale::Tr, include_str!("locales/tr.json")),
(Locale::Nl, include_str!("locales/nl.json")),
(Locale::Pl, include_str!("locales/pl.json")),
];
#[derive(Debug, Deserialize)]
struct Data {
code: String,
name: String,
decimal: char,
thousands: Option<char>,
#[serde(default)]
open_interval: bool,
#[serde(default)]
written: HashMap<String, String>,
speech: HashMap<String, String>,
#[serde(default)]
fractions: Option<Fractions>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Fractions {
pub numerals: Vec<String>,
pub singular: Vec<String>,
pub plural: Vec<String>,
}
fn table() -> &'static HashMap<Locale, Data> {
static TABLE: OnceLock<HashMap<Locale, Data>> = OnceLock::new();
TABLE.get_or_init(|| {
SOURCES
.iter()
.map(|(locale, json)| {
let data: Data = serde_json::from_str(json).unwrap_or_else(|e| panic!("src/locales/{}.json: {e}", locale.code()));
assert_eq!(data.code, locale.code(), "the file of {locale:?} declares another code");
(*locale, data)
})
.collect()
})
}
impl Locale {
pub const ALL: [Locale; 16] = [
Locale::En,
Locale::PtBr,
Locale::Es,
Locale::Fr,
Locale::De,
Locale::It,
Locale::Ru,
Locale::Zh,
Locale::Ja,
Locale::Ko,
Locale::Hi,
Locale::Ar,
Locale::Id,
Locale::Tr,
Locale::Nl,
Locale::Pl,
];
pub fn code(self) -> &'static str {
match self {
Locale::PtBr => "pt-BR",
Locale::En => "en",
Locale::Es => "es",
Locale::Fr => "fr",
Locale::De => "de",
Locale::It => "it",
Locale::Ru => "ru",
Locale::Zh => "zh",
Locale::Ja => "ja",
Locale::Ko => "ko",
Locale::Hi => "hi",
Locale::Ar => "ar",
Locale::Id => "id",
Locale::Tr => "tr",
Locale::Nl => "nl",
Locale::Pl => "pl",
}
}
fn data(self) -> &'static Data {
&table()[&self]
}
pub fn name(self) -> &'static str {
&self.data().name
}
pub fn decimal_separator(self) -> char {
self.data().decimal
}
pub fn thousands_separator(self) -> Option<char> {
self.data().thousands
}
pub fn comma_is_decimal(self) -> bool {
self.data().decimal == ','
}
pub fn open_interval(self) -> bool {
self.data().open_interval
}
pub fn function_name(self, canonical: &str) -> String {
self.data().written.get(canonical).cloned().unwrap_or_else(|| canonical.into())
}
pub fn speak_function(self, canonical: &str) -> String {
match self.function_spoken(canonical) {
Some(name) => self.fill("function_of", &[name]),
None => self.function_name(canonical),
}
}
pub(crate) fn function_spoken(self, canonical: &str) -> Option<&'static str> {
self.lookup(&format!("fn.{canonical}"))
}
pub(crate) fn fractions(self) -> Option<&'static Fractions> {
self.data().fractions.as_ref()
}
fn lookup(self, key: &str) -> Option<&'static str> {
self.data().speech.get(key).or_else(|| Locale::En.data().speech.get(key)).map(String::as_str)
}
pub(crate) fn phrase(self, key: &str) -> &'static str {
self.lookup(key).unwrap_or_else(|| panic!("speech phrase '{key}' missing from src/locales/en.json"))
}
pub(crate) fn fill(self, key: &str, args: &[&str]) -> String {
let mut s = self.phrase(key).to_string();
for (i, a) in args.iter().enumerate() {
s = s.replace(&format!("{{{i}}}"), a);
}
s
}
pub(crate) fn optional(self, key: &str) -> Option<&'static str> {
self.lookup(key)
}
pub fn missing_phrases(self) -> Vec<&'static str> {
let own = &self.data().speech;
let mut keys: Vec<&'static str> = Locale::En.data().speech.keys().map(String::as_str).filter(|k| !own.contains_key(*k)).collect();
keys.sort_unstable();
keys
}
}
impl FromStr for Locale {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower = s.to_ascii_lowercase().replace('_', "-");
let language = lower.split('-').next().unwrap_or_default();
Locale::ALL.into_iter().find(|l| l.code().to_ascii_lowercase() == lower || l.code().split('-').next() == Some(language)).ok_or_else(|| {
let codes: Vec<&str> = Locale::ALL.iter().map(|l| l.code()).collect();
format!("unknown locale: {s} (use one of {})", codes.join(", "))
})
}
}
impl fmt::Display for Locale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.code())
}
}