Files
el/ui/vessels/el-i18n/src/main.el

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"))