use std::fmt::Write;
#[derive(Debug)]
pub enum Part {
Text(&'static str),
Named(&'static str),
List(usize),
Literal(&'static str),
Linked {
key: &'static str,
modifier: Option<&'static str>,
},
}
#[derive(Debug)]
pub struct Message {
pub cases: &'static [&'static [Part]],
}
#[derive(Debug)]
pub struct Locale {
pub name: &'static str,
pub messages: &'static [(&'static str, Message)],
}
impl Locale {
fn get(&'static self, key: &str) -> Option<&'static Message> {
self.messages
.binary_search_by(|(k, _)| (*k).cmp(key))
.ok()
.map(|i| &self.messages[i].1)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Value<'a> {
Str(&'a str),
Int(i64),
Float(f64),
Bool(bool),
}
impl Value<'_> {
fn write(&self, out: &mut String) {
match self {
Value::Str(s) => out.push_str(s),
Value::Int(n) => crate::push_int(out, *n),
Value::Float(x) => crate::push_number(out, *x),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
}
}
fn truthy(&self) -> bool {
match self {
Value::Str(s) => !s.is_empty(),
Value::Int(n) => *n != 0,
Value::Float(x) => *x != 0.0 && !x.is_nan(),
Value::Bool(b) => *b,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Args<'a> {
pub named: &'a [(&'a str, Value<'a>)],
pub list: &'a [Value<'a>],
pub plural: Option<i64>,
}
#[derive(Clone, Debug)]
pub struct I18n {
locales: &'static [Locale],
chain: Vec<usize>,
locale: String,
}
const MAX_LINK_DEPTH: usize = 32;
impl I18n {
pub fn new(locales: &'static [Locale], locale: &str, fallback: &[&str]) -> I18n {
let index = |name: &str| locales.iter().position(|l| l.name == name);
let mut chain: Vec<usize> = Vec::new();
for name in std::iter::once(locale).chain(fallback.iter().copied()) {
if let Some(i) = index(name)
&& !chain.contains(&i)
{
chain.push(i);
}
}
I18n {
locales,
chain,
locale: locale.to_owned(),
}
}
pub fn locale(&self) -> &str {
&self.locale
}
pub fn t(&self, key: &str, args: &Args<'_>) -> String {
match self.find(key) {
Some((_, message)) => {
let mut out = String::new();
self.evaluate(message, args, &mut out, 0);
out
}
None => key.to_owned(),
}
}
fn find(&self, key: &str) -> Option<(&'static Locale, &'static Message)> {
self.chain.iter().find_map(|&i| {
let locale = &self.locales[i];
locale.get(key).map(|m| (locale, m))
})
}
fn evaluate(&self, message: &Message, args: &Args<'_>, out: &mut String, depth: usize) {
let case = match message.cases {
[] => return,
[only] => *only,
cases => {
let index = plural_index(plural_choice(args), cases.len());
debug_assert!(
index.is_some(),
"a plural number of {} chooses none of the cases",
plural_choice(args)
);
match index.and_then(|i| cases.get(i)) {
Some(case) => *case,
None => return,
}
}
};
for part in case {
match part {
Part::Text(text) | Part::Literal(text) => out.push_str(text),
Part::Named(name) => {
if let Some(value) = named(args, name) {
value.write(out);
}
}
Part::List(index) => {
if let Some(value) = args.list.get(*index) {
value.write(out);
}
}
Part::Linked { key, modifier } => {
let mut linked = String::new();
if depth < MAX_LINK_DEPTH
&& let Some((_, m)) = self.find(key)
{
self.evaluate(m, args, &mut linked, depth + 1);
}
if linked.is_empty() {
linked.push_str(key);
}
match *modifier {
Some("upper") => out.push_str(&linked.to_uppercase()),
Some("lower") => out.push_str(&linked.to_lowercase()),
Some("capitalize") => {
let mut chars = linked.chars();
if let Some(first) = chars.next() {
let _ = write!(out, "{}{}", first.to_uppercase(), chars.as_str());
}
}
_ => out.push_str(&linked),
}
}
}
}
}
}
fn named<'a>(args: &Args<'a>, name: &str) -> Option<Value<'a>> {
let given = args.named.iter().find(|(k, _)| *k == name).map(|(_, v)| *v);
match (name, args.plural) {
("count" | "n", Some(n)) if !given.is_some_and(|v| v.truthy()) => Some(Value::Int(n)),
_ => given,
}
}
fn plural_choice(args: &Args<'_>) -> f64 {
for name in ["count", "n"] {
match args.named.iter().find(|(k, _)| *k == name).map(|(_, v)| *v) {
Some(Value::Int(n)) => return n as f64,
Some(Value::Float(x)) if x.is_finite() => return x,
_ => {}
}
}
args.plural.map_or(-1.0, |n| n as f64)
}
fn plural_index(choice: f64, cases: usize) -> Option<usize> {
let choice = choice.abs();
if cases == 2 {
Some(usize::from(choice != 1.0))
} else {
let index = choice.min(2.0);
(index.fract() == 0.0).then_some(index as usize)
}
}