use std::collections::HashMap;
use std::sync::LazyLock;
use crate::js::{self, Value};
const LANGS: [&str; 5] = ["en", "de", "es", "fr", "pt"];
fn group(lang: &str) -> &'static str {
match lang {
"de" | "es" | "pt" => ".",
"fr" => "\u{202F}",
_ => ",",
}
}
fn decimal_mark(lang: &str) -> &'static str {
if lang == "en" { "." } else { "," }
}
const MONTHS: [[&str; 12]; 5] = [
[
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
[
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember",
],
[
"enero",
"febrero",
"marzo",
"abril",
"mayo",
"junio",
"julio",
"agosto",
"septiembre",
"octubre",
"noviembre",
"diciembre",
],
[
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"août",
"septembre",
"octobre",
"novembre",
"décembre",
],
[
"janeiro",
"fevereiro",
"março",
"abril",
"maio",
"junho",
"julho",
"agosto",
"setembro",
"outubro",
"novembro",
"dezembro",
],
];
const SHORT_MONTHS: [[&str; 12]; 5] = [
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
["Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez."],
["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sept", "oct", "nov", "dic"],
["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."],
["jan.", "fev.", "mar.", "abr.", "mai.", "jun.", "jul.", "ago.", "set.", "out.", "nov.", "dez."],
];
const DATE_PATTERNS: [[&str; 3]; 5] = [
["{M} {y}", "{m} {d}", "{m} {d}, {y}"],
["{M} {y}", "{d}. {m}", "{d}. {m} {y}"],
["{M} de {y}", "{d} {m}", "{d} {m} {y}"],
["{M} {y}", "{d} {m}", "{d} {m} {y}"],
["{M} de {y}", "{d} de {m}", "{d} de {m} de {y}"],
];
fn index(lang: &str) -> usize {
LANGS.iter().position(|l| *l == lang).unwrap_or(0)
}
fn lang_of(lang: &str) -> &'static str {
LANGS[index(lang)]
}
struct Money {
unknown: (String, String),
symbols: HashMap<String, (String, String)>,
digits: HashMap<String, usize>,
}
struct Data {
regions: HashMap<String, HashMap<String, String>>,
money: HashMap<String, Money>,
}
static DATA: LazyLock<Data> = LazyLock::new(|| {
let raw = js::parse(include_str!("../assets/intl.json")).expect("assets/intl.json is JSON");
let pair = |v: &Value| -> (String, String) {
let a = v.as_array().map(Vec::as_slice).unwrap_or(&[]);
let side = |i: usize| a.get(i).and_then(Value::as_str).unwrap_or("").to_string();
(side(0), side(1))
};
let mut regions = HashMap::new();
if let Some(langs) = raw.get("regions").and_then(Value::as_object) {
for (lang, names) in langs.iter() {
let mut own = HashMap::new();
for (code, name) in names.as_object().map(|o| o.iter().collect::<Vec<_>>()).unwrap_or_default() {
own.insert(code.to_string(), name.as_str().unwrap_or(code).to_string());
}
regions.insert(lang.to_string(), own);
}
}
let mut money = HashMap::new();
if let Some(langs) = raw.get("currencies").and_then(Value::as_object) {
for (lang, m) in langs.iter() {
let symbols = m
.get("symbols")
.and_then(Value::as_object)
.map(|o| o.iter().map(|(code, v)| (code.to_string(), pair(v))).collect())
.unwrap_or_default();
let digits = m
.get("digits")
.and_then(Value::as_object)
.map(|o| o.iter().map(|(code, v)| (code.to_string(), v.as_f64().unwrap_or(2.0) as usize)).collect())
.unwrap_or_default();
money.insert(lang.to_string(), Money { unknown: pair(m.at("unknown")), symbols, digits });
}
}
Data { regions, money }
});
pub fn number(lang: &str, n: f64, min_fraction: usize, max_fraction: usize) -> String {
let lang = lang_of(lang);
if n.is_nan() {
return "NaN".into();
}
if n.is_infinite() {
return format!("{}∞", if n < 0.0 { "-" } else { "" });
}
let (negative, digits, point) = shortest(n);
written(lang, negative, &digits, point, min_fraction, max_fraction)
}
pub fn percent(lang: &str, n: f64) -> String {
let lang = lang_of(lang);
let amount = if n.is_finite() {
let (negative, digits, point) = shortest(n);
written(lang, negative, &digits, point + 2, 0, 0)
} else {
number(lang, n, 0, 0)
};
match lang {
"en" | "pt" => format!("{amount}%"),
_ => format!("{amount}\u{A0}%"),
}
}
pub fn currency(lang: &str, n: f64, currency: &str, max_fraction: usize) -> String {
let lang = lang_of(lang);
if currency.len() != 3 || !currency.bytes().all(|b| b.is_ascii_alphabetic()) {
return format!("{} {currency}", js::format_number(n));
}
let code = currency.to_ascii_uppercase();
let Some(money) = DATA.money.get(lang) else {
return format!("{} {currency}", js::format_number(n));
};
let (before, after) = money
.symbols
.get(&code)
.cloned()
.unwrap_or_else(|| (money.unknown.0.replace("{c}", &code), money.unknown.1.replace("{c}", &code)));
let min = money.digits.get(&code).copied().unwrap_or(2).min(max_fraction);
let negative = n < 0.0 || (n == 0.0 && n.is_sign_negative());
let amount = number(lang, n.abs(), min, max_fraction);
format!("{}{before}{amount}{after}", if negative { "-" } else { "" })
}
pub fn month_year(lang: &str, date: &str) -> String {
date_text(lang, date, 0)
}
pub fn short_day(lang: &str, date: &str, with_year: bool) -> String {
date_text(lang, date, if with_year { 2 } else { 1 })
}
fn date_text(lang: &str, date: &str, pattern: usize) -> String {
let at = index(lang);
let mut parts = date.split('-').map(|p| p.parse::<i64>().unwrap_or(0));
let (y, m, d) = (parts.next().unwrap_or(0), parts.next().unwrap_or(1), parts.next().unwrap_or(1));
let month = (m.clamp(1, 12) - 1) as usize;
DATE_PATTERNS[at][pattern]
.replace("{M}", MONTHS[at][month])
.replace("{m}", SHORT_MONTHS[at][month])
.replace("{d}", &d.to_string())
.replace("{y}", &y.to_string())
}
pub fn region(lang: &str, code: &str) -> String {
let b = code.as_bytes();
let shaped =
(b.len() == 2 && b.iter().all(u8::is_ascii_uppercase)) || (b.len() == 3 && b.iter().all(u8::is_ascii_digit));
if !shaped {
return code.to_string();
}
DATA.regions.get(lang_of(lang)).and_then(|names| names.get(code)).cloned().unwrap_or_else(|| code.to_string())
}
fn shortest(n: f64) -> (bool, String, i64) {
let negative = n < 0.0 || (n == 0.0 && n.is_sign_negative());
if n == 0.0 {
return (negative, "0".into(), 1);
}
let text = format!("{:e}", n.abs());
let (mantissa, exponent) = text.split_once('e').unwrap_or((&text, "0"));
let exponent: i64 = exponent.parse().unwrap_or(0);
let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
let trimmed = digits.trim_end_matches('0');
let trimmed = if trimmed.is_empty() { "0" } else { trimmed };
(negative, trimmed.to_string(), 1 + exponent)
}
fn written(lang: &str, negative: bool, digits: &str, point: i64, min: usize, max: usize) -> String {
let keep = point + max as i64;
let units = if keep < 0 {
"0".to_string()
} else if digits.len() as i64 > keep {
let k = keep as usize;
let head = if k == 0 { "0".to_string() } else { digits[..k].to_string() };
if digits.as_bytes()[k] >= b'5' { crate::messages::increment(&head) } else { head }
} else {
format!("{digits}{}", "0".repeat(keep as usize - digits.len()))
};
let units = format!("{}{units}", "0".repeat((max + 1).saturating_sub(units.len())));
let whole = units[..units.len() - max].trim_start_matches('0');
let whole = if whole.is_empty() { "0" } else { whole };
let fraction = if max > 0 { &units[units.len() - max..] } else { "" };
let mut fraction = fraction.trim_end_matches('0').to_string();
while fraction.len() < min {
fraction.push('0');
}
let grouped = if lang == "es" && whole.len() < 5 {
whole.to_string()
} else {
let mut out = String::new();
for (i, c) in whole.chars().enumerate() {
if i > 0 && (whole.len() - i) % 3 == 0 {
out.push_str(group(lang));
}
out.push(c);
}
out
};
format!(
"{}{grouped}{}",
if negative { "-" } else { "" },
if fraction.is_empty() { String::new() } else { format!("{}{fraction}", decimal_mark(lang)) }
)
}