///|
fn is_ascii_letter(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
}
///|
fn is_ascii_number(ch : Char) -> Bool {
ch >= '0' && ch <= '9'
}
///|
fn is_ascii_alphanumeric(ch : Char) -> Bool {
is_ascii_letter(ch) || is_ascii_number(ch)
}
///|
fn all_ascii_letters(value : String) -> Bool {
if value == "" {
return false
}
for ch in value {
if !is_ascii_letter(ch) {
return false
}
}
true
}
///|
fn all_ascii_numbers(value : String) -> Bool {
if value == "" {
return false
}
for ch in value {
if !is_ascii_number(ch) {
return false
}
}
true
}
///|
fn all_ascii_alphanumeric(value : String) -> Bool {
if value == "" {
return false
}
for ch in value {
if !is_ascii_alphanumeric(ch) {
return false
}
}
true
}
///|
fn valid_language_subtag(value : String) -> Bool {
value.length() >= 2 && value.length() <= 8 && all_ascii_letters(value)
}
///|
fn valid_script_subtag(value : String) -> Bool {
value.length() == 4 && all_ascii_letters(value)
}
///|
fn valid_region_subtag(value : String) -> Bool {
(value.length() == 2 && all_ascii_letters(value)) ||
(value.length() == 3 && all_ascii_numbers(value))
}
///|
fn valid_variant_subtag(value : String) -> Bool {
value.length() >= 4 && value.length() <= 8 && all_ascii_alphanumeric(value)
}
///|
fn title_case_script(value : String) -> String {
let lower = value.to_lower()
let output = StringBuilder::new()
for index, ch in lower {
if index == 0 {
output.write_char(ch.to_ascii_uppercase())
} else {
output.write_char(ch)
}
}
output.to_string()
}
///|
fn owned_locale_parts(value : String) -> Array[String] {
let normalized = value.replace_all(old="_", new="-")
let parts : Array[String] = []
for part in normalized.split("-") {
parts.push("\{part}")
}
parts
}
///|
fn join_locale_parts(parts : Array[String]) -> String {
parts.join("-")
}
///|
fn canonical_parts(
language : String,
script : String?,
region : String?,
variants : Array[String],
) -> Array[String] {
let parts : Array[String] = [language]
match script {
Some(value) => parts.push(value)
None => ()
}
match region {
Some(value) => parts.push(value)
None => ()
}
for variant in variants {
parts.push(variant)
}
parts
}
///|
/// Parse and canonicalize a compact BCP-47-style locale identifier.
pub fn LocaleId::parse(value : String) -> LocaleId raise MessageError {
let parts = owned_locale_parts(value)
if parts.length() == 0 || parts[0] == "" {
raise InvalidLocale("Locale identifier cannot be empty.")
}
let language = parts[0].to_lower()
if !valid_language_subtag(language) {
raise InvalidLocale("Invalid language subtag '\{parts[0]}'.")
}
let mut index = 1
let mut script : String? = None
let mut region : String? = None
let variants : Array[String] = []
if index < parts.length() && valid_script_subtag(parts[index]) {
script = Some(title_case_script(parts[index]))
index += 1
}
if index < parts.length() && valid_region_subtag(parts[index]) {
region = Some(parts[index].to_upper())
index += 1
}
while index < parts.length() {
let variant = parts[index]
if !valid_variant_subtag(variant) {
raise InvalidLocale("Invalid locale variant '\{variant}'.")
}
variants.push(variant.to_lower())
index += 1
}
let canonical = join_locale_parts(
canonical_parts(language, script, region, variants),
)
{ original: value, canonical, language, script, region, variants }
}
///|
/// Canonicalize a locale identifier.
pub fn canonicalize_locale(value : String) -> String raise MessageError {
LocaleId::parse(value).canonical
}
///|
fn canonicalize_locale_lossy(value : String) -> String {
canonicalize_locale(value) catch {
_ => value.replace_all(old="_", new="-")
}
}
///|
pub fn LocaleId::original(self : LocaleId) -> String {
self.original
}
///|
pub fn LocaleId::canonical(self : LocaleId) -> String {
self.canonical
}
///|
pub fn LocaleId::language(self : LocaleId) -> String {
self.language
}
///|
pub fn LocaleId::script(self : LocaleId) -> String? {
self.script
}
///|
pub fn LocaleId::region(self : LocaleId) -> String? {
self.region
}
///|
pub fn LocaleId::variants(self : LocaleId) -> Array[String] {
self.variants
}
///|
pub fn LocaleId::has_script(self : LocaleId) -> Bool {
self.script is Some(_)
}
///|
pub fn LocaleId::has_region(self : LocaleId) -> Bool {
self.region is Some(_)
}
///|
pub fn LocaleId::is_language(self : LocaleId, language : String) -> Bool {
self.language == language.to_lower()
}
///|
fn push_unique_locale(
output : Array[String],
seen : Map[String, Unit],
value : String,
) -> Unit {
if value != "" && !seen.contains(value) {
seen[value] = ()
output.push(value)
}
}
///|
/// Return the exact locale followed by progressively broader parent locales.
pub fn LocaleId::fallback_chain(self : LocaleId) -> Array[String] {
let output : Array[String] = []
let seen : Map[String, Unit] = Map([])
let variants = self.variants.copy()
while variants.length() > 0 {
push_unique_locale(
output,
seen,
join_locale_parts(
canonical_parts(self.language, self.script, self.region, variants),
),
)
ignore(variants.pop())
}
push_unique_locale(
output,
seen,
join_locale_parts(
canonical_parts(self.language, self.script, self.region, []),
),
)
match self.region {
Some(_) =>
push_unique_locale(
output,
seen,
join_locale_parts(canonical_parts(self.language, self.script, None, [])),
)
None => ()
}
match self.script {
Some(_) => push_unique_locale(output, seen, self.language)
None => ()
}
output
}
///|
/// Build a fallback chain for a locale, tolerating non-canonical input.
pub fn locale_fallback_chain(locale : String) -> Array[String] {
try LocaleId::parse(locale) catch {
_ => [canonicalize_locale_lossy(locale)]
} noraise {
parsed => parsed.fallback_chain()
}
}
///|
fn supported_language(language : String) -> Bool {
language == "zh" ||
language == "en" ||
language == "ja" ||
language == "ru" ||
language == "ar"
}
///|
/// Return whether MoonL10n has an explicit plural rule for the locale.
pub fn is_supported_locale(locale : String) -> Bool {
try LocaleId::parse(locale) catch {
_ => false
} noraise {
parsed => supported_language(parsed.language)
}
}
///|
fn locale_language_lossy(locale : String) -> String {
try LocaleId::parse(locale) catch {
_ => {
let normalized = canonicalize_locale_lossy(locale).to_lower()
let parts = normalized.split("-").to_array()
match parts {
[first, ..] => "\{first}"
[] => normalized
}
}
} noraise {
parsed => parsed.language
}
}
///|
fn cardinal_family_for_language(language : String) -> PluralRuleFamily {
match language {
"zh" | "ja" => OtherOnly
"ru" => Russian
"ar" => Arabic
_ => English
}
}
///|
fn ordinal_family_for_language(language : String) -> PluralRuleFamily {
if language == "en" {
English
} else {
OtherOnly
}
}
///|
fn categories_for_family(family : PluralRuleFamily) -> Array[String] {
match family {
OtherOnly => ["other"]
English => ["one", "other"]
Russian => ["one", "few", "many", "other"]
Arabic => ["zero", "one", "two", "few", "many", "other"]
}
}
///|
fn ordinal_categories_for_language(language : String) -> Array[String] {
if language == "en" {
["one", "two", "few", "other"]
} else {
["other"]
}
}
///|
fn direction_for_language(language : String) -> TextDirection {
if language == "ar" {
RightToLeft
} else {
LeftToRight
}
}
///|
/// Return plural metadata for a locale.
pub fn locale_profile(locale : String) -> LocaleProfile raise MessageError {
let parsed = LocaleId::parse(locale)
let cardinal_family = cardinal_family_for_language(parsed.language)
let ordinal_family = ordinal_family_for_language(parsed.language)
{
locale: parsed,
direction: direction_for_language(parsed.language),
cardinal_family,
ordinal_family,
cardinal_categories: categories_for_family(cardinal_family),
ordinal_categories: ordinal_categories_for_language(parsed.language),
}
}
///|
pub fn LocaleProfile::locale(self : LocaleProfile) -> LocaleId {
self.locale
}
///|
pub fn LocaleProfile::direction(self : LocaleProfile) -> TextDirection {
self.direction
}
///|
pub fn LocaleProfile::cardinal_family(self : LocaleProfile) -> PluralRuleFamily {
self.cardinal_family
}
///|
pub fn LocaleProfile::ordinal_family(self : LocaleProfile) -> PluralRuleFamily {
self.ordinal_family
}
///|
pub fn LocaleProfile::cardinal_categories(
self : LocaleProfile,
) -> Array[String] {
self.cardinal_categories
}
///|
pub fn LocaleProfile::ordinal_categories(self : LocaleProfile) -> Array[String] {
self.ordinal_categories
}
///|
pub fn TextDirection::name(self : TextDirection) -> String {
match self {
LeftToRight => "ltr"
RightToLeft => "rtl"
}
}
///|
pub fn PluralRuleFamily::name(self : PluralRuleFamily) -> String {
match self {
OtherOnly => "other-only"
English => "english"
Russian => "russian"
Arabic => "arabic"
}
}
///|
/// Return the cardinal categories expected by the linter.
pub fn required_cardinal_categories(locale : String) -> Array[String] {
let language = locale_language_lossy(locale)
categories_for_family(cardinal_family_for_language(language))
}
///|
/// Return the ordinal categories expected by the linter.
pub fn required_ordinal_categories(locale : String) -> Array[String] {
ordinal_categories_for_language(locale_language_lossy(locale))
}
///|
/// Return all locale identifiers with explicit v0.2 plural support.
pub fn supported_locales() -> Array[String] {
["zh", "zh-CN", "en", "ja", "ru", "ar"]
}
///|
/// Return all canonical base languages with explicit plural support.
pub fn supported_languages() -> Array[String] {
["zh", "en", "ja", "ru", "ar"]
}