1use std::fmt::Display;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum Lang {
9 #[default]
10 EnUs,
11 PtBr,
12}
13
14pub const LANGS: [Lang; 2] = [Lang::EnUs, Lang::PtBr];
16
17impl Lang {
18 pub fn code(self) -> &'static str {
19 match self {
20 Lang::EnUs => "en-US",
21 Lang::PtBr => "pt-BR",
22 }
23 }
24
25 pub fn name(self) -> &'static str {
27 match self {
28 Lang::EnUs => "English",
29 Lang::PtBr => "Português",
30 }
31 }
32
33 pub fn parse(text: &str) -> Option<Lang> {
35 let base = text.split(['.', '@']).next().unwrap_or("");
36 let normalized = base.replace('_', "-").to_ascii_lowercase();
37 match normalized.split('-').next().unwrap_or("") {
38 "pt" => Some(Lang::PtBr),
39 "en" => Some(Lang::EnUs),
40 _ => None,
41 }
42 }
43
44 pub fn next(self) -> Lang {
45 match self {
46 Lang::EnUs => Lang::PtBr,
47 Lang::PtBr => Lang::EnUs,
48 }
49 }
50
51 pub fn is_one(self, n: u64) -> bool {
53 match self {
54 Lang::EnUs => n == 1,
55 Lang::PtBr => n <= 1,
56 }
57 }
58}
59
60pub fn detect(
63 flag: Option<&str>,
64 env: &dyn Fn(&str) -> Option<String>,
65 saved: Option<&str>,
66 system: Option<&str>,
67) -> Lang {
68 let non_empty = |name: &str| env(name).filter(|v| !v.is_empty());
69 let candidates = [
70 flag.map(str::to_owned),
71 non_empty("TABA_CLI_LANG"),
72 saved.map(str::to_owned),
73 non_empty("LC_ALL"),
74 non_empty("LC_MESSAGES"),
75 non_empty("LANG"),
76 system.map(str::to_owned),
77 ];
78 candidates
79 .into_iter()
80 .flatten()
81 .find_map(|c| Lang::parse(&c))
82 .unwrap_or_default()
83}
84
85pub fn tr(lang: Lang, key: &str) -> String {
88 let text = crate::_rust_i18n_translate(lang.code(), key).into_owned();
89 let missing = |code: &str| text == format!("{code}.{key}");
91 if missing(lang.code()) || missing(Lang::EnUs.code()) {
92 key.to_owned()
93 } else {
94 text
95 }
96}
97
98pub fn tr_with(lang: Lang, key: &str, args: &[(&str, &dyn Display)]) -> String {
100 let mut text = tr(lang, key);
101 for (name, value) in args {
102 text = text.replace(&format!("%{{{name}}}"), &value.to_string());
103 }
104 text
105}
106
107pub fn tr_n(lang: Lang, key: &str, n: u64) -> String {
109 let form = if lang.is_one(n) { "one" } else { "other" };
110 tr_with(lang, &format!("{key}.{form}"), &[("n", &group(lang, n))])
111}
112
113pub fn variables(text: &str) -> Vec<String> {
115 let mut found = Vec::new();
116 let mut rest = text;
117 while let Some(start) = rest.find("%{") {
118 let after = &rest[start + 2..];
119 let Some(end) = after.find('}') else { break };
120 found.push(after[..end].to_owned());
121 rest = &after[end + 1..];
122 }
123 found.sort();
124 found.dedup();
125 found
126}
127
128pub fn group(lang: Lang, n: u64) -> String {
130 let digits = n.to_string();
131 let separator = match lang {
132 Lang::EnUs => ',',
133 Lang::PtBr => '.',
134 };
135 let mut out = String::new();
136 for (i, c) in digits.chars().enumerate() {
137 if i > 0 && (digits.len() - i).is_multiple_of(3) {
138 out.push(separator);
139 }
140 out.push(c);
141 }
142 out
143}
144
145pub fn decimal(lang: Lang, value: f64, places: usize) -> String {
147 let text = format!("{value:.places$}");
148 match lang {
149 Lang::EnUs => text,
150 Lang::PtBr => text.replace('.', ","),
151 }
152}
153
154pub fn bytes(lang: Lang, n: u64) -> String {
156 const UNITS: [&str; 6] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
157 let mut value = n as f64;
158 let mut unit = 0;
159 while value >= 1024.0 && unit < UNITS.len() - 1 {
160 value /= 1024.0;
161 unit += 1;
162 }
163 if unit == 0 {
164 format!("{n} B")
165 } else {
166 format!("{} {}", decimal(lang, value, 1), UNITS[unit])
167 }
168}
169
170pub fn duration(secs: u64) -> String {
172 let (d, h, m, s) = (secs / 86_400, secs / 3600 % 24, secs / 60 % 60, secs % 60);
173 if d > 0 {
174 format!("{d}d {h:02}h")
175 } else if h > 0 {
176 format!("{h}h {m:02}m")
177 } else if m > 0 {
178 format!("{m}m {s:02}s")
179 } else {
180 format!("{s}s")
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct Civil {
187 pub year: i64,
188 pub month: u32,
189 pub day: u32,
190 pub hour: u32,
191 pub minute: u32,
192 pub second: u32,
193}
194
195fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
197 let y = if month <= 2 { year - 1 } else { year };
198 let era = y.div_euclid(400);
199 let yoe = y.rem_euclid(400);
200 let mp = (i64::from(month) + 9) % 12;
201 let doy = (153 * mp + 2) / 5 + i64::from(day) - 1;
202 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
203 era * 146_097 + doe - 719_468
204}
205
206pub fn civil_from_ms(ms: i64) -> Civil {
207 let secs = ms.div_euclid(1000);
208 let days = secs.div_euclid(86_400);
209 let rem = secs.rem_euclid(86_400);
210 let z = days + 719_468;
211 let era = z.div_euclid(146_097);
212 let doe = z.rem_euclid(146_097);
213 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
214 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
215 let mp = (5 * doy + 2) / 153;
216 let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
217 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
218 let year = yoe + era * 400 + i64::from(month <= 2);
219 Civil {
220 year,
221 month,
222 day,
223 hour: (rem / 3600) as u32,
224 minute: (rem / 60 % 60) as u32,
225 second: (rem % 60) as u32,
226 }
227}
228
229pub fn parse_instant(text: &str) -> Option<i64> {
231 let (date, time) = text.split_once(['T', 't', ' '])?;
232 let mut d = date.split('-');
233 let (y, m, day) = (
234 d.next()?.parse::<i64>().ok()?,
235 d.next()?.parse::<u32>().ok()?,
236 d.next()?.parse::<u32>().ok()?,
237 );
238 if !(1..=12).contains(&m) || !(1..=31).contains(&day) {
239 return None;
240 }
241 let split = time.find(['Z', 'z', '+', '-']).unwrap_or(time.len());
242 let (clock, zone) = time.split_at(split);
243 let mut c = clock.split(':');
244 let hour = c.next()?.parse::<i64>().ok()?;
245 let minute = c.next()?.parse::<i64>().ok()?;
246 let seconds = c.next().unwrap_or("0");
247 let (whole, fraction) = seconds.split_once('.').unwrap_or((seconds, ""));
248 let second = whole.parse::<i64>().ok()?;
249 let millis = format!("{fraction:0<3}")[..3].parse::<i64>().ok()?;
250 let offset = match zone.chars().next() {
251 Some(sign @ ('+' | '-')) => {
252 let (h, m) = zone[1..].split_once(':').unwrap_or((&zone[1..], "0"));
253 let minutes = h.parse::<i64>().ok()? * 60 + m.parse::<i64>().ok()?;
254 if sign == '+' {
255 minutes
256 } else {
257 -minutes
258 }
259 }
260 _ => 0,
261 };
262 let days = days_from_civil(y, m, day);
263 Some(((days * 86_400 + hour * 3600 + minute * 60 + second - offset * 60) * 1000) + millis)
264}
265
266pub fn date_time(lang: Lang, ms: i64) -> String {
268 let c = civil_from_ms(ms);
269 let clock = format!("{:02}:{:02}:{:02}", c.hour, c.minute, c.second);
270 match lang {
271 Lang::EnUs => format!("{:04}-{:02}-{:02} {clock}", c.year, c.month, c.day),
272 Lang::PtBr => format!("{:02}/{:02}/{:04} {clock}", c.day, c.month, c.year),
273 }
274}
275
276pub fn clock(ms: i64) -> String {
278 let c = civil_from_ms(ms);
279 format!("{:02}:{:02}:{:02}", c.hour, c.minute, c.second)
280}
281
282#[cfg(test)]
283mod tests;