Skip to content

Commit 5f65590

Browse files
committed
feat(lib): add autofix helper to fill missing plural categories from 'other' and mark NeedsReview
- Resource-level: autofix_fill_missing_from_other_resource - Codec-level: autofix_fill_missing_from_other - Skips DoNotTranslate entries; sets NeedsReview when autofilling - Per-language counts of entries with missing plural categories - Total missing plural categories per language - Resource-level: autofix_fill_missing_from_other_resource - Codec-level: autofix_fill_missing_from_other - Skips DoNotTranslate entries; sets NeedsReview when autofilling
1 parent cf225b5 commit 5f65590

3 files changed

Lines changed: 88 additions & 3 deletions

File tree

langcodec/src/codec.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,17 @@ impl Codec {
656656
reports
657657
}
658658

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+
659670
/// Cleans up resources by removing empty resources and entries.
660671
pub fn clean_up_resources(&mut self) {
661672
self.resources

langcodec/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ pub use crate::{
160160
formats::FormatType,
161161
placeholder::{extract_placeholders, normalize_placeholders, signature},
162162
plural_rules::{
163-
collect_resource_plural_issues, required_categories_for_str, validate_resource_plurals,
164-
PluralValidationReport,
163+
autofix_fill_missing_from_other_resource, collect_resource_plural_issues,
164+
required_categories_for_str, validate_resource_plurals, PluralValidationReport,
165165
},
166166
types::{
167167
ConflictStrategy, Entry, EntryStatus, Metadata, Plural, PluralCategory, Resource,

langcodec/src/plural_rules.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use unic_langid::LanguageIdentifier;
44

55
use crate::{
66
error::Error,
7-
types::{Plural, PluralCategory, Resource, Translation},
7+
types::{EntryStatus, Plural, PluralCategory, Resource, Translation},
88
};
99

1010
use serde::Serialize;
@@ -172,6 +172,42 @@ pub fn validate_resource_plurals(resource: &Resource) -> Result<(), Error> {
172172
)))
173173
}
174174

175+
/// Autofix: for each plural entry, fill missing categories using the 'other' value if available.
176+
/// Marks the entry status as NeedsReview when any categories are added. Skips DoNotTranslate.
177+
///
178+
/// Returns the number of categories added across the resource.
179+
pub fn autofix_fill_missing_from_other_resource(resource: &mut Resource) -> usize {
180+
let Some(lang_id) = resource.parse_language_identifier() else { return 0; };
181+
let mut added = 0usize;
182+
for entry in &mut resource.entries {
183+
// Skip entries that shouldn't be translated
184+
if matches!(entry.status, EntryStatus::DoNotTranslate) {
185+
continue;
186+
}
187+
if let Translation::Plural(plural) = &mut entry.value {
188+
let missing = missing_categories_for_plural(&lang_id, plural);
189+
if missing.is_empty() {
190+
continue;
191+
}
192+
// Need an 'other' form to duplicate
193+
if let Some(other_val) = plural.forms.get(&PluralCategory::Other).cloned() {
194+
for cat in missing {
195+
// Insert only if still missing (avoid race with duplicates)
196+
if !plural.forms.contains_key(&cat) {
197+
plural.forms.insert(cat, other_val.clone());
198+
added += 1;
199+
}
200+
}
201+
// Mark as needs review if anything was added
202+
if added > 0 && !matches!(entry.status, EntryStatus::NeedsReview) {
203+
entry.status = EntryStatus::NeedsReview;
204+
}
205+
}
206+
}
207+
}
208+
added
209+
}
210+
175211
#[cfg(test)]
176212
mod tests {
177213
use super::*;
@@ -256,4 +292,42 @@ mod tests {
256292
assert!(r.missing.contains(&PluralCategory::One));
257293
assert!(r.have.contains(&PluralCategory::Other));
258294
}
295+
296+
#[test]
297+
fn test_autofix_fill_missing_from_other_resource() {
298+
// English requires one/other; provide only other and autofix should add one
299+
let mut resource = Resource {
300+
metadata: Metadata {
301+
language: "en".into(),
302+
domain: String::new(),
303+
custom: Default::default(),
304+
},
305+
entries: vec![Entry {
306+
id: "apples".into(),
307+
value: Translation::Plural(Plural::new(
308+
"apples",
309+
vec![(PluralCategory::Other, "%d apples".to_string())].into_iter(),
310+
)
311+
.unwrap()),
312+
comment: None,
313+
status: EntryStatus::Translated,
314+
custom: Default::default(),
315+
}],
316+
};
317+
318+
let added = autofix_fill_missing_from_other_resource(&mut resource);
319+
assert!(added >= 1);
320+
let entry = &resource.entries[0];
321+
// Should now contain One and Other
322+
if let Translation::Plural(p) = &entry.value {
323+
assert!(p.forms.contains_key(&PluralCategory::One));
324+
assert_eq!(
325+
p.forms.get(&PluralCategory::One).unwrap(),
326+
p.forms.get(&PluralCategory::Other).unwrap()
327+
);
328+
} else {
329+
panic!("expected plural");
330+
}
331+
assert!(matches!(entry.status, EntryStatus::NeedsReview));
332+
}
259333
}

0 commit comments

Comments
 (0)