use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TextDirection {
#[default]
Ltr,
Rtl,
}
impl TextDirection {
pub fn is_rtl(&self) -> bool {
matches!(self, TextDirection::Rtl)
}
}
impl fmt::Display for TextDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TextDirection::Ltr => write!(f, "ltr"),
TextDirection::Rtl => write!(f, "rtl"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocaleParseError;
impl fmt::Display for LocaleParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to parse locale from tag")
}
}
impl std::error::Error for LocaleParseError {}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct Locale {
language: String,
region: Option<String>,
variant: Option<String>,
}
impl Locale {
pub fn new(tag: &str) -> Result<Self, LocaleParseError> {
let parts: Vec<&str> = tag.split('-').collect();
let language = parts
.first()
.map(|s| s.to_string())
.ok_or(LocaleParseError)?;
let region = parts.get(1).map(|s| s.to_string());
let variant = parts.get(2).map(|s| s.to_string());
Ok(Self {
language,
region,
variant,
})
}
pub fn language(&self) -> &str {
&self.language
}
pub fn region(&self) -> Option<&str> {
self.region.as_deref()
}
pub fn variant(&self) -> Option<&str> {
self.variant.as_deref()
}
pub fn text_direction(&self) -> TextDirection {
match self.language.as_str() {
"ar" | "he" | "fa" | "ur" | "yi" | "ps" => TextDirection::Rtl,
_ => TextDirection::Ltr,
}
}
pub fn display_name(&self) -> String {
if let Some(region) = &self.region {
if let Some(variant) = &self.variant {
format!("{}-{}-{}", self.language, region, variant)
} else {
format!("{}-{}", self.language, region)
}
} else {
self.language.clone()
}
}
pub fn to_tag(&self) -> String {
self.display_name()
}
}
impl Default for Locale {
fn default() -> Self {
Self::new("en").unwrap()
}
}
impl fmt::Display for Locale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
impl FromStr for Locale {
type Err = LocaleParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SupportedLocale {
EnUS,
EnGB,
ZhCN,
ZhTW,
Ja,
Ar,
He,
Fr,
De,
Es,
Ko,
}
impl SupportedLocale {
pub fn to_locale(&self) -> Locale {
match self {
SupportedLocale::EnUS => Locale::new("en-US").unwrap(),
SupportedLocale::EnGB => Locale::new("en-GB").unwrap(),
SupportedLocale::ZhCN => Locale::new("zh-CN").unwrap(),
SupportedLocale::ZhTW => Locale::new("zh-TW").unwrap(),
SupportedLocale::Ja => Locale::new("ja").unwrap(),
SupportedLocale::Ar => Locale::new("ar").unwrap(),
SupportedLocale::He => Locale::new("he").unwrap(),
SupportedLocale::Fr => Locale::new("fr").unwrap(),
SupportedLocale::De => Locale::new("de").unwrap(),
SupportedLocale::Es => Locale::new("es").unwrap(),
SupportedLocale::Ko => Locale::new("ko").unwrap(),
}
}
pub fn all() -> &'static [SupportedLocale] {
&[
SupportedLocale::EnUS,
SupportedLocale::EnGB,
SupportedLocale::ZhCN,
SupportedLocale::ZhTW,
SupportedLocale::Ja,
SupportedLocale::Ar,
SupportedLocale::He,
SupportedLocale::Fr,
SupportedLocale::De,
SupportedLocale::Es,
SupportedLocale::Ko,
]
}
pub fn match_locale(tag: &str) -> Option<SupportedLocale> {
if let Some(&locale) = Self::all().iter().find(|&&l| l.to_locale().to_tag() == tag) {
return Some(locale);
}
let lang = tag.split('-').next()?;
Self::all()
.iter()
.find(|&&locale| locale.to_locale().language() == lang)
.copied()
}
}
impl From<SupportedLocale> for Locale {
fn from(locale: SupportedLocale) -> Self {
locale.to_locale()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_locale_creation() {
let locale = Locale::new("zh-CN").unwrap();
assert_eq!(locale.language(), "zh");
assert_eq!(locale.region(), Some("CN"));
}
#[test]
fn test_text_direction() {
let en = Locale::new("en").unwrap();
assert_eq!(en.text_direction(), TextDirection::Ltr);
let ar = Locale::new("ar").unwrap();
assert_eq!(ar.text_direction(), TextDirection::Rtl);
}
#[test]
fn test_supported_locale_match() {
assert!(SupportedLocale::match_locale("zh-CN").is_some());
assert!(SupportedLocale::match_locale("en").is_some());
assert!(SupportedLocale::match_locale("unknown").is_none());
}
}