Skip to main content

lingora_cli/
app.rs

1use std::{fs, io, path::Path};
2
3use lingora_core::prelude::*;
4
5use crate::{args::CliArgs, error::CliError};
6
7/// High-level application context for `lingora-cli`.
8///
9/// `App` owns:
10/// - the loaded configuration (`LingoraToml`)
11/// - the result of the full audit (`AuditResult`)
12///
13/// This struct acts as the bridge between parsed CLI arguments, core engine execution,
14/// and output/rendering logic.
15pub struct App {
16    settings: LingoraToml,
17    audit_result: AuditResult,
18}
19
20impl App {
21    /// Renders the audit report using `AnalysisRenderer` to the given writer.
22    ///
23    /// This is the primary way to produce the human-readable report in standard mode.
24    /// The renderer groups issues hierarchically (workspace → canonical → primaries → variants → orphans).
25    ///
26    /// # Errors
27    /// Returns `CliError::Io` if writing to the output fails.
28    pub fn output_audit_report<W: io::Write>(&self, out: &mut W) -> Result<(), CliError> {
29        let renderer = AnalysisRenderer::new(&self.audit_result);
30        renderer.render(out)?;
31        Ok(())
32    }
33
34    /// Generates `dioxus_i18n::I18nConfig` Rust code and writes it to the specified file.
35    ///
36    /// - Uses `DioxusI18nConfigRenderer` with the current `settings` and `workspace`
37    /// - Computes relative paths from the **parent directory** of the target file
38    ///   (so `include_str!` or `PathBuf::from` paths are correct relative to the generated file)
39    /// - Creates the file (fails if it already exists — use `create_new` for safety)
40    ///
41    /// # Arguments
42    /// * `path` — destination path (e.g. `src/i18n_config.rs`)
43    ///
44    /// # Errors
45    /// - `CliError::Io` on file creation/write failure
46    /// - Propagates any renderer errors (rare, usually path resolution)
47    pub fn output_dioxus_i18n_config(&self, path: &Path) -> Result<(), CliError> {
48        let base_path = path.parent();
49        let mut file = fs::File::create_new(path)?;
50        let workspace = self.audit_result.workspace();
51        let renderer = DioxusI18nConfigRenderer::new(&self.settings, workspace, base_path);
52        renderer.render(&mut file)?;
53        Ok(())
54    }
55
56    /// Returns `Ok(())` if the audit found **no issues**, otherwise returns
57    /// `Err(CliError::IntegrityErrorsDetected)`.
58    ///
59    /// Determines the final exit code in `main`:
60    /// - `0` → everything is perfect
61    /// - non-zero → issues were found (even if parsing/execution succeeded)
62    pub fn exit_status(&self) -> Result<(), CliError> {
63        if self.audit_result.is_ok() {
64            Ok(())
65        } else {
66            Err(CliError::IntegrityErrorsDetected)
67        }
68    }
69}
70
71impl TryFrom<&LingoraToml> for App {
72    type Error = CliError;
73
74    fn try_from(settings: &LingoraToml) -> Result<Self, Self::Error> {
75        let settings = settings.clone();
76
77        let engine = AuditEngine::try_from(&settings)?;
78        let audit_result = engine.run()?;
79
80        Ok(Self {
81            settings,
82            audit_result,
83        })
84    }
85}
86
87impl TryFrom<&CliArgs> for App {
88    type Error = CliError;
89
90    fn try_from(value: &CliArgs) -> Result<Self, Self::Error> {
91        let settings = LingoraToml::try_from(value.core_args())?;
92        Self::try_from(&settings)
93    }
94}
95
96// #[cfg(test)]
97// mod test {
98//     use std::{fs, str::FromStr};
99
100//     use tempfile::TempPath;
101
102//     use super::*;
103
104//     fn do_output_analysis(settings: &LingoraToml) -> String {
105//         let out_buffer = Vec::new();
106//         let mut out = io::BufWriter::new(out_buffer);
107
108//         let app = App::try_from(settings).unwrap();
109
110//         app.output_audit_report(&mut out).unwrap();
111
112//         let bytes = out.buffer();
113//         String::from_utf8_lossy(bytes).to_string()
114//     }
115
116//     #[test]
117//     fn app_will_output_checks_when_no_errors() {
118//         let settings = LingoraToml::from_str(
119//             r#"
120// [lingora]
121// reference = "tests/data/cross_check/reference_matching.ftl"
122// targets = ["tests/data/cross_check/target_matching.ftl"]
123// "#,
124//         )
125//         .unwrap();
126
127//         let result = do_output_analysis(&settings);
128//         insta::assert_snapshot!(result, @r"
129//         Reference: tests/data/cross_check/reference_matching.ftl - Ok
130//         Target: tests/data/cross_check/target_matching.ftl - Ok
131//         ");
132//     }
133
134//     #[test]
135//     fn app_will_output_checks_when_errors() {
136//         let settings = LingoraToml::from_str(
137//             r#"
138// [lingora]
139// fluent_sources = ["tests/data/cross_check/reference_missing.ftl"
140// targets = ["tests/data/cross_check/target_redundant.ftl"]
141// "#,
142//         )
143//         .unwrap();
144
145//         let result = do_output_analysis(&settings);
146//         insta::assert_snapshot!(result, @r"
147//         Reference: tests/data/cross_check/reference_missing.ftl - Ok
148//         Target: tests/data/cross_check/target_redundant.ftl
149//             Missing translation: -missing-term
150//                                  missing-message
151//             Superfluous translation: -superfluous-term
152//                                      superfluous-message
153//         ");
154//     }
155
156//     fn create_temp_filepath() -> TempPath {
157//         let file = tempfile::NamedTempFile::new().unwrap();
158
159//         let temp_path = file.into_temp_path();
160//         let path = temp_path.to_path_buf();
161//         fs::remove_file(&path).expect("temporary file must be deleted");
162
163//         temp_path
164//     }
165
166//     #[test]
167//     fn will_output_dioxus_i18n_config_for_auto() {
168//         let settings = LingoraToml::from_str(
169//             r#"
170// [lingora]
171// root = "tests/data/i18n"
172// reference = "tests/data/i18n/en/en-GB.ftl"
173// [dioxus_i18n]
174// with_locale = "auto"
175// fallback = "en-GB"
176// "#,
177//         )
178//         .unwrap();
179
180//         let path = create_temp_filepath();
181//         let app = App::try_from(&settings).unwrap();
182//         app.output_dioxus_i18n_config(&path).unwrap();
183
184//         let content = fs::read_to_string(path).unwrap();
185//         insta::assert_snapshot!(content, @r#"
186//         use dioxus_i18n::{prelude::*, *};
187//         use unic_langid::{langid, LanguageIdentifier};
188//         use std::path::PathBuf;
189
190//         pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
191//             I18nConfig::new(initial_language)
192//                 .with_auto_locales(PathBuf::from("tests/data/i18n"))
193//                 .with_fallback(langid!("en-GB"))
194//         }
195//         "#);
196//     }
197
198//     #[test]
199//     #[cfg(not(target_os = "windows"))]
200//     fn will_output_dioxus_i18n_config_for_pathbuf() {
201//         let settings = LingoraToml::from_str(
202//             r#"
203// [lingora]
204// root = "tests/data/i18n"
205// reference = "tests/data/i18n/en/en-GB.ftl"
206// [dioxus_i18n]
207// with_locale = "pathbuf"
208// fallback = "en-GB"
209// "#,
210//         )
211//         .unwrap();
212
213//         let path = create_temp_filepath();
214//         let app = App::try_from(&settings).unwrap();
215//         app.output_dioxus_i18n_config(&path).unwrap();
216
217//         let content = fs::read_to_string(path).unwrap();
218//         insta::assert_snapshot!(content, @r#"
219//         use dioxus_i18n::{prelude::*, *};
220//         use unic_langid::{langid, LanguageIdentifier};
221//         use std::path::PathBuf;
222
223//         pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
224//             I18nConfig::new(initial_language)
225//                 .with_locale((
226//                     langid!("en-AU"),
227//                     PathBuf::from("tests/data/i18n/en/en-AU.ftl")
228//                 ))
229//                 .with_locale((
230//                     langid!("en-GB"),
231//                     PathBuf::from("tests/data/i18n/en/en-GB.ftl")
232//                 ))
233//                 .with_locale((
234//                     langid!("en"),
235//                     PathBuf::from("tests/data/i18n/en/en.ftl")
236//                 ))
237//                 .with_locale((
238//                     langid!("it-IT"),
239//                     PathBuf::from("tests/data/i18n/it/it-IT.ftl")
240//                 ))
241//                 .with_fallback(langid!("en-GB"))
242//         }
243//         "#);
244//     }
245
246//     #[test]
247//     #[cfg(not(target_os = "windows"))]
248//     fn will_output_dioxus_i18n_config_for_include_str() {
249//         let settings = LingoraToml::from_str(
250//             r#"
251// [lingora]
252// fluent_sources = ["tests/data/i18n"]
253// canonical = "en-GB"
254// primaries = ["it-IT"]
255// [dioxus_i18n]
256// config_inclusion = "includestr"
257// "#,
258//         )
259//         .unwrap();
260
261//         let path = create_temp_filepath();
262//         let app = App::try_from(&settings).unwrap();
263//         app.output_dioxus_i18n_config(&path).unwrap();
264
265//         let content = fs::read_to_string(path).unwrap();
266//         insta::assert_snapshot!(content, @r#"
267//         use dioxus_i18n::{prelude::*, *};
268//         use unic_langid::{langid, LanguageIdentifier};
269
270//         pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
271//             I18nConfig::new(initial_language)
272//                 .with_locale((
273//                     langid!("en-AU"),
274//                     include_str!("tests/data/i18n/en/en-AU.ftl")
275//                 ))
276//                 .with_locale((
277//                     langid!("en-GB"),
278//                     include_str!("tests/data/i18n/en/en-GB.ftl")
279//                 ))
280//                 .with_locale((
281//                     langid!("en"),
282//                     include_str!("tests/data/i18n/en/en.ftl")
283//                 ))
284//                 .with_locale((
285//                     langid!("it-IT"),
286//                     include_str!("tests/data/i18n/it/it-IT.ftl")
287//                 ))
288//                 .with_fallback(langid!("en-GB"))
289//         }
290//         "#);
291//     }
292
293//     #[test]
294//     #[cfg(not(target_os = "windows"))]
295//     fn will_output_dioxus_i18n_config_shares_for_pathbuf() {
296//         let settings = LingoraToml::from_str(
297//             r#"
298// [lingora]
299// fluent_sources = ["tests/data/i18n"]
300// canonical = "en-GB"
301// primaries = ["it-IT"]
302// [dioxus_i18n]
303// config_inclusion = "includestr"
304// "#,
305//         )
306//         .unwrap();
307
308//         let path = create_temp_filepath();
309//         let app = App::try_from(&settings).unwrap();
310//         app.output_dioxus_i18n_config(&path).unwrap();
311
312//         let content = fs::read_to_string(path).unwrap();
313//         insta::assert_snapshot!(content, @r#"
314//         use dioxus_i18n::{prelude::*, *};
315//         use unic_langid::{langid, LanguageIdentifier};
316//         use std::path::PathBuf;
317
318//         pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
319//             I18nConfig::new(initial_language)
320//                 .with_locale((
321//                     langid!("en-AU"),
322//                     PathBuf::from("tests/data/i18n/en/en-AU.ftl")
323//                 ))
324//                 .with_locale((
325//                     langid!("en-GB"),
326//                     PathBuf::from("tests/data/i18n/en/en-GB.ftl")
327//                 ))
328//                 .with_locale((
329//                     langid!("en"),
330//                     PathBuf::from("tests/data/i18n/en/en.ftl")
331//                 ))
332//                 .with_locale((
333//                     langid!("it-IT"),
334//                     PathBuf::from("tests/data/i18n/it/it-IT.ftl")
335//                 ))
336//                 .with_locale((
337//                     langid!("en-US"),
338//                     PathBuf::from("tests/data/i18n/en/en-GB.ftl")
339//                 ))
340//                 .with_locale((
341//                     langid!("it"),
342//                     PathBuf::from("tests/data/i18n/it/it-IT.ftl")
343//                 ))
344//                 .with_locale((
345//                     langid!("it-CH"),
346//                     PathBuf::from("tests/data/i18n/it/it-IT.ftl")
347//                 ))
348//                 .with_fallback(langid!("en-GB"))
349//         }
350//         "#);
351//     }
352
353//     #[test]
354//     #[cfg(not(target_os = "windows"))]
355//     fn will_output_dioxus_i18n_config_shares_for_include_str() {
356//         let settings = LingoraToml::from_str(
357//             r#"
358// [lingora]
359// fluent_sources = ["tests/data/i18n"]
360// canonical = "en-GB"
361// primaries = ["it-IT"]
362// [dioxus_i18n]
363// config_inclusion = "includestr"
364// "#,
365//         )
366//         .unwrap();
367
368//         let path = create_temp_filepath();
369//         let app = App::try_from(&settings).unwrap();
370//         app.output_dioxus_i18n_config(&path).unwrap();
371
372//         let content = fs::read_to_string(path).unwrap();
373//         insta::assert_snapshot!(content, @r#"
374//         use dioxus_i18n::{prelude::*, *};
375//         use unic_langid::{langid, LanguageIdentifier};
376
377//         pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
378//             I18nConfig::new(initial_language)
379//                 .with_locale((
380//                     langid!("en-AU"),
381//                     include_str!("tests/data/i18n/en/en-AU.ftl")
382//                 ))
383//                 .with_locale((
384//                     langid!("en-GB"),
385//                     include_str!("tests/data/i18n/en/en-GB.ftl")
386//                 ))
387//                 .with_locale((
388//                     langid!("en"),
389//                     include_str!("tests/data/i18n/en/en.ftl")
390//                 ))
391//                 .with_locale((
392//                     langid!("it-IT"),
393//                     include_str!("tests/data/i18n/it/it-IT.ftl")
394//                 ))
395//                 .with_locale((
396//                     langid!("en-US"),
397//                     include_str!("tests/data/i18n/en/en-GB.ftl")
398//                 ))
399//                 .with_locale((
400//                     langid!("it"),
401//                     include_str!("tests/data/i18n/it/it-IT.ftl")
402//                 ))
403//                 .with_locale((
404//                     langid!("it-CH"),
405//                     include_str!("tests/data/i18n/it/it-IT.ftl")
406//                 ))
407//                 .with_fallback(langid!("en-GB"))
408//         }
409//         "#);
410//     }
411// }