Skip to content

Commit deb2b5c

Browse files
committed
feat(core): align Xcode's xcstrings file layout
Add deterministic serialization for HashMap fields by introducing serialize_sorted_map and applying it to Format.strings and Item.localizations. Import Serializer and SerializeMap from serde and add a helper is_none_or_true to skip should_translate when it's None or true. Also adjust serde attributes on Item (default/skip for localizations, reorder fields) to produce stable, compact output. Minor test formatting tweak only.
1 parent b7ad5d2 commit deb2b5c

1 file changed

Lines changed: 32 additions & 9 deletions

File tree

langcodec/src/formats/xcstrings.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use serde::{Deserialize, Serialize};
1+
use serde::{Deserialize, Serialize, Serializer, ser::SerializeMap};
22
use std::{
33
collections::HashMap,
44
io::{BufRead, Write},
@@ -11,12 +11,28 @@ use crate::{
1111
types::{Entry, EntryStatus, Metadata, Plural, PluralCategory, Resource, Translation},
1212
};
1313

14+
fn serialize_sorted_map<S, V>(map: &HashMap<String, V>, serializer: S) -> Result<S::Ok, S::Error>
15+
where
16+
S: Serializer,
17+
V: Serialize,
18+
{
19+
let mut keys: Vec<&String> = map.keys().collect();
20+
keys.sort_unstable();
21+
22+
let mut out = serializer.serialize_map(Some(map.len()))?;
23+
for k in keys {
24+
out.serialize_entry(k, &map[k])?;
25+
}
26+
out.end()
27+
}
28+
1429
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1530
#[serde(rename_all = "camelCase")]
1631
pub struct Format {
1732
pub source_language: String,
18-
pub version: String,
33+
#[serde(serialize_with = "serialize_sorted_map")]
1934
pub strings: HashMap<String, Item>,
35+
pub version: String,
2036
}
2137

2238
impl Parser for Format {
@@ -230,20 +246,25 @@ impl TryFrom<Format> for Vec<Resource> {
230246
}
231247
}
232248

249+
fn is_none_or_true(v: &Option<bool>) -> bool {
250+
v.is_none() || *v == Some(true)
251+
}
252+
233253
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
234254
#[serde(rename_all = "camelCase")]
235255
pub struct Item {
236-
#[serde(default)]
237-
#[serde(skip_serializing_if = "HashMap::is_empty")]
238-
pub localizations: HashMap<String, Localization>,
239256
#[serde(skip_serializing_if = "Option::is_none")]
240257
pub comment: Option<String>,
241258
#[serde(skip_serializing_if = "Option::is_none")]
242-
pub extraction_state: Option<ExtractionState>,
259+
pub is_comment_auto_generated: Option<bool>,
243260
#[serde(skip_serializing_if = "Option::is_none")]
261+
pub extraction_state: Option<ExtractionState>,
262+
#[serde(skip_serializing_if = "is_none_or_true")]
244263
pub should_translate: Option<bool>,
245-
#[serde(skip_serializing_if = "Option::is_none")]
246-
pub is_comment_auto_generated: Option<bool>,
264+
#[serde(default)]
265+
#[serde(serialize_with = "serialize_sorted_map")]
266+
#[serde(skip_serializing_if = "HashMap::is_empty")]
267+
pub localizations: HashMap<String, Localization>,
247268
}
248269

249270
impl Item {
@@ -666,7 +687,9 @@ mod tests {
666687
assert!(item.localizations.is_empty());
667688
assert_eq!(
668689
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.")
690+
Some(
691+
"Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack."
692+
)
670693
);
671694
assert_eq!(item.is_comment_auto_generated, Some(true));
672695
}

0 commit comments

Comments
 (0)