Remove all .rs bootstrap files from el-ui vessels
El SDK CI - dev / build-and-test (pull_request) Successful in 3m33s

el-ui vessels are El. The Rust bootstrap implementations were added as
a stopgap but don't belong here — everything should be El source.
Each vessel's src/main.el and manifest.el are the source of truth.
This commit is contained in:
2026-05-06 23:12:25 -05:00
parent e8b01583d8
commit 5d9299a472
98 changed files with 0 additions and 17426 deletions
-299
View File
@@ -1,299 +0,0 @@
/// LocaleBundle — loads and caches translation strings.
///
/// A bundle holds all translation strings for one locale. Strings are
/// keyed by dot-delimited paths (e.g. "profile.followers"). The bundle
/// supports both flat strings and plural forms.
use std::collections::HashMap;
use crate::locale::Locale;
use crate::plural::{plural_form, PluralForm};
/// A single translation value — either a simple string or a plural map.
#[derive(Debug, Clone)]
pub enum TranslationValue {
/// A simple translated string. May contain `{key}` interpolation placeholders.
Simple(String),
/// A plural-form map. Keys are form names: "zero", "one", "two", "few", "many", "other".
Plural(HashMap<String, String>),
}
/// A bundle of translations for a single locale.
#[derive(Debug, Clone)]
pub struct LocaleBundle {
pub locale: Locale,
translations: HashMap<String, TranslationValue>,
}
impl LocaleBundle {
/// Create an empty bundle for a locale.
pub fn new(locale: Locale) -> Self {
Self {
locale,
translations: HashMap::new(),
}
}
/// Insert a simple translation.
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.translations.insert(
key.into(),
TranslationValue::Simple(value.into()),
);
}
/// Insert a plural translation.
pub fn insert_plural(
&mut self,
key: impl Into<String>,
forms: HashMap<String, String>,
) {
self.translations
.insert(key.into(), TranslationValue::Plural(forms));
}
/// Look up a key and return the simple string (no interpolation).
pub fn get_raw(&self, key: &str) -> Option<&str> {
match self.translations.get(key)? {
TranslationValue::Simple(s) => Some(s.as_str()),
TranslationValue::Plural(_) => None,
}
}
/// Look up a key with variable interpolation.
///
/// Replaces `{name}` placeholders with values from `vars`.
pub fn translate(&self, key: &str, vars: &HashMap<&str, String>) -> Option<String> {
let raw = match self.translations.get(key)? {
TranslationValue::Simple(s) => s.clone(),
TranslationValue::Plural(forms) => {
// For translate(), use "other" as default
forms.get("other")?.clone()
}
};
Some(interpolate(&raw, vars))
}
/// Look up a plural key with a count.
///
/// Selects the correct plural form for the locale's language and count,
/// then interpolates `{n}` and any other `vars`.
pub fn translate_plural(
&self,
key: &str,
count: i64,
vars: &HashMap<&str, String>,
) -> Option<String> {
let forms = match self.translations.get(key)? {
TranslationValue::Plural(f) => f,
TranslationValue::Simple(s) => {
// Fall through: treat the simple string as "other"
let mut result_vars = vars.clone();
result_vars.insert("n", count.to_string());
return Some(interpolate(s, &result_vars));
}
};
let form = plural_form(&self.locale.language, count);
let form_key = match form {
PluralForm::Zero => "zero",
PluralForm::One => "one",
PluralForm::Two => "two",
PluralForm::Few => "few",
PluralForm::Many => "many",
PluralForm::Other => "other",
};
let template = forms
.get(form_key)
.or_else(|| forms.get("other"))?;
let mut result_vars = vars.clone();
result_vars.insert("n", count.to_string());
Some(interpolate(template, &result_vars))
}
/// Number of translations loaded.
pub fn len(&self) -> usize {
self.translations.len()
}
pub fn is_empty(&self) -> bool {
self.translations.is_empty()
}
}
/// Replace `{key}` placeholders in `template` with values from `vars`.
fn interpolate(template: &str, vars: &HashMap<&str, String>) -> String {
let mut result = template.to_string();
for (key, value) in vars {
result = result.replace(&format!("{{{}}}", key), value);
}
result
}
/// Load a bundle from a TOML string.
///
/// Format:
/// ```toml
/// [profile]
/// follow = "Follow"
/// followers = { one = "{n} Follower", other = "{n} Followers" }
/// ```
pub fn load_toml(locale: Locale, toml_str: &str) -> Result<LocaleBundle, String> {
let value: toml::Value = toml::from_str(toml_str)
.map_err(|e| format!("TOML parse error: {}", e))?;
let mut bundle = LocaleBundle::new(locale);
if let toml::Value::Table(table) = value {
load_table(&mut bundle, &table, "");
}
Ok(bundle)
}
fn load_table(bundle: &mut LocaleBundle, table: &toml::value::Table, prefix: &str) {
for (key, value) in table {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
match value {
toml::Value::String(s) => {
bundle.insert(full_key, s.clone());
}
toml::Value::Table(inner) => {
// Check if it's a plural table (has "one", "other", etc.)
let is_plural = inner.contains_key("one")
|| inner.contains_key("other")
|| inner.contains_key("zero")
|| inner.contains_key("few")
|| inner.contains_key("many");
if is_plural {
let mut forms = HashMap::new();
for (form, form_val) in inner {
if let toml::Value::String(s) = form_val {
forms.insert(form.clone(), s.clone());
}
}
bundle.insert_plural(full_key, forms);
} else {
// Nested namespace
load_table(bundle, inner, &full_key);
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn en_bundle() -> LocaleBundle {
let mut b = LocaleBundle::new(Locale::en_us());
b.insert("profile.follow", "Follow");
b.insert("profile.bio", "Bio");
let mut forms = HashMap::new();
forms.insert("one".to_string(), "{n} Follower".to_string());
forms.insert("other".to_string(), "{n} Followers".to_string());
b.insert_plural("profile.followers", forms);
b
}
#[test]
fn get_raw_simple() {
let b = en_bundle();
assert_eq!(b.get_raw("profile.follow"), Some("Follow"));
}
#[test]
fn get_raw_missing() {
let b = en_bundle();
assert_eq!(b.get_raw("nonexistent.key"), None);
}
#[test]
fn translate_simple() {
let b = en_bundle();
let vars = HashMap::new();
assert_eq!(
b.translate("profile.follow", &vars),
Some("Follow".to_string())
);
}
#[test]
fn translate_plural_one() {
let b = en_bundle();
let vars = HashMap::new();
assert_eq!(
b.translate_plural("profile.followers", 1, &vars),
Some("1 Follower".to_string())
);
}
#[test]
fn translate_plural_many() {
let b = en_bundle();
let vars = HashMap::new();
assert_eq!(
b.translate_plural("profile.followers", 42, &vars),
Some("42 Followers".to_string())
);
}
#[test]
fn interpolation_replaces_placeholder() {
let mut b = LocaleBundle::new(Locale::en_us());
b.insert("greeting", "Hello, {name}!");
let mut vars = HashMap::new();
vars.insert("name", "Alice".to_string());
assert_eq!(
b.translate("greeting", &vars),
Some("Hello, Alice!".to_string())
);
}
#[test]
fn bundle_len() {
let b = en_bundle();
assert_eq!(b.len(), 3);
}
#[test]
fn load_toml_simple() {
let toml = r#"
[profile]
follow = "Follow"
bio = "Bio"
"#;
let bundle = load_toml(Locale::en_us(), toml).unwrap();
assert_eq!(bundle.get_raw("profile.follow"), Some("Follow"));
assert_eq!(bundle.get_raw("profile.bio"), Some("Bio"));
}
#[test]
fn load_toml_plural() {
let toml = r#"
[profile]
followers = { one = "{n} Follower", other = "{n} Followers" }
"#;
let bundle = load_toml(Locale::en_us(), toml).unwrap();
let vars = HashMap::new();
assert_eq!(
bundle.translate_plural("profile.followers", 1, &vars),
Some("1 Follower".to_string())
);
}
#[test]
fn load_toml_invalid() {
let result = load_toml(Locale::en_us(), "not valid toml %%%");
assert!(result.is_err());
}
}
-221
View File
@@ -1,221 +0,0 @@
/// Number, date, and currency formatting per locale.
///
/// Formatting is locale-sensitive: number grouping, decimal separators,
/// currency symbol placement, and date ordering all vary by locale.
/// Use these formatters rather than hardcoding formatting logic.
use crate::locale::Locale;
/// Format a number with locale-appropriate grouping and decimals.
///
/// Examples:
/// - en-US: 1,234,567.89
/// - de-DE: 1.234.567,89
/// - fr-FR: 1 234 567,89
pub fn format_number(value: f64, locale: &Locale, decimal_places: usize) -> String {
let (group_sep, decimal_sep) = separators_for_locale(locale);
let rounded = round_to(value, decimal_places);
let is_negative = rounded < 0.0;
let abs_value = rounded.abs();
let int_part = abs_value.trunc() as u64;
let frac_part = ((abs_value.fract() * 10f64.powi(decimal_places as i32)).round()) as u64;
let int_str = format_integer_with_grouping(int_part, group_sep);
let result = if decimal_places > 0 {
format!(
"{}{}{}",
int_str,
decimal_sep,
format!("{:0>width$}", frac_part, width = decimal_places)
)
} else {
int_str
};
if is_negative {
format!("-{}", result)
} else {
result
}
}
/// Format a currency value with locale-appropriate symbol and placement.
///
/// Examples:
/// - en-US / USD: $1,234.56
/// - de-DE / EUR: 1.234,56 €
/// - ja / JPY: ¥1,235
pub fn format_currency(value: f64, locale: &Locale, currency_code: &str) -> String {
let (symbol, prefix, decimals) = currency_info(currency_code);
let formatted = format_number(value, locale, decimals);
if prefix {
format!("{}{}", symbol, formatted)
} else {
format!("{} {}", formatted, symbol)
}
}
/// Format an integer with locale-appropriate grouping separators.
pub fn format_integer(value: i64, locale: &Locale) -> String {
let (group_sep, _) = separators_for_locale(locale);
let is_negative = value < 0;
let abs_val = value.unsigned_abs();
let grouped = format_integer_with_grouping(abs_val, group_sep);
if is_negative {
format!("-{}", grouped)
} else {
grouped
}
}
/// Format a percentage (0.85 → "85%", locale-aware).
pub fn format_percent(value: f64, locale: &Locale, decimal_places: usize) -> String {
let pct = value * 100.0;
let (_, decimal_sep) = separators_for_locale(locale);
let int_part = pct.trunc() as u64;
let frac = ((pct.fract() * 10f64.powi(decimal_places as i32)).round()) as u64;
if decimal_places > 0 {
format!(
"{}{}{}%",
int_part,
decimal_sep,
format!("{:0>width$}", frac, width = decimal_places)
)
} else {
format!("{}%", int_part)
}
}
// --- Internal helpers ---
fn separators_for_locale(locale: &Locale) -> (char, char) {
match locale.language.as_str() {
// Comma grouping, period decimal (en-US style)
"en" | "ja" | "ko" | "zh" | "th" => (',', '.'),
// Period grouping, comma decimal (European style)
"de" | "nl" | "it" | "pt" | "es" | "tr" | "pl" | "ru" | "uk" | "el" => ('.', ','),
// Thin space grouping, comma decimal (French style)
"fr" | "sv" | "no" | "nb" | "da" | "fi" => ('\u{202F}', ','),
// Default: comma grouping, period decimal
_ => (',', '.'),
}
}
fn format_integer_with_grouping(value: u64, sep: char) -> String {
let s = value.to_string();
if s.len() <= 3 {
return s;
}
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
for (i, &ch) in chars.iter().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
result.push(sep);
}
result.push(ch);
}
result
}
fn round_to(value: f64, places: usize) -> f64 {
let factor = 10f64.powi(places as i32);
(value * factor).round() / factor
}
fn currency_info(code: &str) -> (&'static str, bool, usize) {
// (symbol, prefix, decimal_places)
match code.to_uppercase().as_str() {
"USD" => ("$", true, 2),
"EUR" => ("", false, 2),
"GBP" => ("£", true, 2),
"JPY" => ("¥", true, 0),
"CNY" => ("¥", true, 2),
"KRW" => ("", true, 0),
"INR" => ("", true, 2),
"CHF" => ("CHF", true, 2),
"CAD" => ("CA$", true, 2),
"AUD" => ("A$", true, 2),
"BRL" => ("R$", true, 2),
"MXN" => ("MX$", true, 2),
"RUB" => ("", false, 2),
"SEK" => ("kr", false, 2),
"NOK" => ("kr", false, 2),
"DKK" => ("kr", false, 2),
"PLN" => ("", false, 2),
"TRY" => ("", true, 2),
"SAR" => ("", false, 2),
"AED" => ("د.إ", false, 2),
_ => ("¤", true, 2), // generic currency sign for unknown codes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_number_en_us() {
let locale = Locale::en_us();
assert_eq!(format_number(1234567.89, &locale, 2), "1,234,567.89");
}
#[test]
fn format_number_de() {
let locale = Locale::new("de-DE");
assert_eq!(format_number(1234.56, &locale, 2), "1.234,56");
}
#[test]
fn format_number_no_decimals() {
let locale = Locale::en_us();
assert_eq!(format_number(42.0, &locale, 0), "42");
}
#[test]
fn format_number_negative() {
let locale = Locale::en_us();
assert_eq!(format_number(-1000.0, &locale, 2), "-1,000.00");
}
#[test]
fn format_currency_usd() {
let locale = Locale::en_us();
assert_eq!(format_currency(1234.56, &locale, "USD"), "$1,234.56");
}
#[test]
fn format_currency_jpy_no_decimals() {
let locale = Locale::ja();
assert_eq!(format_currency(1234.0, &locale, "JPY"), "¥1,234");
}
#[test]
fn format_integer_groups() {
let locale = Locale::en_us();
assert_eq!(format_integer(1000000, &locale), "1,000,000");
}
#[test]
fn format_integer_small() {
let locale = Locale::en_us();
assert_eq!(format_integer(42, &locale), "42");
}
#[test]
fn format_percent_whole() {
let locale = Locale::en_us();
assert_eq!(format_percent(0.85, &locale, 0), "85%");
}
#[test]
fn format_percent_with_decimal() {
let locale = Locale::en_us();
assert_eq!(format_percent(0.856, &locale, 1), "85.6%");
}
}
-44
View File
@@ -1,44 +0,0 @@
//! el-i18n — Localization for el-ui.
//!
//! RTL-aware, plural forms, CLDR-based number/currency formatting.
//!
//! ## Quick start
//!
//! ```
//! use el_i18n::prelude::*;
//! use std::collections::HashMap;
//!
//! // Build a bundle
//! let mut bundle = LocaleBundle::new(Locale::en_us());
//! bundle.insert("profile.follow", "Follow");
//! let mut forms = HashMap::new();
//! forms.insert("one".to_string(), "{n} Follower".to_string());
//! forms.insert("other".to_string(), "{n} Followers".to_string());
//! bundle.insert_plural("profile.followers", forms);
//!
//! // Create a context
//! let ctx = LocaleContext::new(Locale::en_us(), bundle);
//!
//! // Translate
//! assert_eq!(ctx.t("profile.follow"), "Follow");
//! assert_eq!(ctx.t_plural("profile.followers", 1), "1 Follower");
//! assert_eq!(ctx.t_plural("profile.followers", 42), "42 Followers");
//! ```
#![deny(warnings)]
pub mod bundle;
pub mod format;
pub mod locale;
pub mod plural;
pub mod t;
pub mod prelude {
pub use crate::bundle::{load_toml, LocaleBundle, TranslationValue};
pub use crate::format::{format_currency, format_integer, format_number, format_percent};
pub use crate::locale::{Locale, TextDirection};
pub use crate::plural::{plural_form, PluralForm};
pub use crate::t::LocaleContext;
}
pub use prelude::*;
-182
View File
@@ -1,182 +0,0 @@
/// Locale — language + optional region + directionality.
///
/// Locale identifies both the language for translation lookup and the
/// region for number/date/currency formatting.
/// Text direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
}
/// A locale identifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Locale {
/// BCP 47 language tag (e.g. "en", "ar", "zh-Hant").
pub language: String,
/// Optional region (e.g. "US", "GB", "TW").
pub region: Option<String>,
}
impl Locale {
/// Create from a BCP 47 tag like "en-US" or "ar".
pub fn new(tag: impl Into<String>) -> Self {
let tag = tag.into();
if let Some(idx) = tag.find('-') {
let (lang, rest) = tag.split_at(idx);
let region = rest.trim_start_matches('-');
Self {
language: lang.to_lowercase(),
region: if region.is_empty() {
None
} else {
Some(region.to_uppercase())
},
}
} else {
Self {
language: tag.to_lowercase(),
region: None,
}
}
}
/// The full BCP 47 tag (e.g. "en-US").
pub fn tag(&self) -> String {
match &self.region {
Some(r) => format!("{}-{}", self.language, r),
None => self.language.clone(),
}
}
/// The text direction for this locale.
pub fn direction(&self) -> TextDirection {
if self.is_rtl() {
TextDirection::RightToLeft
} else {
TextDirection::LeftToRight
}
}
/// Whether this locale uses right-to-left script.
pub fn is_rtl(&self) -> bool {
// RTL language codes per Unicode CLDR
matches!(
self.language.as_str(),
"ar" // Arabic
| "he" | "iw" // Hebrew
| "fa" | "per" // Persian/Farsi
| "ur" // Urdu
| "ps" // Pashto
| "ug" // Uyghur
| "yi" // Yiddish
| "dv" // Maldivian/Dhivehi
| "ku" // Kurdish (some scripts)
| "sd" // Sindhi
)
}
/// English (US).
pub fn en_us() -> Self {
Self::new("en-US")
}
/// English (GB).
pub fn en_gb() -> Self {
Self::new("en-GB")
}
/// Arabic (a common RTL locale).
pub fn ar() -> Self {
Self::new("ar")
}
/// Arabic (Saudi Arabia).
pub fn ar_sa() -> Self {
Self::new("ar-SA")
}
/// Spanish (Spain).
pub fn es_es() -> Self {
Self::new("es-ES")
}
/// French (France).
pub fn fr_fr() -> Self {
Self::new("fr-FR")
}
/// Japanese.
pub fn ja() -> Self {
Self::new("ja")
}
/// Chinese (Traditional).
pub fn zh_hant() -> Self {
Self::new("zh-Hant")
}
}
impl std::fmt::Display for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.tag())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locale_parse_with_region() {
let l = Locale::new("en-US");
assert_eq!(l.language, "en");
assert_eq!(l.region, Some("US".to_string()));
}
#[test]
fn locale_parse_without_region() {
let l = Locale::new("ja");
assert_eq!(l.language, "ja");
assert_eq!(l.region, None);
}
#[test]
fn locale_tag_roundtrip() {
let l = Locale::new("fr-FR");
assert_eq!(l.tag(), "fr-FR");
}
#[test]
fn arabic_is_rtl() {
assert!(Locale::new("ar").is_rtl());
assert!(Locale::new("ar-SA").is_rtl());
}
#[test]
fn hebrew_is_rtl() {
assert!(Locale::new("he").is_rtl());
}
#[test]
fn persian_is_rtl() {
assert!(Locale::new("fa").is_rtl());
}
#[test]
fn english_is_ltr() {
assert!(!Locale::new("en").is_rtl());
assert_eq!(Locale::new("en-US").direction(), TextDirection::LeftToRight);
}
#[test]
fn rtl_direction() {
assert_eq!(Locale::ar().direction(), TextDirection::RightToLeft);
}
#[test]
fn locale_display() {
assert_eq!(format!("{}", Locale::en_us()), "en-US");
}
}
-189
View File
@@ -1,189 +0,0 @@
/// Plural forms — handles language-specific plurality rules.
///
/// Different languages have very different plural forms. English has two
/// (one / other). Russian has four. Arabic has six. This module maps counts
/// to the correct form for a given locale.
/// Named plural categories per Unicode CLDR.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PluralForm {
/// Exactly zero (some languages have a special zero form).
Zero,
/// Exactly one.
One,
/// Small numbers (e.g. 24 in Slavic languages).
Two,
/// Few (language-specific).
Few,
/// Many (language-specific).
Many,
/// The catch-all / default.
Other,
}
/// Determine the plural form for a count in a given language.
///
/// Rules are a simplified implementation of CLDR plural rules for the most
/// common languages. The full CLDR spec covers hundreds of languages; these
/// cover the major cases.
pub fn plural_form(language: &str, count: i64) -> PluralForm {
let n = count.unsigned_abs(); // absolute value for matching
match language {
// English and most Western European languages: one / other
"en" | "de" | "nl" | "sv" | "da" | "no" | "nb" | "nn" | "fi"
| "et" | "hu" | "tr" | "pt" | "it" | "es" | "ca" | "el" | "id"
| "ms" | "th" | "zh" | "ja" | "ko" | "vi" | "ur" => {
if n == 1 { PluralForm::One } else { PluralForm::Other }
}
// French: one for 0 and 1, other for rest
"fr" => {
if n <= 1 { PluralForm::One } else { PluralForm::Other }
}
// Russian, Ukrainian, Belarusian: complex Slavic rules
"ru" | "uk" | "be" => {
let n10 = n % 10;
let n100 = n % 100;
if n10 == 1 && n100 != 11 {
PluralForm::One
} else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
PluralForm::Few
} else {
PluralForm::Many
}
}
// Polish: similar Slavic rules
"pl" => {
let n10 = n % 10;
let n100 = n % 100;
if n == 1 {
PluralForm::One
} else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
PluralForm::Few
} else {
PluralForm::Many
}
}
// Czech, Slovak
"cs" | "sk" => {
if n == 1 {
PluralForm::One
} else if (2..=4).contains(&n) {
PluralForm::Few
} else {
PluralForm::Other
}
}
// Arabic: 6 forms
"ar" => {
let n100 = n % 100;
if n == 0 {
PluralForm::Zero
} else if n == 1 {
PluralForm::One
} else if n == 2 {
PluralForm::Two
} else if (3..=10).contains(&n100) {
PluralForm::Few
} else if (11..=99).contains(&n100) {
PluralForm::Many
} else {
PluralForm::Other
}
}
// Hebrew
"he" | "iw" => {
if n == 1 {
PluralForm::One
} else if n == 2 {
PluralForm::Two
} else if n >= 11 && n % 10 == 0 {
PluralForm::Many
} else {
PluralForm::Other
}
}
// Default: one / other
_ => {
if n == 1 { PluralForm::One } else { PluralForm::Other }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn english_one() {
assert_eq!(plural_form("en", 1), PluralForm::One);
}
#[test]
fn english_other() {
assert_eq!(plural_form("en", 0), PluralForm::Other);
assert_eq!(plural_form("en", 2), PluralForm::Other);
assert_eq!(plural_form("en", 100), PluralForm::Other);
}
#[test]
fn french_zero_is_one() {
assert_eq!(plural_form("fr", 0), PluralForm::One);
assert_eq!(plural_form("fr", 1), PluralForm::One);
assert_eq!(plural_form("fr", 2), PluralForm::Other);
}
#[test]
fn russian_one() {
assert_eq!(plural_form("ru", 1), PluralForm::One);
assert_eq!(plural_form("ru", 21), PluralForm::One);
assert_eq!(plural_form("ru", 101), PluralForm::One);
}
#[test]
fn russian_few() {
assert_eq!(plural_form("ru", 2), PluralForm::Few);
assert_eq!(plural_form("ru", 3), PluralForm::Few);
assert_eq!(plural_form("ru", 22), PluralForm::Few);
}
#[test]
fn russian_many() {
assert_eq!(plural_form("ru", 5), PluralForm::Many);
assert_eq!(plural_form("ru", 11), PluralForm::Many);
assert_eq!(plural_form("ru", 20), PluralForm::Many);
}
#[test]
fn arabic_zero() {
assert_eq!(plural_form("ar", 0), PluralForm::Zero);
}
#[test]
fn arabic_two() {
assert_eq!(plural_form("ar", 2), PluralForm::Two);
}
#[test]
fn arabic_few() {
assert_eq!(plural_form("ar", 5), PluralForm::Few);
assert_eq!(plural_form("ar", 10), PluralForm::Few);
}
#[test]
fn arabic_many() {
assert_eq!(plural_form("ar", 15), PluralForm::Many);
}
#[test]
fn hebrew_two() {
assert_eq!(plural_form("he", 2), PluralForm::Two);
}
}
-209
View File
@@ -1,209 +0,0 @@
/// The t() translation function and LocaleContext.
///
/// Components call `ctx.t("key")` or `ctx.t_plural("key", count)`.
/// The context flows down from the experience root and carries the active bundle.
use std::collections::HashMap;
use crate::bundle::LocaleBundle;
use crate::locale::Locale;
/// The active localization context.
///
/// Holds the current locale and its translation bundle. Passed down
/// through the component tree. When the locale changes, the context
/// is updated and all components that used it re-render.
#[derive(Debug, Clone)]
pub struct LocaleContext {
pub locale: Locale,
bundle: LocaleBundle,
/// Fallback bundle (typically English) used when a key is missing.
fallback: Option<LocaleBundle>,
}
impl LocaleContext {
/// Create a context with a locale and its bundle.
pub fn new(locale: Locale, bundle: LocaleBundle) -> Self {
Self {
locale,
bundle,
fallback: None,
}
}
/// Set a fallback bundle for missing keys.
pub fn with_fallback(mut self, fallback: LocaleBundle) -> Self {
self.fallback = Some(fallback);
self
}
/// Translate a key with optional variable interpolation.
///
/// Returns the key itself if not found (never panics).
pub fn t(&self, key: &str) -> String {
self.t_vars(key, &HashMap::new())
}
/// Translate a key with variables.
pub fn t_vars(&self, key: &str, vars: &HashMap<&str, String>) -> String {
// Try primary bundle
if let Some(s) = self.bundle.translate(key, vars) {
return s;
}
// Try fallback bundle
if let Some(ref fb) = self.fallback {
if let Some(s) = fb.translate(key, vars) {
return s;
}
}
// Return the key itself — visible but never panics
key.to_string()
}
/// Translate a plural key with a count.
pub fn t_plural(&self, key: &str, count: i64) -> String {
self.t_plural_vars(key, count, &HashMap::new())
}
/// Translate a plural key with a count and extra variables.
pub fn t_plural_vars(
&self,
key: &str,
count: i64,
vars: &HashMap<&str, String>,
) -> String {
if let Some(s) = self.bundle.translate_plural(key, count, vars) {
return s;
}
if let Some(ref fb) = self.fallback {
if let Some(s) = fb.translate_plural(key, count, vars) {
return s;
}
}
key.to_string()
}
/// The current locale tag (e.g. "en-US").
pub fn locale_tag(&self) -> String {
self.locale.tag()
}
/// Whether the current locale is RTL.
pub fn is_rtl(&self) -> bool {
self.locale.is_rtl()
}
}
/// Convenience macro for translation calls.
///
/// ```ignore
/// // Simple
/// let text = t!(ctx, "profile.follow");
///
/// // With variables
/// let text = t!(ctx, "greeting", name => "Alice");
///
/// // Plural
/// let text = t_n!(ctx, "profile.followers", count);
/// ```
#[macro_export]
macro_rules! t {
($ctx:expr, $key:expr) => {
$ctx.t($key)
};
($ctx:expr, $key:expr, $($var:ident => $val:expr),+) => {{
let mut vars = std::collections::HashMap::new();
$(vars.insert(stringify!($var), $val.to_string());)+
$ctx.t_vars($key, &vars)
}};
}
#[macro_export]
macro_rules! t_n {
($ctx:expr, $key:expr, $count:expr) => {
$ctx.t_plural($key, $count as i64)
};
($ctx:expr, $key:expr, $count:expr, $($var:ident => $val:expr),+) => {{
let mut vars = std::collections::HashMap::new();
$(vars.insert(stringify!($var), $val.to_string());)+
$ctx.t_plural_vars($key, $count as i64, &vars)
}};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bundle::LocaleBundle;
fn make_ctx() -> LocaleContext {
let mut bundle = LocaleBundle::new(Locale::en_us());
bundle.insert("profile.follow", "Follow");
bundle.insert("greeting", "Hello, {name}!");
let mut forms = HashMap::new();
forms.insert("one".to_string(), "{n} Follower".to_string());
forms.insert("other".to_string(), "{n} Followers".to_string());
bundle.insert_plural("profile.followers", forms);
LocaleContext::new(Locale::en_us(), bundle)
}
#[test]
fn t_simple() {
let ctx = make_ctx();
assert_eq!(ctx.t("profile.follow"), "Follow");
}
#[test]
fn t_missing_returns_key() {
let ctx = make_ctx();
assert_eq!(ctx.t("no.such.key"), "no.such.key");
}
#[test]
fn t_vars_interpolation() {
let ctx = make_ctx();
let mut vars = HashMap::new();
vars.insert("name", "Bob".to_string());
assert_eq!(ctx.t_vars("greeting", &vars), "Hello, Bob!");
}
#[test]
fn t_plural_one() {
let ctx = make_ctx();
assert_eq!(ctx.t_plural("profile.followers", 1), "1 Follower");
}
#[test]
fn t_plural_many() {
let ctx = make_ctx();
assert_eq!(ctx.t_plural("profile.followers", 42), "42 Followers");
}
#[test]
fn is_rtl_english() {
let ctx = make_ctx();
assert!(!ctx.is_rtl());
}
#[test]
fn is_rtl_arabic() {
let bundle = LocaleBundle::new(Locale::ar());
let ctx = LocaleContext::new(Locale::ar(), bundle);
assert!(ctx.is_rtl());
}
#[test]
fn fallback_bundle_used() {
let primary_bundle = LocaleBundle::new(Locale::new("fr"));
// Key not in French bundle
let mut fallback = LocaleBundle::new(Locale::en_us());
fallback.insert("app.name", "My App");
let ctx = LocaleContext::new(Locale::new("fr"), primary_bundle)
.with_fallback(fallback);
assert_eq!(ctx.t("app.name"), "My App");
}
#[test]
fn locale_tag() {
let ctx = make_ctx();
assert_eq!(ctx.locale_tag(), "en-US");
}
}