add el-style, el-layout, el-i18n, el-config, el-secrets: responsive by default, theme-driven, zero breakpoints
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user