use std::
{
fs,
fmt::
{
Display,
Write,
},
sync::
{
LazyLock,
Mutex,
RwLock,
},
collections::HashMap,
};
use toml_edit::
{
DocumentMut,
Item,
Table,
};
use crate::
{
config,
consts,
misc,
};
struct Locale {
text: HashMap<String, String>,
plurals: String,
}
const ENGLISH: &str = include_str!("../locales/en.toml");
const CZECH: &str = include_str!("../locales/cz.toml"); const GERMAN: &str = include_str!("../locales/de.toml");
const SPANISH: &str = include_str!("../locales/es.toml");
const FRENCH: &str = include_str!("../locales/fr.toml");
const ITALIAN: &str = include_str!("../locales/it.toml");
const PORTUGUESE: &str = include_str!("../locales/pt.toml");
const DUTCH: &str = include_str!("../locales/nl.toml");
const POLISH: &str = include_str!("../locales/pl.toml");
const SLOVAK: &str = include_str!("../locales/sk.toml");
const RUSSIAN: &str = include_str!("../locales/ru.toml");
const UKRAINIAN: &str = include_str!("../locales/uk.toml");
const TURKISH: &str = include_str!("../locales/tr.toml");
const CHINESE: &str = include_str!("../locales/zh.toml");
const JAPANESE: &str = include_str!("../locales/ja.toml");
const KOREAN: &str = include_str!("../locales/ko.toml");
const BUILTIN: &[(&str, &str)] = &[
("en", ENGLISH),
("cs", CZECH),
("de", GERMAN),
("es", SPANISH),
("fr", FRENCH),
("it", ITALIAN),
("pt", PORTUGUESE),
("nl", DUTCH),
("pl", POLISH),
("sk", SLOVAK),
("ru", RUSSIAN),
("uk", UKRAINIAN),
("tr", TURKISH),
("zh", CHINESE),
("ja", JAPANESE),
("ko", KOREAN),
];
static FALLBACK: LazyLock<Locale> = LazyLock::new(|| parse(ENGLISH).expect("Parsing the English locale failed"));
static LOADED: LazyLock<Mutex<HashMap<String, Option<&'static Locale>>>> = LazyLock::new(Default::default);
static ACTIVE: LazyLock<RwLock<Option<&'static Locale>>> = LazyLock::new(||
{
RwLock::new(load(config::read_config::<String>("language").trim()))
});
#[macro_export]
macro_rules! t {
(@value $name:ident = $value:expr) => { $value };
(@value $name:ident) => { $name };
($key:literal) => { $crate::i18n::text($key) };
($key:literal $(, $name:ident $(= $value:expr)?)+ $(,)?) =>
{
$crate::i18n::format($crate::i18n::text($key),
&[$((stringify!($name), &$crate::t!(@value $name $(= $value)?) as &dyn ::std::fmt::Display)),+])
};
}
#[macro_export]
macro_rules! tn {
($key:literal, $count:expr $(, $name:ident $(= $value:expr)?)* $(,)?) =>
{{
let count = $count;
$crate::i18n::format($crate::i18n::plural($key, count as u64),
&[("count", &count as &dyn ::std::fmt::Display) $(, (stringify!($name), &$crate::t!(@value $name $(= $value)?) as &dyn ::std::fmt::Display))*])
}};
}
fn locales_dir() -> String
{
format!("{}{}", misc::get_why2_dir(), consts::LOCALES_DIR)
}
fn load(code: &str) -> Option<&'static Locale> {
*LOADED.lock().unwrap().entry(code.to_owned()).or_insert_with(||
{
fs::read_to_string(format!("{}/{code}.toml", locales_dir())).ok().and_then(|content| parse(&content))
.or_else(|| BUILTIN.iter().find(|(name, _)| *name == code).and_then(|(_, content)| parse(content)))
.map(|locale| &*Box::leak(Box::new(locale)))
})
}
fn active() -> Option<&'static Locale>
{
*ACTIVE.read().unwrap()
}
fn parse(content: &str) -> Option<Locale>
{
let document = content.parse::<DocumentMut>().ok()?;
let mut text = HashMap::new();
flatten(document.as_table(), "", &mut text);
let plurals = text.remove("meta.plurals").unwrap_or_else(|| String::from("one-other"));
Some(Locale { text, plurals })
}
fn flatten(table: &Table, prefix: &str, out: &mut HashMap<String, String>) {
for (key, item) in table
{
let key = format!("{prefix}{key}");
match item
{
Item::Table(table) => flatten(table, &format!("{key}."), out),
Item::Value(value) => if let Some(value) = value.as_str() { out.insert(key, value.to_owned()); },
_ => {},
}
}
}
fn category(rule: &str, count: u64) -> &'static str {
match rule
{
"other" => "other",
"one-few-other" => match count
{
1 => "one",
2..=4 => "few",
_ => "other",
},
"one-few-many" => match (count % 10, count % 100)
{
(1, rest) if rest != 11 => "one",
(2..=4, rest) if !(12..=14).contains(&rest) => "few",
_ => "many",
},
"polish" => match (count, count % 10, count % 100)
{
(1, ..) => "one",
(_, 2..=4, rest) if !(12..=14).contains(&rest) => "few",
_ => "many",
},
_ => if count == 1 { "one" } else { "other" },
}
}
fn lookup(locale: &'static Locale, key: &str) -> Option<&'static str>
{
locale.text.get(key).map(String::as_str)
}
fn plural_in(locale: &'static Locale, key: &str, count: u64) -> Option<&'static str>
{
lookup(locale, &format!("{key}.{}", category(&locale.plurals, count)))
.or_else(|| lookup(locale, &format!("{key}.other")))
}
pub fn languages() -> Vec<String> {
let mut codes: Vec<String> = BUILTIN.iter().map(|(code, _)| (*code).to_owned()).collect();
let mut extra: Vec<String> = fs::read_dir(locales_dir()).into_iter().flatten().flatten()
.filter_map(|entry|
{
let path = entry.path();
if path.extension()? != "toml" { return None; }
path.file_stem()?.to_str().map(str::to_owned)
})
.filter(|code| !codes.contains(code))
.collect();
extra.sort();
codes.append(&mut extra);
codes.retain(|code| load(code).is_some());
codes
}
pub fn language_name(code: &str) -> &str {
load(code).and_then(|locale| lookup(locale, "meta.name")).unwrap_or(code)
}
pub fn set_language(code: &str) {
*ACTIVE.write().unwrap() = load(code);
}
pub fn get(key: &str) -> Option<&'static str> {
active().and_then(|locale| lookup(locale, key)).or_else(|| lookup(&FALLBACK, key))
}
pub fn text(key: &'static str) -> &'static str {
get(key).unwrap_or(key)
}
pub fn plural(key: &'static str, count: u64) -> &'static str {
active().and_then(|locale| plural_in(locale, key, count))
.or_else(|| plural_in(&FALLBACK, key, count))
.unwrap_or(key)
}
pub fn format(template: &str, args: &[(&str, &dyn Display)]) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(at) = rest.find(['{', '}'])
{
out.push_str(&rest[..at]);
let tail = &rest[at..];
if tail.starts_with("{{") || tail.starts_with("}}")
{
out.push_str(&tail[..1]);
rest = &tail[2..];
continue;
}
if tail.starts_with('{')
&& let Some(close) = tail.find('}')
&& let Some((_, value)) = args.iter().find(|(name, _)| *name == &tail[1..close])
{
write!(out, "{value}").ok();
rest = &tail[close + 1..];
continue;
}
out.push_str(&tail[..1]);
rest = &tail[1..];
}
out.push_str(rest);
out
}
pub fn split<'a>(template: &'a str, name: &str) -> (&'a str, &'a str) {
template.split_once(&format!("{{{name}}}")).unwrap_or((template, ""))
}