1use std::collections::BTreeMap;
24use std::fmt;
25use std::sync::{Arc, OnceLock};
26
27pub mod model;
28
29pub const FALLBACK: &str = "en";
31
32const BUILTIN: &[(&str, &str)] = &[
34 ("en", include_str!("../texts/en/ai.json")),
35 ("en", include_str!("../texts/en/format.json")),
36 ("en", include_str!("../texts/en/movie.json")),
37 ("en", include_str!("../texts/en/pane.json")),
38 ("en", include_str!("../texts/en/report.json")),
39 ("en", include_str!("../texts/en/rules.json")),
40 ("en", include_str!("../texts/en/sqlite.json")),
41 ("en", include_str!("../texts/en/spreadsheet.json")),
42 ("en", include_str!("../texts/en/ui.json")),
43 ("en", include_str!("../texts/en/viewer.json")),
44 ("en", include_str!("../texts/en/widgets.json")),
45 ("pt-BR", include_str!("../texts/pt-BR/ai.json")),
46 ("pt-BR", include_str!("../texts/pt-BR/format.json")),
47 ("pt-BR", include_str!("../texts/pt-BR/movie.json")),
48 ("pt-BR", include_str!("../texts/pt-BR/pane.json")),
49 ("pt-BR", include_str!("../texts/pt-BR/report.json")),
50 ("pt-BR", include_str!("../texts/pt-BR/rules.json")),
51 ("pt-BR", include_str!("../texts/pt-BR/sqlite.json")),
52 ("pt-BR", include_str!("../texts/pt-BR/spreadsheet.json")),
53 ("pt-BR", include_str!("../texts/pt-BR/ui.json")),
54 ("pt-BR", include_str!("../texts/pt-BR/viewer.json")),
55 ("pt-BR", include_str!("../texts/pt-BR/widgets.json")),
56];
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
60#[non_exhaustive]
61pub enum Language {
62 #[default]
64 English,
65 BrazilianPortuguese,
67}
68
69impl Language {
70 pub const ALL: &'static [Language] = &[Self::English, Self::BrazilianPortuguese];
72
73 pub fn code(self) -> &'static str {
75 match self {
76 Self::English => "en",
77 Self::BrazilianPortuguese => "pt-BR",
78 }
79 }
80
81 pub fn from_code(code: &str) -> Option<Self> {
84 let code = code.trim().replace('_', "-").to_ascii_lowercase();
85 match code.as_str() {
86 "pt" | "pt-br" => Some(Self::BrazilianPortuguese),
87 "en" => Some(Self::English),
88 _ if code.starts_with("en-") => Some(Self::English),
89 _ => None,
90 }
91 }
92
93 pub fn name(self) -> &'static str {
95 match self {
96 Self::English => "English",
97 Self::BrazilianPortuguese => "Português (Brasil)",
98 }
99 }
100}
101
102impl fmt::Display for Language {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(self.name())
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct TextError(pub String);
111
112impl fmt::Display for TextError {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.write_str(&self.0)
115 }
116}
117
118impl std::error::Error for TextError {}
119
120#[derive(Clone, Debug, Default, PartialEq, Eq)]
122pub struct Catalog {
123 languages: BTreeMap<String, BTreeMap<String, String>>,
124}
125
126impl Catalog {
127 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn builtin() -> Self {
134 static BUILT: OnceLock<Catalog> = OnceLock::new();
135 BUILT
136 .get_or_init(|| {
137 let mut catalog = Catalog::new();
138 for (language, json) in BUILTIN {
139 let read = catalog.merge_json(language, json);
141 debug_assert!(read.is_ok(), "{read:?}");
142 }
143 catalog
144 })
145 .clone()
146 }
147
148 pub fn set(&mut self, language: &str, key: impl Into<String>, text: impl Into<String>) {
150 self.languages.entry(language.to_owned()).or_default().insert(key.into(), text.into());
151 }
152
153 pub fn with(mut self, language: &str, key: impl Into<String>, text: impl Into<String>) -> Self {
155 self.set(language, key, text);
156 self
157 }
158
159 pub fn merge_json(&mut self, language: &str, json: &str) -> Result<(), TextError> {
162 let wrong = |detail: String| TextError(format!("the texts of {language}: {detail}"));
163 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| wrong(e.to_string()))?;
164 let serde_json::Value::Object(entries) = value else {
165 return Err(wrong("they are not an object of keys".into()));
166 };
167 let mut read = Vec::with_capacity(entries.len());
168 for (key, text) in entries {
169 match text {
170 serde_json::Value::String(text) => read.push((key, text)),
171 _ => return Err(wrong(format!("`{key}` is not text"))),
172 }
173 }
174 let texts = self.languages.entry(language.to_owned()).or_default();
175 texts.extend(read);
176 Ok(())
177 }
178
179 pub fn merge(&mut self, other: &Catalog) {
181 for (language, texts) in &other.languages {
182 let mine = self.languages.entry(language.clone()).or_default();
183 mine.extend(texts.iter().map(|(key, text)| (key.clone(), text.clone())));
184 }
185 }
186
187 pub fn text(&self, language: &str, key: &str) -> Option<&str> {
189 self.own_text(language, key).or_else(|| self.own_text(FALLBACK, key))
190 }
191
192 pub fn own_text(&self, language: &str, key: &str) -> Option<&str> {
194 self.languages.get(language)?.get(key).map(String::as_str)
195 }
196
197 pub fn languages(&self) -> impl Iterator<Item = &str> {
199 self.languages.keys().map(String::as_str)
200 }
201
202 pub fn keys(&self, language: &str) -> impl Iterator<Item = &str> {
204 self.languages.get(language).into_iter().flat_map(|texts| texts.keys().map(String::as_str))
205 }
206
207 pub fn missing(&self, language: &str) -> Vec<String> {
209 self.keys(FALLBACK).filter(|key| self.own_text(language, key).is_none()).map(str::to_owned).collect()
210 }
211
212 pub fn texts(&self, language: Language) -> Texts {
214 Texts::of(self.clone(), language.code())
215 }
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct Texts {
221 catalog: Arc<Catalog>,
222 language: String,
223}
224
225impl Texts {
226 pub fn new(language: Language) -> Self {
228 Catalog::builtin().texts(language)
229 }
230
231 pub fn of(catalog: Catalog, language: impl Into<String>) -> Self {
234 Self { catalog: Arc::new(catalog), language: language.into() }
235 }
236
237 pub fn language(&self) -> &str {
239 &self.language
240 }
241
242 pub fn catalog(&self) -> &Catalog {
244 &self.catalog
245 }
246
247 pub fn get(&self, key: &str) -> String {
249 self.catalog.text(&self.language, key).unwrap_or(key).to_owned()
250 }
251
252 pub fn char(&self, key: &str, otherwise: char) -> char {
254 self.catalog.text(&self.language, key).and_then(|text| text.chars().next()).unwrap_or(otherwise)
255 }
256
257 pub fn list(&self, key: &str) -> Vec<String> {
259 (0..)
260 .map_while(|i| self.catalog.text(&self.language, &format!("{key}.{i}")).map(str::to_owned))
261 .collect()
262 }
263
264 pub fn array<const N: usize>(&self, key: &str) -> [String; N] {
266 std::array::from_fn(|i| self.get(&format!("{key}.{i}")))
267 }
268
269 pub fn pair(&self, key: &str) -> (String, String) {
271 (self.get(&format!("{key}.0")), self.get(&format!("{key}.1")))
272 }
273
274 pub fn pair_list(&self, key: &str) -> Vec<(String, String)> {
276 (0..)
277 .map_while(|i| {
278 let at = format!("{key}.{i}");
279 self.catalog.text(&self.language, &format!("{at}.0")).map(|_| self.pair(&at))
280 })
281 .collect()
282 }
283
284 pub fn pairs<const N: usize>(&self, key: &str) -> [(String, String); N] {
286 std::array::from_fn(|i| self.pair(&format!("{key}.{i}")))
287 }
288
289 pub fn format(&self, key: &str, values: &[&str]) -> String {
291 format(&self.get(key), values)
292 }
293}
294
295impl Default for Texts {
296 fn default() -> Self {
297 Self::new(Language::default())
298 }
299}
300
301pub fn format(template: &str, values: &[&str]) -> String {
303 format_with(template, |index, out| match values.get(index) {
304 Some(value) => {
305 out.push_str(value);
306 true
307 }
308 None => false,
309 })
310}
311
312pub fn format_with(template: &str, mut value: impl FnMut(usize, &mut String) -> bool) -> String {
316 let mut out = String::with_capacity(template.len() + 8);
317 let mut rest = template;
318 while let Some(open) = rest.find('{') {
319 out.push_str(&rest[..open]);
320 let after = &rest[open + 1..];
321 let index =
322 after.find('}').and_then(|close| after[..close].parse::<usize>().ok().map(|i| (i, close)));
323 match index {
324 Some((index, close)) if value(index, &mut out) => rest = &after[close + 1..],
325 _ => {
326 out.push('{');
327 rest = after;
328 }
329 }
330 }
331 out.push_str(rest);
332 out
333}