1use qframe::i18n::I18n;
14
15use crate::store::Config;
16
17const FALLBACK: &str = "en";
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Languages {
24 order: Vec<&'static str>,
25}
26
27impl Languages {
28 #[must_use]
34 pub fn new(system: Option<&str>, name: impl Fn(&str) -> String) -> Self {
35 let first =
36 system.and_then(|wanted| Config::LANGUAGES.into_iter().find(|known| *known == wanted)).unwrap_or(FALLBACK);
37 let mut rest: Vec<&'static str> = Config::LANGUAGES.into_iter().filter(|code| *code != first).collect();
38 rest.sort_by_cached_key(|code| sort_key(&name(code)));
39 let mut order = Vec::with_capacity(Config::LANGUAGES.len());
40 order.push(first);
41 order.extend(rest);
42 Self { order }
43 }
44
45 #[must_use]
48 pub fn shown_in(system: Option<&str>, speaking: &str) -> Self {
49 let mut catalog = I18n::builtin();
50 for (file, text) in crate::locales() {
51 catalog.add_source(&file, &text);
52 }
53 catalog.set_active(speaking);
54 Self::new(system, |code| catalog.translate(&format!("setup.language-{code}"), &[]))
55 }
56
57 #[must_use]
59 pub fn codes(&self) -> &[&'static str] {
60 &self.order
61 }
62
63 #[must_use]
65 pub fn row_of(&self, code: &str) -> usize {
66 self.order.iter().position(|known| *known == code).unwrap_or(0)
67 }
68
69 #[must_use]
71 pub fn on_row(&self, row: usize) -> &'static str {
72 self.order.get(row).copied().unwrap_or(self.order[0])
73 }
74}
75
76#[must_use]
81pub fn system(lookup: impl Fn(&str) -> Option<String>) -> Option<String> {
82 let found = I18n::builtin().detect(lookup)?;
83 Config::LANGUAGES.into_iter().find(|known| *known == found).map(str::to_owned)
84}
85
86#[must_use]
88pub fn system_here() -> Option<String> {
89 system(|name| std::env::var(name).ok())
90}
91
92fn sort_key(name: &str) -> String {
98 name.to_lowercase().chars().map(fold).collect()
99}
100
101fn fold(letter: char) -> char {
103 match letter {
104 'á' | 'à' | 'â' | 'ä' | 'ã' | 'å' | 'ā' => 'a',
105 'ç' | 'ć' | 'č' => 'c',
106 'é' | 'è' | 'ê' | 'ë' | 'ē' => 'e',
107 'ğ' | 'ģ' => 'g',
108 'í' | 'ì' | 'î' | 'ï' | 'ī' | 'ı' => 'i',
109 'ñ' | 'ń' => 'n',
110 'ó' | 'ò' | 'ô' | 'ö' | 'õ' | 'ø' | 'ō' => 'o',
111 'ş' | 'ś' | 'š' => 's',
112 'ú' | 'ù' | 'û' | 'ü' | 'ū' => 'u',
113 'ý' | 'ÿ' => 'y',
114 'ž' | 'ź' | 'ż' => 'z',
115 '\u{0307}' => '\0',
117 other => other,
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::{Languages, system};
124
125 fn english(code: &str) -> String {
126 match code {
127 "en" => "English",
128 "tr" => "Turkish",
129 "de" => "German",
130 "es" => "Spanish",
131 "fr" => "French",
132 "pt-BR" => "Portuguese (Brazil)",
133 "ru" => "Russian",
134 "zh-Hans" => "Chinese (Simplified)",
135 _ => "Japanese",
136 }
137 .to_owned()
138 }
139
140 fn env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
141 move |name: &str| pairs.iter().find(|(key, _)| *key == name).map(|(_, value)| (*value).to_owned())
142 }
143
144 #[test]
145 fn the_machines_own_language_takes_the_first_row() {
146 let order = Languages::new(Some("tr"), english);
147 assert_eq!(order.codes()[0], "tr");
148 assert_eq!(order.row_of("tr"), 0);
149 }
150
151 #[test]
152 fn a_machine_that_names_no_language_qcode_speaks_gets_english_first() {
153 for system in [None, Some("fi"), Some("")] {
156 let order = Languages::new(system, english);
157 assert_eq!(order.codes()[0], "en", "{system:?}");
158 }
159 }
160
161 #[test]
162 fn the_rest_of_the_list_reads_alphabetically() {
163 let order = Languages::new(Some("tr"), english);
164 assert_eq!(
165 order.codes(),
166 ["tr", "zh-Hans", "en", "fr", "de", "ja", "pt-BR", "ru", "es"],
167 "Chinese, English, French, German, Japanese, Portuguese, Russian, Spanish"
168 );
169 }
170
171 #[test]
172 fn the_first_row_is_not_repeated_further_down() {
173 let order = Languages::new(Some("de"), english);
174 assert_eq!(order.codes().len(), 9);
175 assert_eq!(order.codes().iter().filter(|code| **code == "de").count(), 1);
176 }
177
178 #[test]
179 fn a_marked_letter_sorts_as_the_plain_one() {
180 let turkish = |code: &str| {
183 match code {
184 "en" => "İngilizce",
185 "tr" => "Türkçe",
186 "de" => "Almanca",
187 "es" => "İspanyolca",
188 "fr" => "Fransızca",
189 "pt-BR" => "Portekizce (Brezilya)",
190 "ru" => "Rusça",
191 "zh-Hans" => "Çince (Basitleştirilmiş)",
192 _ => "Japonca",
193 }
194 .to_owned()
195 };
196 let order = Languages::new(Some("tr"), turkish);
197 assert_eq!(order.codes(), ["tr", "de", "zh-Hans", "fr", "en", "es", "ja", "pt-BR", "ru"]);
198 }
199
200 #[test]
201 fn the_language_of_the_machine_is_read_the_way_every_quvyta_application_reads_it() {
202 assert_eq!(system(env(&[("LANG", "tr_TR.UTF-8")])), Some("tr".to_owned()));
203 assert_eq!(system(env(&[("LC_ALL", "pt_BR.UTF-8"), ("LANG", "tr_TR.UTF-8")])), Some("pt-BR".to_owned()));
204 assert_eq!(system(env(&[("LANG", "C")])), None);
205 }
208
209 #[test]
210 fn the_names_the_order_is_built_from_are_the_ones_the_wizard_shows() {
211 let order = Languages::shown_in(Some("tr"), "tr");
214 assert_eq!(order.codes(), ["tr", "de", "zh-Hans", "fr", "en", "es", "ja", "pt-BR", "ru"]);
215 }
216}