cranpose_localization/
locale.rs1use std::{fmt, str::FromStr};
2
3use unic_langid::{CharacterDirection, LanguageIdentifier};
4
5use crate::LocalizationError;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum PreviewMode {
10 #[default]
12 None,
13 Expanded,
15 Rtl,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Locale {
22 pub(crate) language: LanguageIdentifier,
23 pub(crate) preview: PreviewMode,
24}
25
26impl Locale {
27 pub fn parse(tag: &str) -> Result<Self, LocalizationError> {
30 let invalid = || LocalizationError::InvalidLocale(tag.to_owned());
31 let language = language_identifier(tag)
32 .ok_or_else(invalid)?
33 .parse()
34 .map_err(|_| invalid())?;
35 Ok(Self {
36 language,
37 preview: PreviewMode::None,
38 })
39 }
40
41 pub fn with_preview(mut self, preview: PreviewMode) -> Self {
43 self.preview = preview;
44 self
45 }
46
47 pub fn is_rtl(&self) -> bool {
49 self.preview == PreviewMode::Rtl
50 || self.language.character_direction() == CharacterDirection::RTL
51 }
52}
53
54fn language_identifier(tag: &str) -> Option<&str> {
55 let mut end = tag.len();
56 let mut offset = 0;
57 let mut extension = false;
58 let mut private = false;
59 let mut needs_value = false;
60 let mut seen = 0u64;
61 for part in tag.split('-') {
62 if !extension && offset != 0 && part.len() == 1 {
63 end = offset - 1;
64 extension = true;
65 }
66 if extension {
67 if part.len() == 1 && !private {
68 let byte = part.as_bytes()[0].to_ascii_lowercase();
69 let index = match byte {
70 b'0'..=b'9' => byte - b'0',
71 b'a'..=b'z' => byte - b'a' + 10,
72 _ => return None,
73 };
74 let flag = 1u64 << index;
75 if needs_value || seen & flag != 0 {
76 return None;
77 }
78 seen |= flag;
79 private = byte == b'x';
80 needs_value = true;
81 } else {
82 let minimum = if private { 1 } else { 2 };
83 if !(minimum..=8).contains(&part.len())
84 || !part.bytes().all(|byte| byte.is_ascii_alphanumeric())
85 {
86 return None;
87 }
88 needs_value = false;
89 }
90 }
91 offset += part.len() + 1;
92 }
93 (!needs_value).then_some(&tag[..end])
94}
95
96impl FromStr for Locale {
97 type Err = LocalizationError;
98
99 fn from_str(tag: &str) -> Result<Self, Self::Err> {
100 Self::parse(tag)
101 }
102}
103
104impl fmt::Display for Locale {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 self.language.fmt(formatter)
107 }
108}
109
110pub(crate) fn preview_text(text: String, preview: PreviewMode) -> String {
111 match preview {
112 PreviewMode::None => text,
113 PreviewMode::Rtl => format!("\u{2067}{text}\u{2069}"),
114 PreviewMode::Expanded => {
115 let mut output = String::with_capacity(text.len() * 2 + 4);
116 output.push('[');
117 for ch in text.chars() {
118 output.push(match ch {
119 'a' => 'á',
120 'e' => 'ë',
121 'i' => 'ï',
122 'o' => 'ö',
123 'u' => 'ü',
124 'A' => 'Á',
125 'E' => 'Ë',
126 'I' => 'Ï',
127 'O' => 'Ö',
128 'U' => 'Ü',
129 other => other,
130 });
131 if ch.is_ascii_alphabetic() && "aeiouAEIOU".contains(ch) {
132 output.push('~');
133 }
134 }
135 output.push(']');
136 output
137 }
138 }
139}