// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Language tag (BCP 47), stored in canonical lowercase form.
pub struct Language {
tag : String
} derive(Eq, Show, ToJson)
///|
/// Invalid language value.
pub let language_invalid : Language = Language::{ tag: "" }
///|
/// Canonicalize an ASCII language tag.
fn canonicalize_language_char(c : Char) -> Char? {
if c.is_ascii_digit() {
Some(c)
} else if c.is_ascii_alphabetic() {
Some(c.to_ascii_lowercase())
} else if c == '-' || c == '_' {
Some('-')
} else {
None
}
}
///|
/// Convert a string to a language value.
pub fn Language::from_string(s : String, len? : Int = -1) -> Language {
if s is "" || len == 0 {
return language_invalid
}
let max_len = if len < 0 { s.length() } else if len > 63 { 63 } else { len }
let sb = StringBuilder::new(size_hint=max_len)
let mut count = 0
for c in s {
if count >= max_len {
break
}
match canonicalize_language_char(c) {
None => break
Some(mapped) => sb.write_char(mapped)
}
count = count + 1
}
let tag = sb.to_string()
if tag is "" {
language_invalid
} else {
Language::{ tag, }
}
}
///|
/// Return the canonical string, or None for invalid.
pub fn Language::to_string(self : Language) -> String? {
if self.tag is "" {
None
} else {
Some(self.tag)
}
}
///|
/// True when the language is valid.
pub fn Language::is_valid(self : Language) -> Bool {
self.tag != ""
}
///|
/// Check whether `specific` is the same or more specific than `self`.
pub fn Language::matches(self : Language, specific : Language) -> Bool {
if self.tag == specific.tag {
return true
}
if self.tag is "" || specific.tag is "" {
return false
}
let base = self.tag
let spec = specific.tag
if base.length() > spec.length() {
return false
}
if !spec.has_prefix(base[:]) {
return false
}
if base.length() == spec.length() {
true
} else {
spec.get_char(base.length()) == Some('-')
}
}
///|
/// Access the canonical language tag.
pub fn Language::as_string(self : Language) -> String {
self.tag
}