use std::collections::BTreeMap;
mod format;
mod plural;
pub mod policy;
#[cfg(test)]
mod tests;
pub use format::{MessageValue, MessageValues, format_message};
pub use plural::PluralCategory;
pub const LOCALES: [Locale; 5] = [Locale::En, Locale::Ru, Locale::Vi, Locale::Fr, Locale::De];
pub const DEFAULT_LOCALE: Locale = Locale::En;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Locale {
#[default]
En,
Ru,
Vi,
Fr,
De,
}
impl Locale {
pub fn code(self) -> &'static str {
match self {
Self::En => "en",
Self::Ru => "ru",
Self::Vi => "vi",
Self::Fr => "fr",
Self::De => "de",
}
}
pub fn label(self) -> &'static str {
match self {
Self::En => "English",
Self::Ru => "Русский",
Self::Vi => "Tiếng Việt",
Self::Fr => "Français",
Self::De => "Deutsch",
}
}
pub fn parse(value: &str) -> Option<Self> {
LOCALES.into_iter().find(|l| l.code() == value)
}
}
impl std::fmt::Display for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
impl std::str::FromStr for Locale {
type Err = UnknownLocale;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s).ok_or_else(|| UnknownLocale(s.to_owned()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownLocale(pub String);
impl std::fmt::Display for UnknownLocale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unknown locale {:?} — EV publishes en, ru, vi, fr, de", self.0)
}
}
impl std::error::Error for UnknownLocale {}
pub fn locale_path(locale: Locale, path: &str) -> String {
let clean = if path.starts_with('/') { path.to_owned() } else { format!("/{path}") };
if locale == DEFAULT_LOCALE {
return clean;
}
if clean == "/" { format!("/{locale}") } else { format!("/{locale}{clean}") }
}
pub fn split_locale_path(pathname: &str) -> (Locale, String) {
let clean = if pathname.starts_with('/') { pathname.to_owned() } else { format!("/{pathname}") };
let after = &clean[1..];
let (head, rest) = match after.find('/') {
Some(idx) => (&after[..idx], &after[idx..]),
None => (after, ""),
};
match Locale::parse(head) {
Some(locale) if locale != DEFAULT_LOCALE => (locale, if rest.is_empty() { "/".to_owned() } else { rest.to_owned() }),
_ => (DEFAULT_LOCALE, clean),
}
}
pub fn locale_alternates(path: &str, locales: &[Locale]) -> Vec<(Locale, String)> {
locales.iter().map(|&l| (l, locale_path(l, path))).collect()
}
pub fn negotiate(header: Option<&str>, locales: &[Locale]) -> Locale {
let Some(header) = header else {
return DEFAULT_LOCALE;
};
let mut ranked: Vec<(String, f64)> = header
.split(',')
.filter_map(|part| {
let mut params = part.trim().split(';');
let tag = params.next().unwrap_or("").trim().to_ascii_lowercase();
let quality = params
.map(str::trim)
.find_map(|p| p.strip_prefix("q="))
.map_or(1.0, |q| q.parse::<f64>().unwrap_or(0.0));
(!tag.is_empty() && quality > 0.0).then_some((tag, quality))
})
.collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
for (tag, _) in ranked {
let base = tag.split('-').next().unwrap_or("");
if let Some(hit) = locales.iter().find(|l| l.code() == tag || l.code() == base) {
return *hit;
}
if tag == "*" {
return DEFAULT_LOCALE;
}
}
DEFAULT_LOCALE
}
pub type Messages = BTreeMap<String, String>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Translator {
messages: Messages,
locale: Locale,
}
impl Translator {
pub fn new(messages: Messages, locale: Locale) -> Self {
Self { messages, locale }
}
pub fn locale(&self) -> Locale {
self.locale
}
pub fn t(&self, key: &str) -> String {
self.render(key, &MessageValues::new())
}
pub fn tv(&self, key: &str, values: &MessageValues) -> String {
self.render(key, values)
}
pub fn count(&self, key: &str, name: &str, n: f64) -> String {
let mut values = MessageValues::new();
values.insert(name.to_owned(), MessageValue::Num(n));
self.render(key, &values)
}
pub fn has(&self, key: &str) -> bool {
self.messages.contains_key(key)
}
fn render(&self, key: &str, values: &MessageValues) -> String {
match self.messages.get(key) {
Some(pattern) => format_message(pattern, self.locale, values),
None => key.to_owned(),
}
}
}