1use std::{fs, io, path::Path};
2
3use lingora_core::prelude::*;
4
5use crate::{args::CliArgs, error::CliError};
6
7pub struct App {
16 settings: LingoraToml,
17 audit_result: AuditResult,
18}
19
20impl App {
21 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 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 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 let engine = AuditEngine::try_from(&settings)?;
77 let audit_result = engine.run()?;
78
79 Ok(Self {
80 settings,
81 audit_result,
82 })
83 }
84}
85
86impl TryFrom<&CliArgs> for App {
87 type Error = CliError;
88
89 fn try_from(value: &CliArgs) -> Result<Self, Self::Error> {
90 let settings = LingoraToml::try_from(value.core_args())?;
91 Self::try_from(&settings)
92 }
93}
94
95#[cfg(test)]
96mod test {
97 use std::{env, fs, str::FromStr};
98
99 use tempfile::TempPath;
100
101 use super::*;
102
103 fn do_output_analysis(settings: &LingoraToml) -> String {
104 let out_buffer = Vec::new();
105 let mut out = io::BufWriter::new(out_buffer);
106
107 let app = App::try_from(settings).unwrap();
108
109 app.output_audit_report(&mut out).unwrap();
110
111 let bytes = out.buffer();
112 String::from_utf8_lossy(bytes).to_string()
113 }
114
115 fn with_filters(f: impl FnOnce()) {
116 let mut settings = insta::Settings::clone_current();
117 let manifest_dir = regex::escape(env!("CARGO_MANIFEST_DIR"));
118 let manifest_dir = Path::new(&manifest_dir)
119 .parent()
120 .expect("require CARGO_MANIFEST_DIR parent")
121 .display()
122 .to_string();
123 settings.add_filter(&manifest_dir, "...");
124 settings.bind(f)
125 }
126
127 #[test]
128 fn app_will_output_checks_when_no_errors() {
129 let settings = LingoraToml::from_str(
130 r#"
131[lingora]
132fluent_sources = ["../core/tests/data/i18n/en", "../core/tests/data/i18n/it"]
133canonical = "en-GB"
134primaries = ["it-IT"]
135"#,
136 )
137 .unwrap();
138
139 let result = do_output_analysis(&settings);
140
141 with_filters(|| {
142 insta::assert_snapshot!(result, @r"
143 Language: en
144 Canonical: en-GB - Ok
145 Variant: en-AU - Ok
146 Language: it
147 Primary: it-IT - Ok
148 ");
149 })
150 }
151
152 #[test]
153 fn app_will_output_checks_when_errors() {
154 let settings = LingoraToml::from_str(
155 r#"
156[lingora]
157fluent_sources = ["../core/tests/data/i18n"]
158canonical = "en-GB"
159primaries = ["fr-FR", "it-IT", "sr-Cyrl-RS"]
160"#,
161 )
162 .unwrap();
163
164 let result = do_output_analysis(&settings);
165
166 with_filters(|| {
167 insta::assert_snapshot!(result, @r"
168 Language: en
169 Canonical: en-GB - Ok
170 Variant: en-AU - Ok
171 Language: fr
172 Primary: fr-FR
173 missing translation 'en'
174 missing translation 'en-AU'
175 missing translation 'en-GB'
176 Language: it
177 Primary: it-IT - Ok
178 Language: sr
179 Primary: sr-Cyrl-RS
180 missing translation 'en-GB'
181 redundant translation '-en-GB'
182 Variant: sr-Cyrl-BA - Ok
183 ");
184 });
185 }
186
187 fn create_temp_filepath() -> TempPath {
188 let file = tempfile::NamedTempFile::new().unwrap();
189
190 let temp_path = file.into_temp_path();
191 let path = temp_path.to_path_buf();
192 fs::remove_file(&path).expect("temporary file must be deleted");
193
194 temp_path
195 }
196
197 #[test]
198 fn will_output_dioxus_i18n_config_for_auto() {
199 let settings = LingoraToml::from_str(
200 r#"
201[lingora]
202fluent_sources = ["../core/tests/data/i18n_semantic"]
203canonical = "en-GB"
204primaries = ["fr-FR", "it-IT", "sr-Cyrl-RS"]
205[dioxus_i18n]
206config_inclusion = "auto"
207"#,
208 )
209 .unwrap();
210
211 let path = create_temp_filepath();
212 let app = App::try_from(&settings).unwrap();
213 app.output_dioxus_i18n_config(&path).unwrap();
214
215 let content = fs::read_to_string(path).unwrap();
216
217 with_filters(|| {
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_auto_locales(PathBuf::from(".../core/tests/data/i18n_semantic"))
226 .with_locale(langid!("en"), PathBuf::from(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl"))
227 .with_locale(langid!("it"), PathBuf::from(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl"))
228 .with_fallback(langid!("en-GB"))
229 }
230 "#);
231 });
232 }
233
234 #[test]
235 #[cfg(not(target_os = "windows"))]
236 fn will_output_dioxus_i18n_config_for_pathbuf() {
237 let settings = LingoraToml::from_str(
238 r#"
239[lingora]
240fluent_sources = ["../core/tests/data/i18n_semantic"]
241canonical = "en-GB"
242primaries = ["fr-FR", "it-IT", "sr-Cyrl-RS"]
243[dioxus_i18n]
244config_inclusion = "pathbuf"
245"#,
246 )
247 .unwrap();
248
249 let path = create_temp_filepath();
250 let app = App::try_from(&settings).unwrap();
251 app.output_dioxus_i18n_config(&path).unwrap();
252
253 let content = fs::read_to_string(path).unwrap();
254
255 with_filters(|| {
256 insta::assert_snapshot!(content, @r#"
257 use dioxus_i18n::{prelude::*, *};
258 use unic_langid::{langid, LanguageIdentifier};
259 use std::path::PathBuf;
260
261 pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
262 I18nConfig::new(initial_language)
263 .with_locale((
264 langid!("en-AU"),
265 PathBuf::from(".../core/tests/data/i18n_semantic/en/en-AU/errors.ftl")
266 ))
267 .with_locale((
268 langid!("en-GB"),
269 PathBuf::from(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl")
270 ))
271 .with_locale((
272 langid!("it-IT"),
273 PathBuf::from(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl")
274 ))
275 .with_locale(langid!("en"), PathBuf::from(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl"))
276 .with_locale(langid!("it"), PathBuf::from(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl"))
277 .with_fallback(langid!("en-GB"))
278 }
279 "#);
280 });
281 }
282
283 #[test]
284 #[cfg(not(target_os = "windows"))]
285 fn will_output_dioxus_i18n_config_for_include_str() {
286 let settings = LingoraToml::from_str(
287 r#"
288[lingora]
289fluent_sources = ["../core/tests/data/i18n_semantic"]
290canonical = "en-GB"
291primaries = ["fr-FR", "it-IT", "sr-Cyrl-RS"]
292[dioxus_i18n]
293config_inclusion = "includestr"
294"#,
295 )
296 .unwrap();
297
298 let path = create_temp_filepath();
299 let app = App::try_from(&settings).unwrap();
300 app.output_dioxus_i18n_config(&path).unwrap();
301
302 let content = fs::read_to_string(path).unwrap();
303
304 with_filters(|| {
305 insta::assert_snapshot!(content, @r#"
306 use dioxus_i18n::{prelude::*, *};
307 use unic_langid::{langid, LanguageIdentifier};
308
309
310 pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
311 I18nConfig::new(initial_language)
312 .with_locale((
313 langid!("en-AU"),
314 include_str!(".../core/tests/data/i18n_semantic/en/en-AU/errors.ftl")
315 ))
316 .with_locale((
317 langid!("en-GB"),
318 include_str!(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl")
319 ))
320 .with_locale((
321 langid!("it-IT"),
322 include_str!(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl")
323 ))
324 .with_locale(langid!("en"), include_str!(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl"))
325 .with_locale(langid!("it"), include_str!(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl"))
326 .with_fallback(langid!("en-GB"))
327 }
328 "#);
329 });
330 }
331
332 #[test]
333 #[cfg(not(target_os = "windows"))]
334 fn will_output_dioxus_i18n_config_shares_for_pathbuf() {
335 let settings = LingoraToml::from_str(
336 r#"
337[lingora]
338fluent_sources = ["../core/tests/data/i18n_semantic"]
339canonical = "en-GB"
340primaries = ["fr-FR", "it-IT", "sr-Cyrl-RS"]
341[dioxus_i18n]
342config_inclusion = "pathbuf"
343"#,
344 )
345 .unwrap();
346
347 let path = create_temp_filepath();
348 let app = App::try_from(&settings).unwrap();
349 app.output_dioxus_i18n_config(&path).unwrap();
350
351 let content = fs::read_to_string(path).unwrap();
352
353 with_filters(|| {
354 insta::assert_snapshot!(content, @r#"
355 use dioxus_i18n::{prelude::*, *};
356 use unic_langid::{langid, LanguageIdentifier};
357 use std::path::PathBuf;
358
359 pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
360 I18nConfig::new(initial_language)
361 .with_locale((
362 langid!("en-AU"),
363 PathBuf::from(".../core/tests/data/i18n_semantic/en/en-AU/errors.ftl")
364 ))
365 .with_locale((
366 langid!("en-GB"),
367 PathBuf::from(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl")
368 ))
369 .with_locale((
370 langid!("it-IT"),
371 PathBuf::from(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl")
372 ))
373 .with_locale(langid!("en"), PathBuf::from(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl"))
374 .with_locale(langid!("it"), PathBuf::from(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl"))
375 .with_fallback(langid!("en-GB"))
376 }
377 "#);
378 });
379 }
380
381 #[test]
382 #[cfg(not(target_os = "windows"))]
383 fn will_output_dioxus_i18n_config_shares_for_include_str() {
384 let settings = LingoraToml::from_str(
385 r#"
386[lingora]
387fluent_sources = ["../core/tests/data/i18n_semantic"]
388canonical = "en-GB"
389primaries = ["fr-FR", "it-IT", "sr-Cyrl-RS"]
390[dioxus_i18n]
391config_inclusion = "includestr"
392"#,
393 )
394 .unwrap();
395
396 let path = create_temp_filepath();
397 let app = App::try_from(&settings).unwrap();
398 app.output_dioxus_i18n_config(&path).unwrap();
399
400 let content = fs::read_to_string(path).unwrap();
401
402 with_filters(|| {
403 insta::assert_snapshot!(content, @r#"
404 use dioxus_i18n::{prelude::*, *};
405 use unic_langid::{langid, LanguageIdentifier};
406
407
408 pub fn config(initial_language: LanguageIdentifier) -> I18nConfig {
409 I18nConfig::new(initial_language)
410 .with_locale((
411 langid!("en-AU"),
412 include_str!(".../core/tests/data/i18n_semantic/en/en-AU/errors.ftl")
413 ))
414 .with_locale((
415 langid!("en-GB"),
416 include_str!(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl")
417 ))
418 .with_locale((
419 langid!("it-IT"),
420 include_str!(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl")
421 ))
422 .with_locale(langid!("en"), include_str!(".../core/tests/data/i18n_semantic/en/en-GB/errors.ftl"))
423 .with_locale(langid!("it"), include_str!(".../core/tests/data/i18n_semantic/it/it-IT/errors.ftl"))
424 .with_fallback(langid!("en-GB"))
425 }
426 "#);
427 });
428 }
429}