use std::borrow::Cow;
#[doc(hidden)]
pub mod runtime_format {
pub struct FormatArg<'a> {
#[doc(hidden)]
pub format_str: &'a str,
#[doc(hidden)]
pub args: &'a [(&'static str, &'a dyn (::std::fmt::Display))],
}
impl<'a> ::std::fmt::Display for FormatArg<'a> {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let mut arg_idx = 0;
let mut pos = 0;
while let Some(mut p) = self.format_str[pos..].find(|x| x == '{' || x == '}') {
if self.format_str.len() - pos < p + 1 {
break;
}
p += pos;
if self.format_str.get(p..=p) == Some("}") {
self.format_str[pos..=p].fmt(f)?;
if self.format_str.get(p + 1..=p + 1) == Some("}") {
pos = p + 2;
} else {
pos = p + 1;
}
continue;
}
if self.format_str.get(p + 1..=p + 1) == Some("{") {
self.format_str[pos..=p].fmt(f)?;
pos = p + 2;
continue;
}
let end = if let Some(end) = self.format_str[p..].find('}') {
end + p
} else {
self.format_str[pos..=p].fmt(f)?;
pos = p + 1;
continue;
};
let argument = self.format_str[p + 1..end].trim();
let pa = if p == end - 1 {
arg_idx += 1;
arg_idx - 1
} else if let Ok(n) = argument.parse::<usize>() {
n
} else if let Some(p) = self.args.iter().position(|x| x.0 == argument) {
p
} else {
self.format_str[pos..end].fmt(f)?;
pos = end;
continue;
};
self.format_str[pos..p].fmt(f)?;
if let Some(a) = self.args.get(pa) {
a.1.fmt(f)?;
} else {
self.format_str[p..=end].fmt(f)?;
}
pos = end + 1;
}
self.format_str[pos..].fmt(f)
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! runtime_format {
($fmt:expr) => {{
format!("{}", $fmt)
}};
($fmt:expr, $($tail:tt)* ) => {{
let format_str = $fmt;
let fa = $crate::runtime_format::FormatArg {
format_str: AsRef::as_ref(&format_str),
args: $crate::runtime_format!(@parse_args [] $($tail)*)
};
format!("{}", fa)
}};
(@parse_args [$($args:tt)*]) => { &[ $( $args ),* ] };
(@parse_args [$($args:tt)*] $name:ident) => {
$crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$name)])
};
(@parse_args [$($args:tt)*] $name:ident, $($tail:tt)*) => {
$crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$name)] $($tail)*)
};
(@parse_args [$($args:tt)*] $name:ident = $e:expr) => {
$crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$e)])
};
(@parse_args [$($args:tt)*] $name:ident = $e:expr, $($tail:tt)*) => {
$crate::runtime_format!(@parse_args [$($args)* (stringify!($name) , &$e)] $($tail)*)
};
(@parse_args [$($args:tt)*] $e:expr) => {
$crate::runtime_format!(@parse_args [$($args)* ("" , &$e)])
};
(@parse_args [$($args:tt)*] $e:expr, $($tail:tt)*) => {
$crate::runtime_format!(@parse_args [$($args)* ("" , &$e)] $($tail)*)
};
}
#[cfg(test)]
mod tests {
#[test]
fn test_format() {
assert_eq!(runtime_format!("Hello"), "Hello");
assert_eq!(runtime_format!("Hello {}!", "world"), "Hello world!");
assert_eq!(runtime_format!("Hello {0}!", "world"), "Hello world!");
assert_eq!(
runtime_format!("Hello -{1}- -{0}-", 40 + 5, "World"),
"Hello -World- -45-"
);
assert_eq!(
runtime_format!(format!("Hello {{}}!"), format!("{}", "world")),
"Hello world!"
);
assert_eq!(
runtime_format!("Hello -{}- -{}-", 40 + 5, "World"),
"Hello -45- -World-"
);
assert_eq!(
runtime_format!("Hello {name}!", name = "world"),
"Hello world!"
);
let name = "world";
assert_eq!(runtime_format!("Hello {name}!", name), "Hello world!");
assert_eq!(runtime_format!("{} {}!", "Hello", name), "Hello world!");
assert_eq!(runtime_format!("{} {name}!", "Hello", name), "Hello world!");
assert_eq!(
runtime_format!("{0} {name}!", "Hello", name = "world"),
"Hello world!"
);
assert_eq!(
runtime_format!("Hello {{0}} {}", "world"),
"Hello {0} world"
);
}
}
}
pub trait Translator: Send + Sync {
fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str>;
fn ntranslate<'a>(
&'a self,
n: u64,
singular: &'a str,
plural: &'a str,
context: Option<&'a str>,
) -> Cow<'a, str>;
}
#[doc(hidden)]
pub mod internal {
use super::Translator;
use std::{borrow::Cow, collections::HashMap, sync::RwLock};
lazy_static::lazy_static! {
static ref TRANSLATORS: RwLock<HashMap<&'static str, Box<dyn Translator>>> =
Default::default();
}
pub fn with_translator<T>(module: &'static str, func: impl FnOnce(&dyn Translator) -> T) -> T {
let domain = domain_from_module(module);
let def = DefaultTranslator(domain);
func(
TRANSLATORS
.read()
.unwrap()
.get(domain)
.map(|x| &**x)
.unwrap_or(&def),
)
}
fn domain_from_module(module: &str) -> &str {
module.split("::").next().unwrap_or(module)
}
#[cfg(feature = "gettext-rs")]
fn mangle_context(ctx: &str, s: &str) -> String {
format!("{}\u{4}{}", ctx, s)
}
#[cfg(feature = "gettext-rs")]
fn demangle_context(r: String) -> String {
if let Some(x) = r.split('\u{4}').last() {
return x.to_owned();
}
r
}
struct DefaultTranslator(&'static str);
#[cfg(feature = "gettext-rs")]
impl Translator for DefaultTranslator {
fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str> {
Cow::Owned(if let Some(ctx) = context {
demangle_context(gettextrs::dgettext(self.0, &mangle_context(ctx, string)))
} else {
gettextrs::dgettext(self.0, string)
})
}
fn ntranslate<'a>(
&'a self,
n: u64,
singular: &'a str,
plural: &'a str,
context: Option<&'a str>,
) -> Cow<'a, str> {
let n = n as u32;
Cow::Owned(if let Some(ctx) = context {
demangle_context(gettextrs::dngettext(
self.0,
&mangle_context(ctx, singular),
&mangle_context(ctx, plural),
n,
))
} else {
gettextrs::dngettext(self.0, singular, plural, n)
})
}
}
#[cfg(not(feature = "gettext-rs"))]
impl Translator for DefaultTranslator {
fn translate<'a>(&'a self, string: &'a str, _context: Option<&'a str>) -> Cow<'a, str> {
Cow::Borrowed(string)
}
fn ntranslate<'a>(
&'a self,
n: u64,
singular: &'a str,
plural: &'a str,
_context: Option<&'a str>,
) -> Cow<'a, str> {
Cow::Borrowed(if n == 1 { singular } else { plural })
}
}
#[cfg(feature = "gettext-rs")]
pub fn init<T: Into<Vec<u8>>>(module: &'static str, dir: T) {
gettextrs::bindtextdomain::<Vec<u8>>(domain_from_module(module).into(), dir.into());
static START: std::sync::Once = std::sync::Once::new();
START.call_once(|| {
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
});
}
pub fn set_translator(module: &'static str, translator: impl Translator + 'static) {
TRANSLATORS
.write()
.unwrap()
.insert(module, Box::new(translator));
}
}
#[macro_export]
macro_rules! tr {
($msgid:tt, $($tail:tt)* ) => {
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.translate($msgid, None), $($tail)*))
};
($msgid:tt) => {
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.translate($msgid, None)))
};
($msgctx:tt => $msgid:tt, $($tail:tt)* ) => {
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.translate($msgid, Some($msgctx)), $($tail)*))
};
($msgctx:tt => $msgid:tt) => {
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.translate($msgid, Some($msgctx))))
};
($msgid:tt | $plur:tt % $n:expr, $($tail:tt)* ) => {{
let n = $n;
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.ntranslate(n as u64, $msgid, $plur, None), $($tail)*, n=n))
}};
($msgid:tt | $plur:tt % $n:expr) => {{
let n = $n;
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.ntranslate(n as u64, $msgid, $plur, None), n))
}};
($msgctx:tt => $msgid:tt | $plur:tt % $n:expr, $($tail:tt)* ) => {{
let n = $n;
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.ntranslate(n as u64, $msgid, $plur, Some($msgctx)), $($tail)*, n=n))
}};
($msgctx:tt => $msgid:tt | $plur:tt % $n:expr) => {{
let n = $n;
$crate::internal::with_translator(module_path!(), |t| $crate::runtime_format!(
t.ntranslate(n as u64, $msgid, $plur, Some($msgctx)), n))
}};
}
#[cfg(feature = "gettext-rs")]
#[macro_export]
macro_rules! tr_init {
($path:expr) => {
$crate::internal::init(module_path!(), $path)
};
}
#[macro_export]
macro_rules! set_translator {
($translator:expr) => {
$crate::internal::set_translator(module_path!(), $translator)
};
}
#[cfg(feature = "gettext")]
impl Translator for gettext::Catalog {
fn translate<'a>(&'a self, string: &'a str, context: Option<&'a str>) -> Cow<'a, str> {
Cow::Borrowed(if let Some(ctx) = context {
self.pgettext(ctx, string)
} else {
self.gettext(string)
})
}
fn ntranslate<'a>(
&'a self,
n: u64,
singular: &'a str,
plural: &'a str,
context: Option<&'a str>,
) -> Cow<'a, str> {
Cow::Borrowed(if let Some(ctx) = context {
self.npgettext(ctx, singular, plural, n)
} else {
self.ngettext(singular, plural, n)
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(tr!("Hello"), "Hello");
assert_eq!(tr!("ctx" => "Hello"), "Hello");
assert_eq!(tr!("Hello {}", "world"), "Hello world");
assert_eq!(tr!("ctx" => "Hello {}", "world"), "Hello world");
assert_eq!(
tr!("I have one item" | "I have {n} items" % 1),
"I have one item"
);
assert_eq!(
tr!("ctx" => "I have one item" | "I have {n} items" % 42),
"I have 42 items"
);
assert_eq!(
tr!("{} have one item" | "{} have {n} items" % 42, "I"),
"I have 42 items"
);
assert_eq!(
tr!("ctx" => "{0} have one item" | "{0} have {n} items" % 42, "I"),
"I have 42 items"
);
}
}