use std::collections::HashMap;
pub type LocaleId = String;
pub type TranslationKey = String;
pub type PluralRule = fn(n: usize) -> usize;
pub fn default_plural_rule(n: usize) -> usize {
if n == 1 {
0
} else {
1
}
}
#[derive(Clone, Debug)]
pub struct Locale {
pub id: LocaleId,
pub name: String,
pub native_name: String,
pub direction: Direction,
plural_rule: PluralRule,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Direction {
#[default]
Ltr,
Rtl,
}
impl Locale {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
let id = id.into();
let name = name.into();
Self {
native_name: name.clone(),
id,
name,
direction: Direction::Ltr,
plural_rule: default_plural_rule,
}
}
pub fn native_name(mut self, name: impl Into<String>) -> Self {
self.native_name = name.into();
self
}
pub fn direction(mut self, dir: Direction) -> Self {
self.direction = dir;
self
}
pub fn plural_rule(mut self, rule: PluralRule) -> Self {
self.plural_rule = rule;
self
}
pub fn get_plural_form(&self, n: usize) -> usize {
(self.plural_rule)(n)
}
}
impl Locale {
pub fn english() -> Self {
Locale::new("en", "English").native_name("English")
}
pub fn korean() -> Self {
Locale::new("ko", "Korean")
.native_name("한국어")
.plural_rule(|_| 0) }
pub fn japanese() -> Self {
Locale::new("ja", "Japanese")
.native_name("日本語")
.plural_rule(|_| 0) }
pub fn chinese_simplified() -> Self {
Locale::new("zh-CN", "Chinese (Simplified)")
.native_name("简体中文")
.plural_rule(|_| 0)
}
pub fn chinese_traditional() -> Self {
Locale::new("zh-TW", "Chinese (Traditional)")
.native_name("繁體中文")
.plural_rule(|_| 0)
}
pub fn spanish() -> Self {
Locale::new("es", "Spanish").native_name("Español")
}
pub fn french() -> Self {
Locale::new("fr", "French")
.native_name("Français")
.plural_rule(|n| if n <= 1 { 0 } else { 1 })
}
pub fn german() -> Self {
Locale::new("de", "German").native_name("Deutsch")
}
pub fn russian() -> Self {
Locale::new("ru", "Russian")
.native_name("Русский")
.plural_rule(|n| {
if n % 10 == 1 && n % 100 != 11 {
0
} else if n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) {
1
} else {
2
}
})
}
pub fn arabic() -> Self {
Locale::new("ar", "Arabic")
.native_name("العربية")
.direction(Direction::Rtl)
.plural_rule(|n| {
if n == 0 {
0
} else if n == 1 {
1
} else if n == 2 {
2
} else if n % 100 >= 3 && n % 100 <= 10 {
3
} else if n % 100 >= 11 {
4
} else {
5
}
})
}
}
#[derive(Clone, Debug)]
pub struct Translation {
singular: String,
plurals: Vec<String>,
}
impl Translation {
pub fn simple(text: impl Into<String>) -> Self {
Self {
singular: text.into(),
plurals: Vec::new(),
}
}
pub fn with_plural(singular: impl Into<String>, plural: impl Into<String>) -> Self {
Self {
singular: singular.into(),
plurals: vec![plural.into()],
}
}
pub fn with_plurals(singular: impl Into<String>, plurals: Vec<String>) -> Self {
Self {
singular: singular.into(),
plurals,
}
}
pub fn get(&self, form: usize) -> &str {
if form == 0 || self.plurals.is_empty() {
&self.singular
} else {
self.plurals.get(form - 1).unwrap_or(&self.singular)
}
}
}
impl<S: Into<String>> From<S> for Translation {
fn from(s: S) -> Self {
Translation::simple(s)
}
}
#[derive(Clone, Debug)]
pub struct I18n {
locales: HashMap<LocaleId, Locale>,
translations: HashMap<LocaleId, HashMap<TranslationKey, Translation>>,
current_locale: LocaleId,
fallback_locale: LocaleId,
}
impl I18n {
pub fn new() -> Self {
let mut instance = Self {
locales: HashMap::new(),
translations: HashMap::new(),
current_locale: "en".to_string(),
fallback_locale: "en".to_string(),
};
instance.add_locale(Locale::english());
instance
}
pub fn with_locale(locale: Locale) -> Self {
let id = locale.id.clone();
let mut instance = Self {
locales: HashMap::new(),
translations: HashMap::new(),
current_locale: id.clone(),
fallback_locale: id,
};
instance.add_locale(locale);
instance
}
pub fn add_locale(&mut self, locale: Locale) {
let id = locale.id.clone();
self.locales.insert(id.clone(), locale);
self.translations.entry(id).or_default();
}
pub fn set_locale(&mut self, locale: impl Into<String>) {
let locale = locale.into();
if self.locales.contains_key(&locale) {
self.current_locale = locale;
}
}
pub fn locale(&self) -> &str {
&self.current_locale
}
pub fn current_locale(&self) -> Option<&Locale> {
self.locales.get(&self.current_locale)
}
pub fn set_fallback(&mut self, locale: impl Into<String>) {
self.fallback_locale = locale.into();
}
pub fn available_locales(&self) -> Vec<&Locale> {
self.locales.values().collect()
}
pub fn add_translation(&mut self, locale: &str, key: &str, value: impl Into<Translation>) {
if let Some(translations) = self.translations.get_mut(locale) {
translations.insert(key.to_string(), value.into());
}
}
pub fn add_translations(&mut self, locale: &str, translations: HashMap<String, Translation>) {
if let Some(existing) = self.translations.get_mut(locale) {
existing.extend(translations);
}
}
pub fn t<'a>(&'a self, key: &'a str) -> &'a str {
self.get_translation(key, &self.current_locale)
.or_else(|| self.get_translation(key, &self.fallback_locale))
.unwrap_or(key)
}
pub fn t_args(&self, key: &str, args: &[(&str, &str)]) -> String {
let mut result = self.t(key).to_string();
for (name, value) in args {
result = result.replace(&format!("{{{}}}", name), value);
}
result
}
pub fn t_plural<'a>(&'a self, key: &'a str, n: usize) -> &'a str {
let form = self
.current_locale()
.map(|l| l.get_plural_form(n))
.unwrap_or(if n == 1 { 0 } else { 1 });
self.get_translation_with_form(key, &self.current_locale, form)
.or_else(|| self.get_translation_with_form(key, &self.fallback_locale, form))
.unwrap_or(key)
}
pub fn t_plural_args(&self, key: &str, n: usize, args: &[(&str, &str)]) -> String {
let mut result = self.t_plural(key, n).to_string();
for (name, value) in args {
result = result.replace(&format!("{{{}}}", name), value);
}
result
}
fn get_translation(&self, key: &str, locale: &str) -> Option<&str> {
self.translations
.get(locale)
.and_then(|t| t.get(key))
.map(|t| t.get(0))
}
fn get_translation_with_form(&self, key: &str, locale: &str, form: usize) -> Option<&str> {
self.translations
.get(locale)
.and_then(|t| t.get(key))
.map(|t| t.get(form))
}
pub fn has_translation(&self, key: &str) -> bool {
self.translations
.get(&self.current_locale)
.map(|t| t.contains_key(key))
.unwrap_or(false)
}
pub fn direction(&self) -> Direction {
self.current_locale()
.map(|l| l.direction)
.unwrap_or_default()
}
pub fn is_rtl(&self) -> bool {
self.direction() == Direction::Rtl
}
}
impl Default for I18n {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! translations {
($locale:expr => { $($key:expr => $value:expr),* $(,)? }) => {{
let mut map = std::collections::HashMap::new();
$(
map.insert($key.to_string(), $crate::utils::i18n::Translation::simple($value));
)*
map
}};
}