pub mod err {
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("IO error: `{0}`")]
Io(#[from] std::io::Error),
#[error("strfmt error: `{0}`")]
Strfmt(#[from] strfmt::FmtError),
#[cfg(feature = "yaml")]
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[cfg(feature = "toml")]
#[error("TOML error: {0}")]
Toml(#[from] toml::de::Error),
#[error("Error: {0}")]
Custom(Box<str>),
#[error("Unknown locale: {0}")]
UnknownLocale(Box<str>),
#[error("Unknown key: {0}")]
UnknownKey(Box<str>),
}
pub fn custom<T: std::fmt::Display>(t: T) -> Error {
Error::Custom(t.to_string().into_boxed_str())
}
pub type Result<T> = std::result::Result<T, Error>;
}
mod config;
mod key;
mod opts;
pub mod helpers {
pub mod config {
pub use crate::config::{DefaultLocale, LocalizedPath, PathPattern};
}
pub mod opts {
pub use crate::opts::{Count, DefaultKey, Locale, Var};
}
}
pub mod prelude {
pub use crate::{
helpers::{config::*, opts::*},
Config,
Dictionary,
Opts,
translate,
t
};
}
use once_cell::sync::{Lazy, OnceCell};
use std::collections::HashMap;
pub use config::Config;
pub use key::Key;
pub use opts::Opts;
#[derive(Debug)]
pub struct Dictionary {
inner: HashMap<String, serde_json::Value>,
default_locale: String,
}
impl Default for Dictionary {
fn default() -> Self {
Self { inner: HashMap::new(), default_locale: "en".into() }
}
}
impl Dictionary {
pub fn translate<'a, K: Into<Key<'a>>, I: Into<Opts<'a>>>(
&self,
key: K,
opts: I,
) -> err::Result<String> {
let opts = opts.into();
let mut key = key.into();
let alt_key;
match opts.count {
Some(0) => {
alt_key = key.chain(["zero"].as_ref());
key = alt_key;
}
Some(1) => {
alt_key = key.chain(["one"].as_ref());
key = alt_key;
}
Some(_) => {
alt_key = key.chain(["other"].as_ref());
key = alt_key;
}
_ => {}
}
let locale = opts.locale.unwrap_or_else(|| &self.default_locale);
let localized = self
.inner
.get(locale)
.ok_or_else(|| err::Error::UnknownLocale(String::from(locale).into_boxed_str()))?;
let entry = |key: Key| {
key.find(localized)
.and_then(|val| val.as_str())
.map(String::from)
.ok_or_else(|| err::Error::UnknownKey(key.to_string().into_boxed_str()))
};
let value = match entry(key) {
Ok(value) => value,
Err(e) => match opts.default_key {
Some(default_key) => {
return entry(default_key);
}
_ => {
return Err(e);
}
},
};
match opts.vars {
Some(vars) => Ok(strfmt::strfmt(&value, &vars)?),
None => Ok(value),
}
}
pub fn t<'a, K: Into<Key<'a>>, I: Into<Opts<'a>>>(
&self,
key: K,
opts: I,
) -> err::Result<String> {
self.translate(key, opts)
}
}
static CONFIG: OnceCell<Config> = OnceCell::new();
pub fn set_config<I: Into<Config>>(config: I) -> err::Result<()> {
Ok(CONFIG.set(config.into()).map_err(|_| err::custom("`CONFIG` already set"))?)
}
pub fn translate<'a, K: Into<Key<'a>>, I: Into<Opts<'a>>>(key: K, opts: I) -> err::Result<String> {
static DICTIONARY_RESULT: Lazy<err::Result<Dictionary>> =
Lazy::new(|| CONFIG.get_or_init(Config::global).clone().finish());
DICTIONARY_RESULT.as_ref().map_err(err::custom).and_then(|dict| dict.translate(key, opts))
}
pub fn t<'a, K: Into<Key<'a>>, I: Into<Opts<'a>>>(key: K, opts: I) -> err::Result<String> {
translate(key, opts)
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[test]
fn it_works() {
crate::set_config(PathPattern("examples/locales/*.yml")).unwrap();
assert_eq!(t(&["greeting"], None).unwrap(), String::from("Hello, World!"));
assert_eq!(
t("missed", DefaultKey("missing.default")).unwrap(),
String::from("Sorry, that translation doesn't exist.")
);
assert_eq!(
t(&["custom", "greeting"], Var("name", "Jacob")).unwrap(),
String::from("Hello, Jacob!!!")
);
assert_eq!(
t("greeting", Opts::default().locale("de")).unwrap(),
String::from("Hallo Welt!")
);
assert_eq!(
t("messages", Opts::default().count(1)).unwrap(),
String::from("You have one message.")
);
assert_eq!(
t("messages", Opts::default().count(0)).unwrap(),
String::from("You have no messages.")
);
assert_eq!(t("messages", Count(200)).unwrap(), String::from("You have 200 messages."));
assert!(t("message.x", ()).is_err());
assert_eq!(
t(
"a.very.nested.message",
(Var("name", "you"), Var("message", "\"a very nested message\""))
)
.unwrap(),
String::from("Hello, you. Your message is: \"a very nested message\"")
);
}
}