Skip to content

Commit b7ad5d2

Browse files
committed
fix(core): preserve metadata-only xcstrings entries on edit set
1 parent 2a14747 commit b7ad5d2

2 files changed

Lines changed: 125 additions & 24 deletions

File tree

langcodec-cli/tests/edit_cli_tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,59 @@ fn test_edit_set_xcstrings_preserves_non_translatable_entry_without_localization
575575
"Game Updated"
576576
);
577577
}
578+
579+
#[test]
580+
fn test_edit_set_xcstrings_preserves_translatable_metadata_only_entry() {
581+
let temp_dir = TempDir::new().unwrap();
582+
let input_file = temp_dir.path().join("Localizable.xcstrings");
583+
let initial = r#"{
584+
"sourceLanguage": "en",
585+
"version": "1.0",
586+
"strings": {
587+
"The following rewards have been sent to your backpack.": {
588+
"comment": "Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack.",
589+
"isCommentAutoGenerated": true
590+
}
591+
}
592+
}
593+
"#;
594+
fs::write(&input_file, initial).unwrap();
595+
596+
let out = langcodec_cmd()
597+
.args([
598+
"edit",
599+
"set",
600+
"-i",
601+
input_file.to_str().unwrap(),
602+
"--lang",
603+
"en",
604+
"-k",
605+
"game_title",
606+
"-v",
607+
"Game Updated",
608+
])
609+
.output()
610+
.unwrap();
611+
assert!(
612+
out.status.success(),
613+
"stderr: {}",
614+
String::from_utf8_lossy(&out.stderr)
615+
);
616+
617+
let content = fs::read_to_string(&input_file).unwrap();
618+
let payload: serde_json::Value = serde_json::from_str(&content).unwrap();
619+
620+
let reward_key = "The following rewards have been sent to your backpack.";
621+
assert_eq!(
622+
payload["strings"][reward_key]["comment"],
623+
"Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack."
624+
);
625+
assert_eq!(
626+
payload["strings"][reward_key]["isCommentAutoGenerated"],
627+
serde_json::Value::Bool(true)
628+
);
629+
assert_eq!(
630+
payload["strings"]["game_title"]["localizations"]["en"]["stringUnit"]["value"],
631+
"Game Updated"
632+
);
633+
}

langcodec/src/formats/xcstrings.rs

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ impl TryFrom<Format> for Vec<Resource> {
118118
// Key: Language code, e.g. "en", "fr", etc.
119119
// Value: Resource containing all items for that language
120120
let mut resource_map = HashMap::<String, Resource>::new();
121+
let mut pending_empty_translatable_entries =
122+
Vec::<(String, Option<String>, HashMap<String, String>)>::new();
121123

122124
let mut custom_meta = HashMap::<String, String>::new();
123125
custom_meta.insert(String::from("source_language"), format.source_language);
@@ -137,30 +139,8 @@ impl TryFrom<Format> for Vec<Resource> {
137139

138140
if item.localizations.is_empty() {
139141
if item.should_translate.unwrap_or(true) {
140-
// If the item is empty and should be translated, add a new entry for each language
141-
//
142-
// This method requires that all languages are already present in the resource map, in
143-
// other words, the translated items must be presented above the untranslated items.
144-
let lang_codes = resource_map.keys().cloned().collect::<Vec<_>>();
145-
for lang_code in lang_codes {
146-
resource_map
147-
.entry(lang_code.clone())
148-
.or_insert(Resource {
149-
metadata: Metadata {
150-
language: lang_code.clone(),
151-
domain: String::default(),
152-
custom: custom_meta.clone(),
153-
},
154-
entries: Vec::new(),
155-
})
156-
.add_entry(Entry {
157-
id: id.clone(),
158-
value: Translation::Empty,
159-
comment: item.comment.clone(),
160-
status: EntryStatus::New,
161-
custom: custom.clone(),
162-
});
163-
}
142+
// Defer empty translatable entries until language buckets are known.
143+
pending_empty_translatable_entries.push((id.clone(), item.comment, custom));
164144
} else {
165145
// Preserve non-translatable metadata-only entries by attaching an empty
166146
// do-not-translate entry to source language.
@@ -213,6 +193,39 @@ impl TryFrom<Format> for Vec<Resource> {
213193
}
214194
}
215195

196+
if !pending_empty_translatable_entries.is_empty() {
197+
let mut lang_codes = resource_map.keys().cloned().collect::<Vec<_>>();
198+
if lang_codes.is_empty() {
199+
lang_codes.push(
200+
custom_meta
201+
.get("source_language")
202+
.cloned()
203+
.unwrap_or_else(|| "en".to_string()),
204+
);
205+
}
206+
for (id, comment, custom) in pending_empty_translatable_entries {
207+
for lang_code in &lang_codes {
208+
resource_map
209+
.entry(lang_code.clone())
210+
.or_insert(Resource {
211+
metadata: Metadata {
212+
language: lang_code.clone(),
213+
domain: String::default(),
214+
custom: custom_meta.clone(),
215+
},
216+
entries: Vec::new(),
217+
})
218+
.add_entry(Entry {
219+
id: id.clone(),
220+
value: Translation::Empty,
221+
comment: comment.clone(),
222+
status: EntryStatus::New,
223+
custom: custom.clone(),
224+
});
225+
}
226+
}
227+
}
228+
216229
Ok(resource_map.into_values().collect())
217230
}
218231
}
@@ -625,4 +638,36 @@ mod tests {
625638
assert_eq!(carrom.should_translate, Some(false));
626639
assert_eq!(carrom.is_comment_auto_generated, Some(true));
627640
}
641+
642+
#[test]
643+
fn test_translatable_metadata_only_item_without_localizations_is_preserved() {
644+
let mut strings = HashMap::new();
645+
strings.insert(
646+
"The following rewards have been sent to your backpack.".to_string(),
647+
Item {
648+
localizations: HashMap::new(),
649+
comment: Some("Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack.".to_string()),
650+
extraction_state: None,
651+
should_translate: None,
652+
is_comment_auto_generated: Some(true),
653+
},
654+
);
655+
let format = Format {
656+
source_language: "en".to_string(),
657+
version: "1.0".to_string(),
658+
strings,
659+
};
660+
661+
let resources = Vec::<Resource>::try_from(format).expect("resources from xcstrings");
662+
let roundtrip = Format::try_from(resources).expect("xcstrings from resources");
663+
let key = "The following rewards have been sent to your backpack.";
664+
let item = roundtrip.strings.get(key).expect("reward tip item exists");
665+
666+
assert!(item.localizations.is_empty());
667+
assert_eq!(
668+
item.comment.as_deref(),
669+
Some("Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack.")
670+
);
671+
assert_eq!(item.is_comment_auto_generated, Some(true));
672+
}
628673
}

0 commit comments

Comments
 (0)