pacsea/sources/status/
translate.rs

1use crate::state::AppState;
2
3/// What: Extract AUR suffix information from remaining text after a service ratio.
4///
5/// Inputs:
6/// - `remaining`: Text remaining after extracting service ratio
7///
8/// Output:
9/// - `(Option<u32>, Option<f64>)` tuple with AUR percentage and ratio if found
10fn extract_aur_suffix(remaining: &str) -> (Option<u32>, Option<f64>) {
11    let aur_suffix_pct = remaining.find(" — AUR today: ").and_then(|aur_pos| {
12        let aur_part = &remaining[aur_pos + 14..];
13        aur_part
14            .strip_suffix('%')
15            .and_then(|s| s.parse::<u32>().ok())
16    });
17    let aur_suffix_ratio = remaining.find(" (AUR: ").and_then(|aur_pos| {
18        let aur_part = &remaining[aur_pos + 7..];
19        aur_part
20            .strip_suffix("%)")
21            .and_then(|s| s.parse::<f64>().ok())
22    });
23    (aur_suffix_pct, aur_suffix_ratio)
24}
25
26/// What: Format translated text with optional AUR suffix.
27///
28/// Inputs:
29/// - `app`: Application state
30/// - `main_text`: Translated main text
31/// - `aur_pct`: Optional AUR percentage
32/// - `aur_ratio`: Optional AUR ratio
33///
34/// Output:
35/// - Formatted text with AUR suffix if present
36fn format_with_aur_suffix(
37    app: &AppState,
38    main_text: String,
39    aur_pct: Option<u32>,
40    aur_ratio: Option<f64>,
41) -> String {
42    use crate::i18n;
43    if let Some(pct) = aur_pct {
44        format!(
45            "{}{}",
46            main_text,
47            i18n::t_fmt1(app, "app.arch_status.aur_today_suffix", pct)
48        )
49    } else if let Some(ratio) = aur_ratio {
50        format!("{main_text} (AUR: {ratio:.1}%)")
51    } else {
52        main_text
53    }
54}
55
56/// What: Parse and translate service status pattern with ratio.
57///
58/// Inputs:
59/// - `app`: Application state
60/// - `text`: Full status text
61/// - `pattern`: Pattern to match (e.g., " outage (see status) — ")
62/// - `translation_key`: Translation key for the pattern
63///
64/// Output:
65/// - `Some(translated_text)` if pattern matches, `None` otherwise
66fn translate_service_pattern(
67    app: &AppState,
68    text: &str,
69    pattern: &str,
70    translation_key: &str,
71) -> Option<String> {
72    use crate::i18n;
73    if !text.contains(pattern) || !text.contains(" today: ") || text.contains(" — AUR today: ") {
74        return None;
75    }
76
77    let pattern_pos = text.find(pattern)?;
78    let service_name = &text[..pattern_pos];
79    let today_pos = text.find(" today: ")?;
80    let after_today = &text[today_pos + 8..];
81    let pct_pos = after_today.find('%')?;
82    let ratio_str = &after_today[..pct_pos];
83    let ratio: f64 = ratio_str.parse().ok()?;
84    let ratio_formatted = format!("{ratio:.1}");
85
86    let remaining = &after_today[pct_pos + 1..];
87    let (aur_pct, aur_ratio) = extract_aur_suffix(remaining);
88    let main_text = i18n::t_fmt(
89        app,
90        translation_key,
91        &[&service_name, &service_name, &ratio_formatted],
92    );
93    Some(format_with_aur_suffix(app, main_text, aur_pct, aur_ratio))
94}
95
96/// What: Translate simple status text patterns using lookup table.
97///
98/// Inputs:
99/// - `app`: Application state
100/// - `base_text`: Base text to translate
101///
102/// Output:
103/// - `Some(translated_text)` if pattern matches, `None` otherwise
104fn translate_simple_pattern(app: &AppState, base_text: &str) -> Option<String> {
105    use crate::i18n;
106    let translation_key = match base_text {
107        "Status: AUR Down" => Some("app.arch_status.aur_down"),
108        "Some Arch systems down (see status)" => Some("app.arch_status.some_systems_down"),
109        "Arch systems degraded (see status)" => Some("app.arch_status.systems_degraded"),
110        "Arch systems nominal" => Some("app.arch_status.systems_nominal"),
111        "All systems operational" => Some("app.arch_status.all_systems_operational"),
112        "AUR outage (see status)" => Some("app.arch_status.aur_outage"),
113        "AUR partial outage" => Some("app.arch_status.aur_partial_outage"),
114        "AUR RPC degraded" => Some("app.arch_status.aur_rpc_degraded"),
115        "AUR maintenance ongoing" => Some("app.arch_status.aur_maintenance_ongoing"),
116        "AUR issues detected (see status)" => Some("app.arch_status.aur_issues_detected"),
117        "AUR degraded (see status)" => Some("app.arch_status.aur_degraded"),
118        _ => None,
119    };
120    translation_key.map(|key| i18n::t(app, key))
121}
122
123/// What: Translate "Arch systems nominal — {service} today: {ratio}%" pattern.
124///
125/// Inputs:
126/// - `app`: Application state
127/// - `text`: Full status text
128///
129/// Output:
130/// - `Some(translated_text)` if pattern matches, `None` otherwise
131fn translate_nominal_with_service(app: &AppState, text: &str) -> Option<String> {
132    use crate::i18n;
133    if !text.starts_with("Arch systems nominal — ")
134        || !text.contains(" today: ")
135        || text.contains(" — AUR today: ")
136    {
137        return None;
138    }
139
140    let today_pos = text.find(" today: ")?;
141    let service_part = &text[24..today_pos]; // "Arch systems nominal — " is 24 chars
142    let after_today = &text[today_pos + 8..];
143    let pct_pos = after_today.find('%')?;
144    let ratio_str = &after_today[..pct_pos];
145    let ratio: f64 = ratio_str.parse().ok()?;
146    let ratio_formatted = format!("{ratio:.1}");
147
148    let remaining = &after_today[pct_pos + 1..];
149    let (aur_pct, aur_ratio) = extract_aur_suffix(remaining);
150    let main_text = i18n::t_fmt(
151        app,
152        "app.arch_status.systems_nominal_with_service",
153        &[&service_part, &ratio_formatted],
154    );
155    Some(format_with_aur_suffix(app, main_text, aur_pct, aur_ratio))
156}
157
158/// What: Translate Arch systems status text from English to the current locale.
159///
160/// Inputs:
161/// - `app`: Application state containing translations
162/// - `text`: English status text to translate
163///
164/// Output:
165/// - Translated status text, or original text if translation not found
166///
167/// Details:
168/// - Parses English status messages and maps them to translation keys
169/// - Handles dynamic parts like percentages and service names
170/// - Falls back to original text if pattern doesn't match
171#[must_use]
172pub fn translate_status_text(app: &AppState, text: &str) -> String {
173    // Check for complex patterns with service names first (before extracting AUR suffix)
174    // These patterns have their own "today: {ratio}%" that's not the AUR suffix
175    if let Some(result) = translate_service_pattern(
176        app,
177        text,
178        " outage (see status) — ",
179        "app.arch_status.service_outage",
180    ) {
181        return result;
182    }
183    if let Some(result) = translate_service_pattern(
184        app,
185        text,
186        " degraded (see status) — ",
187        "app.arch_status.service_degraded",
188    ) {
189        return result;
190    }
191    if let Some(result) = translate_service_pattern(
192        app,
193        text,
194        " issues detected (see status) — ",
195        "app.arch_status.service_issues_detected",
196    ) {
197        return result;
198    }
199    if let Some(result) = translate_nominal_with_service(app, text) {
200        return result;
201    }
202
203    // Extract AUR percentage suffix if present (for simple patterns)
204    let (base_text, aur_pct) = text
205        .find(" — AUR today: ")
206        .map_or((text, None), |suffix_pos| {
207            let (base, suffix) = text.split_at(suffix_pos);
208            let pct = suffix
209                .strip_prefix(" — AUR today: ")
210                .and_then(|s| s.strip_suffix('%'))
211                .and_then(|s| s.parse::<u32>().ok());
212            (base, pct)
213        });
214
215    // Extract AUR suffix in parentheses if present
216    let (base_text, aur_ratio) =
217        base_text
218            .find(" (AUR: ")
219            .map_or((base_text, None), |suffix_pos| {
220                let (base, suffix) = base_text.split_at(suffix_pos);
221                let ratio = suffix
222                    .strip_prefix(" (AUR: ")
223                    .and_then(|s| s.strip_suffix("%)"))
224                    .and_then(|s| s.parse::<f64>().ok());
225                (base, ratio)
226            });
227
228    // Match base text patterns and translate
229    let Some(translated) = translate_simple_pattern(app, base_text) else {
230        // Pattern not recognized, return original
231        return text.to_string();
232    };
233
234    // Append AUR percentage suffix if present
235    format_with_aur_suffix(app, translated, aur_pct, aur_ratio)
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::state::AppState;
242
243    #[test]
244    /// What: Test translation of simple status messages.
245    ///
246    /// Inputs:
247    /// - English status text
248    /// - `AppState` with translations
249    ///
250    /// Output:
251    /// - Translated status text
252    ///
253    /// Details:
254    /// - Verifies basic status message translation
255    fn test_translate_simple_status() {
256        let app = AppState::default();
257        let text = "Arch systems nominal";
258        let translated = translate_status_text(&app, text);
259        // Should return translation or fallback to English
260        assert!(!translated.is_empty());
261    }
262
263    #[test]
264    /// What: Test translation of status messages with AUR percentage.
265    ///
266    /// Inputs:
267    /// - English status text with AUR percentage suffix
268    /// - `AppState` with translations
269    ///
270    /// Output:
271    /// - Translated status text with translated suffix
272    ///
273    /// Details:
274    /// - Verifies status message translation with dynamic percentage
275    fn test_translate_status_with_aur_pct() {
276        let app = AppState::default();
277        let text = "Arch systems nominal — AUR today: 97%";
278        let translated = translate_status_text(&app, text);
279        // Should return translation or fallback to English
280        assert!(!translated.is_empty());
281        // The translation should contain the percentage or be a valid translation
282        assert!(translated.contains("97") || translated.contains("AUR") || translated.len() > 10);
283    }
284
285    #[test]
286    /// What: Test translation of service degraded pattern with percentage formatting.
287    ///
288    /// Inputs:
289    /// - English status text with service degraded pattern and percentage
290    /// - `AppState` with translations
291    ///
292    /// Output:
293    /// - Translated status text with properly formatted percentage
294    ///
295    /// Details:
296    /// - Verifies that percentage is formatted with .1 precision and not showing format specifier
297    fn test_translate_service_degraded_with_percentage() {
298        let app = AppState::default();
299        let text = "Website degraded (see status) — Website today: 91.275%";
300        let translated = translate_status_text(&app, text);
301        // Should return translation or fallback to English
302        assert!(!translated.is_empty());
303        // The main bug fix: should not contain the format specifier literal {:.1}
304        // This was the bug where {:.1}% was showing up instead of the actual percentage
305        // We need to check for the literal string "{:.1}" which clippy flags as a format specifier,
306        // but this is intentional - we're testing that translations don't contain this literal.
307        #[allow(clippy::literal_string_with_formatting_args)]
308        let format_spec = "{:.1}";
309        assert!(
310            !translated.contains(format_spec),
311            "Translation should not contain format specifier {format_spec}, got: {translated}"
312        );
313    }
314}