209 lines
7.4 KiB
EmacsLisp
209 lines
7.4 KiB
EmacsLisp
// el-i18n — Localization for el-ui.
|
|
//
|
|
// Two-letter language tag + optional region: en, en-US, ar-EG, zh-Hant.
|
|
// Bundles map keys to either a plain string or a plural-form map.
|
|
// `t(key)` and `t_plural(key, count)` are the surface API.
|
|
|
|
// ── Locale ───────────────────────────────────────────────────────────────────
|
|
|
|
let DIR_LTR: String = "ltr"
|
|
let DIR_RTL: String = "rtl"
|
|
|
|
type Locale {
|
|
language: String // "en"
|
|
region: String // "US" (may be empty)
|
|
direction: String // "ltr" | "rtl"
|
|
}
|
|
|
|
fn locale_new(language: String, region: String) -> Locale {
|
|
let dir: String = "ltr"
|
|
if str_eq(language, "ar") { let dir = "rtl" }
|
|
if str_eq(language, "he") { let dir = "rtl" }
|
|
if str_eq(language, "fa") { let dir = "rtl" }
|
|
if str_eq(language, "ur") { let dir = "rtl" }
|
|
{ "language": language, "region": region, "direction": dir }
|
|
}
|
|
|
|
fn locale_en_us() -> Locale { locale_new("en", "US") }
|
|
fn locale_es_es() -> Locale { locale_new("es", "ES") }
|
|
fn locale_zh_cn() -> Locale { locale_new("zh", "CN") }
|
|
fn locale_ar_eg() -> Locale { locale_new("ar", "EG") }
|
|
fn locale_ja_jp() -> Locale { locale_new("ja", "JP") }
|
|
|
|
fn locale_tag(loc: Locale) -> String {
|
|
if str_eq(loc.region, "") { return loc.language }
|
|
loc.language + "-" + loc.region
|
|
}
|
|
|
|
fn is_rtl(loc: Locale) -> Bool {
|
|
str_eq(loc.direction, "rtl")
|
|
}
|
|
|
|
// ── Plural forms (CLDR cardinal categories) ──────────────────────────────────
|
|
//
|
|
// Categories: zero, one, two, few, many, other.
|
|
// Most languages only use one + other; Arabic uses all six.
|
|
|
|
let PLURAL_ZERO: String = "zero"
|
|
let PLURAL_ONE: String = "one"
|
|
let PLURAL_TWO: String = "two"
|
|
let PLURAL_FEW: String = "few"
|
|
let PLURAL_MANY: String = "many"
|
|
let PLURAL_OTHER: String = "other"
|
|
|
|
fn plural_form(loc: Locale, n: Int) -> String {
|
|
if str_eq(loc.language, "ar") { return plural_form_arabic(n) }
|
|
if str_eq(loc.language, "ru") { return plural_form_russian(n) }
|
|
if str_eq(loc.language, "pl") { return plural_form_polish(n) }
|
|
if str_eq(loc.language, "ja") { return PLURAL_OTHER }
|
|
if str_eq(loc.language, "zh") { return PLURAL_OTHER }
|
|
if str_eq(loc.language, "ko") { return PLURAL_OTHER }
|
|
// Default English-like rule
|
|
if n == 1 { return PLURAL_ONE }
|
|
PLURAL_OTHER
|
|
}
|
|
|
|
fn plural_form_arabic(n: Int) -> String {
|
|
if n == 0 { return PLURAL_ZERO }
|
|
if n == 1 { return PLURAL_ONE }
|
|
if n == 2 { return PLURAL_TWO }
|
|
let mod100: Int = n - ((n / 100) * 100)
|
|
if mod100 >= 3 {
|
|
if mod100 <= 10 { return PLURAL_FEW }
|
|
}
|
|
if mod100 >= 11 {
|
|
if mod100 <= 99 { return PLURAL_MANY }
|
|
}
|
|
PLURAL_OTHER
|
|
}
|
|
|
|
fn plural_form_russian(n: Int) -> String {
|
|
let mod10: Int = n - ((n / 10) * 10)
|
|
let mod100: Int = n - ((n / 100) * 100)
|
|
if mod10 == 1 {
|
|
if mod100 == 11 { return PLURAL_MANY }
|
|
return PLURAL_ONE
|
|
}
|
|
if mod10 >= 2 {
|
|
if mod10 <= 4 {
|
|
if mod100 >= 12 {
|
|
if mod100 <= 14 { return PLURAL_MANY }
|
|
}
|
|
return PLURAL_FEW
|
|
}
|
|
}
|
|
PLURAL_MANY
|
|
}
|
|
|
|
fn plural_form_polish(n: Int) -> String {
|
|
if n == 1 { return PLURAL_ONE }
|
|
let mod10: Int = n - ((n / 10) * 10)
|
|
let mod100: Int = n - ((n / 100) * 100)
|
|
if mod10 >= 2 {
|
|
if mod10 <= 4 {
|
|
if mod100 >= 12 {
|
|
if mod100 <= 14 { return PLURAL_MANY }
|
|
}
|
|
return PLURAL_FEW
|
|
}
|
|
}
|
|
PLURAL_MANY
|
|
}
|
|
|
|
// ── Translation bundle ──────────────────────────────────────────────────────
|
|
//
|
|
// Stored as a JSON map: key -> value | { one: "...", other: "..." }
|
|
// `bundle_load_toml` parses a TOML file at load time (planned runtime fn).
|
|
|
|
fn bundle_new() -> String {
|
|
"{}"
|
|
}
|
|
|
|
fn bundle_insert(bundle: String, key: String, value: String) -> String {
|
|
json_set(bundle, key, "\"" + value + "\"")
|
|
}
|
|
|
|
fn bundle_insert_plural(bundle: String, key: String, plural_map_json: String) -> String {
|
|
json_set(bundle, key, plural_map_json)
|
|
}
|
|
|
|
fn bundle_load_toml(path: String) -> String {
|
|
let raw: String = fs_read(path)
|
|
toml_to_json(raw)
|
|
}
|
|
|
|
// ── LocaleContext + t/t_plural ──────────────────────────────────────────────
|
|
|
|
type LocaleContext {
|
|
locale: Locale
|
|
bundle: String // JSON
|
|
fallback_bundle: String
|
|
}
|
|
|
|
fn locale_context_new(loc: Locale, bundle: String) -> LocaleContext {
|
|
{ "locale": loc, "bundle": bundle, "fallback_bundle": "{}" }
|
|
}
|
|
|
|
fn t(ctx: LocaleContext, key: String) -> String {
|
|
let v: String = json_get(ctx.bundle, key)
|
|
if str_eq(v, "") { let v = json_get(ctx.fallback_bundle, key) }
|
|
if str_eq(v, "") { return key }
|
|
v
|
|
}
|
|
|
|
fn t_plural(ctx: LocaleContext, key: String, n: Int) -> String {
|
|
let entry: String = json_get(ctx.bundle, key)
|
|
if str_eq(entry, "") { return key }
|
|
let form: String = plural_form(ctx.locale, n)
|
|
let template: String = json_get(entry, form)
|
|
if str_eq(template, "") { let template = json_get(entry, PLURAL_OTHER) }
|
|
if str_eq(template, "") { return key }
|
|
str_replace(template, "{n}", int_to_str(n))
|
|
}
|
|
|
|
// ── Number / currency formatting ─────────────────────────────────────────────
|
|
|
|
fn format_integer(loc: Locale, n: Int) -> String {
|
|
// Group thousands by the locale's separator. Stub: en uses ',', most EU uses '.'.
|
|
let sep: String = ","
|
|
if str_eq(loc.language, "es") { let sep = "." }
|
|
if str_eq(loc.language, "de") { let sep = "." }
|
|
if str_eq(loc.language, "fr") { let sep = " " }
|
|
int_with_separator(n, sep)
|
|
}
|
|
|
|
fn format_number(loc: Locale, n: Int, fraction_digits: Int) -> String {
|
|
let dec_sep: String = "."
|
|
if str_eq(loc.language, "es") { let dec_sep = "," }
|
|
if str_eq(loc.language, "de") { let dec_sep = "," }
|
|
if str_eq(loc.language, "fr") { let dec_sep = "," }
|
|
format_integer(loc, n) + dec_sep + repeat_str("0", fraction_digits)
|
|
}
|
|
|
|
fn format_percent(loc: Locale, value_x100: Int) -> String {
|
|
let body: String = int_to_str(value_x100 / 100) + "."
|
|
+ int_to_str(value_x100 - ((value_x100 / 100) * 100))
|
|
if str_eq(loc.language, "fr") { return body + " %" }
|
|
body + "%"
|
|
}
|
|
|
|
fn format_currency(loc: Locale, amount_minor: Int, iso: String) -> String {
|
|
// amount_minor is in the smallest unit (cents). Stub formatting only.
|
|
let major: Int = amount_minor / 100
|
|
let minor: Int = amount_minor - (major * 100)
|
|
let body: String = int_to_str(major) + "." + int_to_str(minor)
|
|
if str_eq(iso, "USD") { return "$" + body }
|
|
if str_eq(iso, "EUR") { return body + " €" }
|
|
if str_eq(iso, "GBP") { return "£" + body }
|
|
if str_eq(iso, "JPY") { return "¥" + int_to_str(amount_minor) }
|
|
body + " " + iso
|
|
}
|
|
|
|
// ── Entry — smoke test ──────────────────────────────────────────────────────
|
|
|
|
let loc: Locale = locale_en_us()
|
|
let bundle: String = bundle_new()
|
|
let bundle = bundle_insert(bundle, "profile.follow", "Follow")
|
|
let ctx: LocaleContext = locale_context_new(loc, bundle)
|
|
println("[el-i18n] " + t(ctx, "profile.follow"))
|