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// }