Skip to content

Commit dd28a65

Browse files
committed
Move sync/diff logic into lib with strict read APIs
1 parent 186e370 commit dd28a65

11 files changed

Lines changed: 1234 additions & 475 deletions

File tree

langcodec-cli/src/convert.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::formats::{self, parse_custom_format};
22
use crate::transformers::custom_format_to_resource;
33
use crate::validation::{self, validate_custom_format_file};
44

5-
use langcodec::{Codec, convert_auto, formats::FormatType};
5+
use langcodec::{Codec, ReadOptions, convert_auto, formats::FormatType};
66
use std::fs::File;
77
use std::io::BufWriter;
88

@@ -497,7 +497,11 @@ pub fn read_resources_from_any_input(
497497
if let Some(std_fmt) = maybe_std {
498498
let mut codec = Codec::new();
499499
codec
500-
.read_file_by_type(input, std_fmt)
500+
.read_file_by_type_with_options(
501+
input,
502+
std_fmt,
503+
&ReadOptions::new().with_strict(true),
504+
)
501505
.map_err(|e| format!("Failed to read input with explicit format: {}", e))?;
502506
return Ok(codec.resources);
503507
}
@@ -515,7 +519,7 @@ pub fn read_resources_from_any_input(
515519
{
516520
let mut codec = Codec::new();
517521
codec
518-
.read_file_by_extension(input, None)
522+
.read_file_by_extension_with_options(input, &ReadOptions::new().with_strict(true))
519523
.map_err(|e| format!("Failed to read input: {}", e))?;
520524
return Ok(codec.resources);
521525
}

langcodec-cli/src/debug.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::formats::parse_custom_format;
22
use crate::transformers::custom_format_to_resource;
33

4-
use langcodec::{Codec, Plural, Translation};
4+
use langcodec::{Codec, Plural, ReadOptions, Translation};
55
use std::fs::File;
66
use std::io::{self, Write};
77

@@ -24,7 +24,12 @@ pub fn run_debug_command(
2424
eprintln!("Error reading {}: {}", input, e);
2525
std::process::exit(1);
2626
}
27-
} else if let Err(e) = codec.read_file_by_extension(&input, lang.clone()) {
27+
} else if let Err(e) = codec.read_file_by_extension_with_options(
28+
&input,
29+
&ReadOptions::new()
30+
.with_language_hint(lang.clone())
31+
.with_strict(true),
32+
) {
2833
eprintln!("❌ Error reading input file");
2934
eprintln!("Error reading {}: {}", input, e);
3035
std::process::exit(1);

langcodec-cli/src/diff.rs

Lines changed: 29 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use crate::convert::read_resources_from_any_input;
22
use crate::validation::{validate_file_path, validate_language_code, validate_output_path};
3-
use langcodec::{Resource, Translation};
4-
use serde_json::json;
5-
use std::collections::{BTreeMap, BTreeSet};
3+
use langcodec::{DiffOptions as LibDiffOptions, DiffReport, Translation, diff_resources};
64

75
#[derive(Debug, Clone)]
86
pub struct DiffOptions {
@@ -14,45 +12,6 @@ pub struct DiffOptions {
1412
pub strict: bool,
1513
}
1614

17-
#[derive(Debug, Clone)]
18-
struct ChangedItem {
19-
key: String,
20-
source: Translation,
21-
target: Translation,
22-
}
23-
24-
#[derive(Debug, Clone, Default)]
25-
struct LanguageDiff {
26-
language: String,
27-
added: Vec<String>,
28-
removed: Vec<String>,
29-
changed: Vec<ChangedItem>,
30-
unchanged: usize,
31-
}
32-
33-
#[derive(Debug, Clone, Default)]
34-
struct DiffSummary {
35-
languages: usize,
36-
added: usize,
37-
removed: usize,
38-
changed: usize,
39-
unchanged: usize,
40-
}
41-
42-
fn normalize_lang(lang: &str) -> String {
43-
lang.trim().replace('_', "-").to_ascii_lowercase()
44-
}
45-
46-
fn lang_base(lang: &str) -> &str {
47-
lang.split('-').next().unwrap_or(lang)
48-
}
49-
50-
fn lang_matches(resource_lang: &str, requested_lang: &str) -> bool {
51-
let res = normalize_lang(resource_lang);
52-
let req = normalize_lang(requested_lang);
53-
res == req || lang_base(&res) == lang_base(&req)
54-
}
55-
5615
fn translation_as_text(value: &Translation) -> String {
5716
match value {
5817
Translation::Empty => String::new(),
@@ -67,79 +26,6 @@ fn translation_as_text(value: &Translation) -> String {
6726
}
6827
}
6928

70-
fn build_language_map(resources: Vec<Resource>) -> BTreeMap<String, BTreeMap<String, Translation>> {
71-
let mut map: BTreeMap<String, BTreeMap<String, Translation>> = BTreeMap::new();
72-
for resource in resources {
73-
let lang = normalize_lang(&resource.metadata.language);
74-
let entry_map = map.entry(lang).or_default();
75-
for entry in resource.entries {
76-
entry_map.insert(entry.id, entry.value);
77-
}
78-
}
79-
map
80-
}
81-
82-
fn collect_diff(
83-
source: &BTreeMap<String, BTreeMap<String, Translation>>,
84-
target: &BTreeMap<String, BTreeMap<String, Translation>>,
85-
lang_filter: &Option<String>,
86-
) -> (DiffSummary, Vec<LanguageDiff>) {
87-
let mut summary = DiffSummary::default();
88-
let mut languages = BTreeSet::new();
89-
languages.extend(source.keys().cloned());
90-
languages.extend(target.keys().cloned());
91-
92-
let mut per_language = Vec::new();
93-
for lang in languages {
94-
if let Some(filter) = lang_filter
95-
&& !lang_matches(&lang, filter)
96-
{
97-
continue;
98-
}
99-
100-
let source_entries = source.get(&lang);
101-
let target_entries = target.get(&lang);
102-
103-
let mut all_keys = BTreeSet::new();
104-
if let Some(entries) = source_entries {
105-
all_keys.extend(entries.keys().cloned());
106-
}
107-
if let Some(entries) = target_entries {
108-
all_keys.extend(entries.keys().cloned());
109-
}
110-
111-
let mut diff = LanguageDiff {
112-
language: lang.clone(),
113-
..LanguageDiff::default()
114-
};
115-
116-
for key in all_keys {
117-
let source_value = source_entries.and_then(|m| m.get(&key));
118-
let target_value = target_entries.and_then(|m| m.get(&key));
119-
match (source_value, target_value) {
120-
(Some(_), None) => diff.added.push(key),
121-
(None, Some(_)) => diff.removed.push(key),
122-
(Some(s), Some(t)) if s != t => diff.changed.push(ChangedItem {
123-
key,
124-
source: s.clone(),
125-
target: t.clone(),
126-
}),
127-
(Some(_), Some(_)) => diff.unchanged += 1,
128-
(None, None) => {}
129-
}
130-
}
131-
132-
summary.languages += 1;
133-
summary.added += diff.added.len();
134-
summary.removed += diff.removed.len();
135-
summary.changed += diff.changed.len();
136-
summary.unchanged += diff.unchanged;
137-
per_language.push(diff);
138-
}
139-
140-
(summary, per_language)
141-
}
142-
14329
fn print_or_write(output: Option<&String>, content: &str) -> Result<(), String> {
14430
if let Some(path) = output {
14531
std::fs::write(path, content).map_err(|e| format!("Failed to write {}: {}", path, e))?;
@@ -150,16 +36,19 @@ fn print_or_write(output: Option<&String>, content: &str) -> Result<(), String>
15036
Ok(())
15137
}
15238

153-
fn render_human(summary: &DiffSummary, per_language: &[LanguageDiff]) -> String {
39+
fn render_human(report: &DiffReport) -> String {
15440
let mut lines = Vec::new();
15541
lines.push("=== Diff ===".to_string());
156-
lines.push(format!("Languages: {}", summary.languages));
42+
lines.push(format!("Languages: {}", report.summary.languages));
15743
lines.push(format!(
15844
"Totals: added={}, removed={}, changed={}, unchanged={}",
159-
summary.added, summary.removed, summary.changed, summary.unchanged
45+
report.summary.added,
46+
report.summary.removed,
47+
report.summary.changed,
48+
report.summary.unchanged
16049
));
16150

162-
for lang in per_language {
51+
for lang in &report.languages {
16352
lines.push(format!("\nLanguage: {}", lang.language));
16453
lines.push(format!(" added: {}", lang.added.len()));
16554
lines.push(format!(" removed: {}", lang.removed.len()));
@@ -188,23 +77,24 @@ fn render_human(summary: &DiffSummary, per_language: &[LanguageDiff]) -> String
18877
lines.join("\n")
18978
}
19079

191-
fn render_json(summary: &DiffSummary, per_language: &[LanguageDiff]) -> Result<String, String> {
192-
let languages_json: Vec<_> = per_language
80+
fn render_json(report: &DiffReport) -> Result<String, String> {
81+
let languages_json: Vec<_> = report
82+
.languages
19383
.iter()
19484
.map(|lang| {
19585
let changed: Vec<_> = lang
19686
.changed
19787
.iter()
19888
.map(|item| {
199-
json!({
89+
serde_json::json!({
20090
"key": item.key,
20191
"source": item.source,
20292
"target": item.target,
20393
})
20494
})
20595
.collect();
20696

207-
json!({
97+
serde_json::json!({
20898
"language": lang.language,
20999
"counts": {
210100
"added": lang.added.len(),
@@ -219,18 +109,18 @@ fn render_json(summary: &DiffSummary, per_language: &[LanguageDiff]) -> Result<S
219109
})
220110
.collect();
221111

222-
let report = json!({
112+
let payload = serde_json::json!({
223113
"summary": {
224-
"languages": summary.languages,
225-
"added": summary.added,
226-
"removed": summary.removed,
227-
"changed": summary.changed,
228-
"unchanged": summary.unchanged,
114+
"languages": report.summary.languages,
115+
"added": report.summary.added,
116+
"removed": report.summary.removed,
117+
"changed": report.summary.changed,
118+
"unchanged": report.summary.unchanged,
229119
},
230120
"languages": languages_json,
231121
});
232122

233-
serde_json::to_string_pretty(&report)
123+
serde_json::to_string_pretty(&payload)
234124
.map_err(|e| format!("Failed to serialize diff report JSON: {}", e))
235125
}
236126

@@ -247,15 +137,19 @@ pub fn run_diff_command(opts: DiffOptions) -> Result<(), String> {
247137
let source_resources = read_resources_from_any_input(&opts.source, None, opts.strict)?;
248138
let target_resources = read_resources_from_any_input(&opts.target, None, opts.strict)?;
249139

250-
let source_map = build_language_map(source_resources);
251-
let target_map = build_language_map(target_resources);
252-
let (summary, per_language) = collect_diff(&source_map, &target_map, &opts.lang);
140+
let report = diff_resources(
141+
&source_resources,
142+
&target_resources,
143+
&LibDiffOptions {
144+
language_filter: opts.lang.clone(),
145+
},
146+
);
253147

254148
if opts.json {
255-
let rendered = render_json(&summary, &per_language)?;
149+
let rendered = render_json(&report)?;
256150
print_or_write(opts.output.as_ref(), &rendered)?;
257151
} else {
258-
let rendered = render_human(&summary, &per_language);
152+
let rendered = render_human(&report);
259153
print_or_write(opts.output.as_ref(), &rendered)?;
260154
}
261155

langcodec-cli/src/merge.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::formats::parse_custom_format;
22
use crate::transformers::custom_format_to_resource;
33

4-
use langcodec::{Codec, converter};
4+
use langcodec::{Codec, ReadOptions, converter};
55
use rayon::prelude::*;
66

77
/// Strategy for handling conflicts when merging localization files.
@@ -177,7 +177,12 @@ fn read_input_to_resources(
177177

178178
let mut local_codec = Codec::new();
179179
local_codec
180-
.read_file_by_extension(input, lang)
180+
.read_file_by_extension_with_options(
181+
input,
182+
&ReadOptions::new()
183+
.with_language_hint(lang)
184+
.with_strict(true),
185+
)
181186
.map_err(|e| format!("Error reading {}: {}", input, e))?;
182187
return Ok(local_codec.resources);
183188
}

0 commit comments

Comments
 (0)