11use crate :: convert:: read_resources_from_any_input;
22use crate :: validation:: { validate_file_path, validate_language_code, validate_output_path} ;
33use langcodec:: { Codec , Resource , Translation , formats:: FormatType } ;
4+ use serde_json:: json;
45use std:: collections:: HashMap ;
56
67#[ derive( Debug , Clone ) ]
@@ -10,6 +11,10 @@ pub struct SyncOptions {
1011 pub output : Option < String > ,
1112 pub lang : Option < String > ,
1213 pub match_lang : Option < String > ,
14+ pub report_json : Option < String > ,
15+ pub fail_on_unmatched : bool ,
16+ pub fail_on_ambiguous : bool ,
17+ pub strict : bool ,
1318 pub dry_run : bool ,
1419}
1520
@@ -263,6 +268,39 @@ fn write_back(
263268 }
264269}
265270
271+ fn write_report (
272+ path : & str ,
273+ options : & SyncOptions ,
274+ match_lang : & str ,
275+ stats : & SyncStats ,
276+ ) -> Result < ( ) , String > {
277+ let report = json ! ( {
278+ "source" : options. source,
279+ "target" : options. target,
280+ "output" : options. output,
281+ "lang" : options. lang,
282+ "match_lang" : match_lang,
283+ "strict" : options. strict,
284+ "fail_on_unmatched" : options. fail_on_unmatched,
285+ "fail_on_ambiguous" : options. fail_on_ambiguous,
286+ "dry_run" : options. dry_run,
287+ "summary" : {
288+ "total_entries" : stats. total_entries,
289+ "updated" : stats. updated,
290+ "unchanged" : stats. unchanged,
291+ "fallback_matches" : stats. fallback_matches,
292+ "skipped_unmatched" : stats. skipped_unmatched,
293+ "skipped_missing_language" : stats. skipped_missing_language,
294+ "skipped_ambiguous_fallback" : stats. skipped_ambiguous_fallback,
295+ "skipped_type_mismatch" : stats. skipped_type_mismatch
296+ }
297+ } ) ;
298+
299+ let text = serde_json:: to_string_pretty ( & report)
300+ . map_err ( |e| format ! ( "Failed to serialize report JSON: {}" , e) ) ?;
301+ std:: fs:: write ( path, text) . map_err ( |e| format ! ( "Failed to write report JSON '{}': {}" , path, e) )
302+ }
303+
266304pub fn run_sync_command ( opts : SyncOptions ) -> Result < ( ) , String > {
267305 validate_file_path ( & opts. source ) ?;
268306 validate_file_path ( & opts. target ) ?;
@@ -275,8 +313,11 @@ pub fn run_sync_command(opts: SyncOptions) -> Result<(), String> {
275313 if let Some ( match_lang) = & opts. match_lang {
276314 validate_language_code ( match_lang) ?;
277315 }
316+ if let Some ( report_path) = & opts. report_json {
317+ validate_output_path ( report_path) ?;
318+ }
278319
279- let source_resources = read_resources_from_any_input ( & opts. source , None ) ?;
320+ let source_resources = read_resources_from_any_input ( & opts. source , None , opts . strict ) ?;
280321 let source_codec = Codec {
281322 resources : source_resources,
282323 } ;
@@ -392,6 +433,26 @@ pub fn run_sync_command(opts: SyncOptions) -> Result<(), String> {
392433 ) ;
393434 println ! ( "Skipped (type mismatch): {}" , stats. skipped_type_mismatch) ;
394435
436+ if let Some ( report_path) = & opts. report_json {
437+ write_report ( report_path, & opts, & match_lang, & stats) ?;
438+ println ! ( "Report JSON written: {}" , report_path) ;
439+ }
440+
441+ let fail_on_unmatched = opts. fail_on_unmatched || opts. strict ;
442+ let fail_on_ambiguous = opts. fail_on_ambiguous || opts. strict ;
443+ if ( fail_on_unmatched && stats. skipped_unmatched > 0 )
444+ || ( fail_on_ambiguous && stats. skipped_ambiguous_fallback > 0 )
445+ {
446+ let mut reasons = Vec :: new ( ) ;
447+ if fail_on_unmatched && stats. skipped_unmatched > 0 {
448+ reasons. push ( format ! ( "unmatched={}" , stats. skipped_unmatched) ) ;
449+ }
450+ if fail_on_ambiguous && stats. skipped_ambiguous_fallback > 0 {
451+ reasons. push ( format ! ( "ambiguous={}" , stats. skipped_ambiguous_fallback) ) ;
452+ }
453+ return Err ( format ! ( "Sync policy failure ({})" , reasons. join( ", " ) ) ) ;
454+ }
455+
395456 if opts. dry_run {
396457 println ! ( "Dry-run mode: no files were written" ) ;
397458 return Ok ( ( ) ) ;
0 commit comments