#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
}
impl TextDirection {
pub fn from_lang(lang: &str) -> Self {
if lang.starts_with("ar") || lang.starts_with("he") || lang.starts_with("fa") || lang.starts_with("ur") || lang.starts_with("yi")
{
TextDirection::RightToLeft
} else {
TextDirection::LeftToRight
}
}
pub fn is_rtl(&self) -> bool {
matches!(self, TextDirection::RightToLeft)
}
}
#[derive(Debug, Clone)]
pub struct Locale {
pub language: String,
pub region: Option<String>,
pub text_direction: TextDirection,
pub decimal_separator: char,
pub thousands_separator: char,
pub date_format: String,
pub time_format: String,
pub currency_symbol: String,
pub currency_before: bool,
}
impl Locale {
pub fn new(language: impl Into<String>, region: Option<String>) -> Self {
let language = language.into();
let text_direction = TextDirection::from_lang(&language);
let (decimal_sep, thousands_sep) = match language.as_str() {
"de" | "es" | "fr" | "it" | "pt" | "ru" => (',', '.'),
_ => ('.', ','),
};
let date_format = match language.as_str() {
"en" if region.as_deref() == Some("US") => "%m/%d/%Y".to_string(),
"en" => "%d/%m/%Y".to_string(),
"ja" | "zh" | "ko" => "%Y-%m-%d".to_string(),
_ => "%d.%m.%Y".to_string(),
};
let time_format = match language.as_str() {
"en" if region.as_deref() == Some("US") => "%I:%M %p".to_string(),
_ => "%H:%M".to_string(),
};
let currency_symbol = match (language.as_str(), region.as_deref()) {
("en", Some("US")) => "$".to_string(),
("en", Some("GB")) => "£".to_string(),
("ja", _) => "Â¥".to_string(),
("ar", Some("SA")) => "ï·¼".to_string(),
_ => "$".to_string(),
};
Locale {
language,
region,
text_direction,
decimal_separator: decimal_sep,
thousands_separator: thousands_sep,
date_format,
time_format,
currency_symbol,
currency_before: true,
}
}
pub fn from_string(locale_str: &str) -> Self {
if locale_str.is_empty() {
return Self::default();
}
let parts: Vec<&str> = locale_str.split('-').collect();
let language = parts
.first()
.filter(|s| !s.is_empty())
.unwrap_or(&"en")
.to_string();
let region = parts.get(1).map(|s| s.to_uppercase());
Self::new(language, region)
}
pub fn from_env() -> Self {
if let Ok(lang) = std::env::var("LANG") {
let locale_part = lang.split('.').next().unwrap_or("en_US");
let normalized = locale_part.replace('_', "-");
Self::from_string(&normalized)
} else {
Self::default()
}
}
pub fn format_number(&self, num: f64, decimals: usize) -> String {
let abs_num = num.abs();
let integer_part = abs_num.trunc() as i64;
let fractional_part = abs_num.fract();
let int_str = integer_part.to_string();
let mut formatted_int = String::new();
for (i, ch) in int_str.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
formatted_int.insert(0, self.thousands_separator);
}
formatted_int.insert(0, ch);
}
let result = if decimals > 0 {
let multiplier = 10_f64.powi(decimals as i32);
let decimal_value = (fractional_part * multiplier).round() as u64;
let decimal_str = format!("{:0width$}", decimal_value, width = decimals);
format!("{}{}{}", formatted_int, self.decimal_separator, decimal_str)
} else {
formatted_int
};
if num < 0.0 {
format!("-{}", result)
} else {
result
}
}
pub fn format_currency(&self, amount: f64) -> String {
let formatted = self.format_number(amount, 2);
if self.currency_before {
format!("{}{}", self.currency_symbol, formatted)
} else {
format!("{} {}", formatted, self.currency_symbol)
}
}
}
impl Default for Locale {
fn default() -> Self {
Self::new("en", Some("US".to_string()))
}
}
impl std::fmt::Display for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref region) = self.region {
write!(f, "{}-{}", self.language, region)
} else {
write!(f, "{}", self.language)
}
}
}
#[derive(Debug, Clone)]
pub struct AccessibilitySettings {
pub high_contrast: bool,
pub prefer_reduced_motion: bool,
pub screen_reader_enabled: bool,
pub font_scale: f32,
}
impl AccessibilitySettings {
pub fn new() -> Self {
Self {
high_contrast: false,
prefer_reduced_motion: false,
screen_reader_enabled: false,
font_scale: 1.0,
}
}
pub fn from_env() -> Self {
Self {
high_contrast: std::env::var("ACCESSIBILITY_HIGH_CONTRAST").is_ok(),
prefer_reduced_motion: std::env::var("ACCESSIBILITY_REDUCED_MOTION").is_ok(),
screen_reader_enabled: std::env::var("SCREEN_READER").is_ok(),
font_scale: std::env::var("FONT_SCALE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1.0),
}
}
pub fn scale_dimension(&self, base: u16) -> u16 {
(base as f32 * self.font_scale).round() as u16
}
}
impl Default for AccessibilitySettings {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessibilityRole {
None,
Button,
Heading { level: u8 },
Link,
List,
ListItem,
TextBox,
Label,
StatusBar,
Menu,
MenuItem,
Dialog,
Alert,
ProgressBar,
Tab,
TabPanel,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_direction_detection() {
assert_eq!(TextDirection::from_lang("en"), TextDirection::LeftToRight);
assert_eq!(TextDirection::from_lang("ar"), TextDirection::RightToLeft);
assert_eq!(TextDirection::from_lang("he"), TextDirection::RightToLeft);
assert_eq!(TextDirection::from_lang("ja"), TextDirection::LeftToRight);
}
#[test]
fn test_locale_parsing() {
let locale = Locale::from_string("en-US");
assert_eq!(locale.language, "en");
assert_eq!(locale.region, Some("US".to_string()));
assert_eq!(locale.text_direction, TextDirection::LeftToRight);
let locale_ar = Locale::from_string("ar-SA");
assert_eq!(locale_ar.text_direction, TextDirection::RightToLeft);
}
#[test]
fn test_number_formatting() {
let locale_us = Locale::new("en", Some("US".to_string()));
assert_eq!(locale_us.format_number(1234.56, 2), "1,234.56");
let locale_de = Locale::new("de", Some("DE".to_string()));
assert_eq!(locale_de.format_number(1234.56, 2), "1.234,56");
}
#[test]
fn test_currency_formatting() {
let locale_us = Locale::new("en", Some("US".to_string()));
let formatted = locale_us.format_currency(1234.56);
assert!(formatted.starts_with('$'));
assert!(formatted.contains("1,234.56"));
}
#[test]
fn test_accessibility_scaling() {
let mut settings = AccessibilitySettings::new();
settings.font_scale = 1.5;
assert_eq!(settings.scale_dimension(10), 15);
assert_eq!(settings.scale_dimension(20), 30);
}
#[test]
fn test_number_formatting_edge_cases() {
let locale_us = Locale::new("en", Some("US".to_string()));
assert_eq!(locale_us.format_number(0.0, 2), "0.00");
assert_eq!(locale_us.format_number(-1234.56, 2), "-1,234.56");
assert_eq!(locale_us.format_number(1234.0, 0), "1,234");
assert_eq!(locale_us.format_number(0.01, 2), "0.01");
}
#[test]
fn test_locale_from_invalid_string() {
let locale = Locale::from_string("");
assert_eq!(locale.language, "en");
let locale2 = Locale::from_string("xyz");
assert_eq!(locale2.language, "xyz");
assert_eq!(locale2.region, None);
}
#[test]
fn test_text_direction_unknown_language() {
assert_eq!(TextDirection::from_lang("xyz"), TextDirection::LeftToRight);
assert_eq!(TextDirection::from_lang(""), TextDirection::LeftToRight);
}
#[test]
fn test_accessibility_scaling_edge_cases() {
let settings = AccessibilitySettings::new();
assert_eq!(settings.scale_dimension(0), 0);
let large_val = settings.scale_dimension(1000);
assert_eq!(large_val, 1000);
let mut extreme = AccessibilitySettings::new();
extreme.font_scale = 3.0;
assert_eq!(extreme.scale_dimension(10), 30);
}
}