use std::collections::HashMap;
use std::sync::Arc;
use crate::interpolate::interpolate;
use crate::locale::Locale;
use crate::plural::plural_category;
pub trait Backend: Send + Sync + 'static {
fn get(&self, locale: &str, key: &str) -> Option<String>;
fn available_locales(&self) -> Vec<String>;
fn has_locale(&self, locale: &str) -> bool;
fn dump(&self, locale: &str) -> HashMap<String, String>;
}
pub trait Reloadable: Backend {
fn reload_from_str(&self, locale: &str, content: &str) -> Result<(), crate::error::I18nError>;
fn file_extension(&self) -> &'static str;
}
#[derive(Clone)]
pub struct I18n {
backend: Arc<dyn Backend>,
default_locale: String,
fallback_locale: String,
}
impl std::fmt::Debug for I18n {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("I18n")
.field("default_locale", &self.default_locale)
.field("fallback_locale", &self.fallback_locale)
.field("available_locales", &self.available_locales())
.finish_non_exhaustive()
}
}
impl I18n {
pub fn new(
backend: Arc<dyn Backend>,
default_locale: impl Into<String>,
fallback_locale: impl Into<String>,
) -> Self {
Self {
backend,
default_locale: default_locale.into(),
fallback_locale: fallback_locale.into(),
}
}
pub fn t(&self, key: &str) -> String {
self.translate(key, &self.default_locale, &[], None)
}
pub fn t_with_locale(&self, key: &str, locale: &str) -> String {
self.translate(key, locale, &[], None)
}
pub fn t_with_args(&self, key: &str, locale: &str, args: &[(&str, &str)]) -> String {
self.translate(key, locale, args, None)
}
pub fn t_with_count(
&self,
key: &str,
locale: &str,
count: i64,
args: &[(&str, &str)],
) -> String {
self.translate(key, locale, args, Some(count))
}
pub fn translate(
&self,
key: &str,
locale: &str,
args: &[(&str, &str)],
count: Option<i64>,
) -> String {
let lookup_key = match count {
Some(c) => {
let cat = plural_category(locale, c);
let plural_key = format!("{key}.{}", cat.suffix());
if self.backend.get(locale, &plural_key).is_some() {
plural_key
} else {
key.to_string()
}
}
None => key.to_string(),
};
let parsed = Locale::parse(locale).unwrap_or_else(|_| Locale::language_only(locale));
let chain = parsed.fallback_chain(&self.fallback_locale);
for loc in &chain {
if let Some(template) = self.backend.get(loc, &lookup_key) {
let mut all_args: Vec<(&str, String)> =
args.iter().map(|(k, v)| (*k, v.to_string())).collect();
if let Some(c) = count {
all_args.push(("count", c.to_string()));
}
return interpolate(&template, &all_args);
}
}
tracing::warn!(key = %key, locale = %locale, "translation key not found");
key.to_string()
}
pub fn default_locale(&self) -> &str {
&self.default_locale
}
pub fn fallback_locale(&self) -> &str {
&self.fallback_locale
}
pub fn available_locales(&self) -> Vec<String> {
self.backend.available_locales()
}
pub fn backend(&self) -> &Arc<dyn Backend> {
&self.backend
}
}