@@ -613,6 +613,60 @@ impl Codec {
613613 Ok ( ( ) )
614614 }
615615
616+ /// Validates plural completeness per CLDR category sets for each locale.
617+ ///
618+ /// For each plural entry in each resource, checks that all required plural
619+ /// categories for the language are present. Returns a Validation error with
620+ /// aggregated details if any are missing.
621+ pub fn validate_plurals ( & self ) -> Result < ( ) , Error > {
622+ use crate :: plural_rules:: collect_resource_plural_issues;
623+
624+ let mut reports = Vec :: new ( ) ;
625+ for res in & self . resources {
626+ reports. extend ( collect_resource_plural_issues ( res) ) ;
627+ }
628+
629+ if reports. is_empty ( ) {
630+ return Ok ( ( ) ) ;
631+ }
632+
633+ // Fold into an Error message for the validating API
634+ let mut lines = Vec :: new ( ) ;
635+ for r in reports {
636+ let miss: Vec < String > = r. missing . iter ( ) . map ( |k| format ! ( "{:?}" , k) ) . collect ( ) ;
637+ let have: Vec < String > = r. have . iter ( ) . map ( |k| format ! ( "{:?}" , k) ) . collect ( ) ;
638+ lines. push ( format ! (
639+ "lang='{}' key='{}': missing plural categories: [{}] (have: [{}])" ,
640+ r. language,
641+ r. key,
642+ miss. join( ", " ) ,
643+ have. join( ", " )
644+ ) ) ;
645+ }
646+ Err ( Error :: validation_error ( lines. join ( "\n " ) ) )
647+ }
648+
649+ /// Collects non-fatal plural validation reports across all resources.
650+ pub fn collect_plural_issues ( & self ) -> Vec < crate :: plural_rules:: PluralValidationReport > {
651+ use crate :: plural_rules:: collect_resource_plural_issues;
652+ let mut reports = Vec :: new ( ) ;
653+ for res in & self . resources {
654+ reports. extend ( collect_resource_plural_issues ( res) ) ;
655+ }
656+ reports
657+ }
658+
659+ /// Autofix: fill missing plural categories using 'other' and mark entries as NeedsReview.
660+ /// Returns total categories added across all resources.
661+ pub fn autofix_fill_missing_from_other ( & mut self ) -> usize {
662+ use crate :: plural_rules:: autofix_fill_missing_from_other_resource;
663+ let mut total = 0usize ;
664+ for res in & mut self . resources {
665+ total += autofix_fill_missing_from_other_resource ( res) ;
666+ }
667+ total
668+ }
669+
616670 /// Cleans up resources by removing empty resources and entries.
617671 pub fn clean_up_resources ( & mut self ) {
618672 self . resources
@@ -962,7 +1016,18 @@ impl Codec {
9621016 path : P ,
9631017 format_type : FormatType ,
9641018 ) -> Result < ( ) , Error > {
965- let language = crate :: converter:: infer_language_from_path ( & path, & format_type) ?;
1019+ let mut language = crate :: converter:: infer_language_from_path ( & path, & format_type) ?;
1020+ // Fallback to explicitly provided language if inference failed
1021+ if language. is_none ( ) {
1022+ match & format_type {
1023+ FormatType :: Strings ( lang_opt) | FormatType :: AndroidStrings ( lang_opt) => {
1024+ if let Some ( l) = lang_opt {
1025+ language = Some ( l. clone ( ) ) ;
1026+ }
1027+ }
1028+ _ => { }
1029+ }
1030+ }
9661031
9671032 let domain = path
9681033 . as_ref ( )
0 commit comments