feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// 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"))
|
||||
Reference in New Issue
Block a user