This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/ui/vessels/el-i18n/src/locale.rs
T

183 lines
4.3 KiB
Rust

/// Locale — language + optional region + directionality.
///
/// Locale identifies both the language for translation lookup and the
/// region for number/date/currency formatting.
/// Text direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
}
/// A locale identifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Locale {
/// BCP 47 language tag (e.g. "en", "ar", "zh-Hant").
pub language: String,
/// Optional region (e.g. "US", "GB", "TW").
pub region: Option<String>,
}
impl Locale {
/// Create from a BCP 47 tag like "en-US" or "ar".
pub fn new(tag: impl Into<String>) -> Self {
let tag = tag.into();
if let Some(idx) = tag.find('-') {
let (lang, rest) = tag.split_at(idx);
let region = rest.trim_start_matches('-');
Self {
language: lang.to_lowercase(),
region: if region.is_empty() {
None
} else {
Some(region.to_uppercase())
},
}
} else {
Self {
language: tag.to_lowercase(),
region: None,
}
}
}
/// The full BCP 47 tag (e.g. "en-US").
pub fn tag(&self) -> String {
match &self.region {
Some(r) => format!("{}-{}", self.language, r),
None => self.language.clone(),
}
}
/// The text direction for this locale.
pub fn direction(&self) -> TextDirection {
if self.is_rtl() {
TextDirection::RightToLeft
} else {
TextDirection::LeftToRight
}
}
/// Whether this locale uses right-to-left script.
pub fn is_rtl(&self) -> bool {
// RTL language codes per Unicode CLDR
matches!(
self.language.as_str(),
"ar" // Arabic
| "he" | "iw" // Hebrew
| "fa" | "per" // Persian/Farsi
| "ur" // Urdu
| "ps" // Pashto
| "ug" // Uyghur
| "yi" // Yiddish
| "dv" // Maldivian/Dhivehi
| "ku" // Kurdish (some scripts)
| "sd" // Sindhi
)
}
/// English (US).
pub fn en_us() -> Self {
Self::new("en-US")
}
/// English (GB).
pub fn en_gb() -> Self {
Self::new("en-GB")
}
/// Arabic (a common RTL locale).
pub fn ar() -> Self {
Self::new("ar")
}
/// Arabic (Saudi Arabia).
pub fn ar_sa() -> Self {
Self::new("ar-SA")
}
/// Spanish (Spain).
pub fn es_es() -> Self {
Self::new("es-ES")
}
/// French (France).
pub fn fr_fr() -> Self {
Self::new("fr-FR")
}
/// Japanese.
pub fn ja() -> Self {
Self::new("ja")
}
/// Chinese (Traditional).
pub fn zh_hant() -> Self {
Self::new("zh-Hant")
}
}
impl std::fmt::Display for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.tag())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locale_parse_with_region() {
let l = Locale::new("en-US");
assert_eq!(l.language, "en");
assert_eq!(l.region, Some("US".to_string()));
}
#[test]
fn locale_parse_without_region() {
let l = Locale::new("ja");
assert_eq!(l.language, "ja");
assert_eq!(l.region, None);
}
#[test]
fn locale_tag_roundtrip() {
let l = Locale::new("fr-FR");
assert_eq!(l.tag(), "fr-FR");
}
#[test]
fn arabic_is_rtl() {
assert!(Locale::new("ar").is_rtl());
assert!(Locale::new("ar-SA").is_rtl());
}
#[test]
fn hebrew_is_rtl() {
assert!(Locale::new("he").is_rtl());
}
#[test]
fn persian_is_rtl() {
assert!(Locale::new("fa").is_rtl());
}
#[test]
fn english_is_ltr() {
assert!(!Locale::new("en").is_rtl());
assert_eq!(Locale::new("en-US").direction(), TextDirection::LeftToRight);
}
#[test]
fn rtl_direction() {
assert_eq!(Locale::ar().direction(), TextDirection::RightToLeft);
}
#[test]
fn locale_display() {
assert_eq!(format!("{}", Locale::en_us()), "en-US");
}
}