Skip to content

Commit 186e370

Browse files
committed
Add global --strict mode and Diff command
Introduce a global --strict flag and strict parsing behavior across the CLI, plus a new Diff subcommand and additional sync options. - README: document Global strict mode. - CLI: add global `--strict` arg and `Diff` subcommand; add sync flags `--report-json`, `--fail-on-unmatched`, `--fail-on-ambiguous`. - convert.rs: propagate `strict` into run_unified_convert_command; implement strict branches that enforce explicit-format conversions, custom-format validation, or extension-only conversion and exit nonzero on failure; update read_resources_from_any_input to accept `strict` and perform stricter parsing/validation when enabled. - debug.rs / merge.rs / main.rs: propagate `strict` through command handlers and file-read helpers; add strict-aware flows for debug, merge, view, sync commands. - Tests: add CLI tests for sync report JSON, fail-on-unmatched behavior, and strict-mode failure on unmatched entries. These changes make parser fallbacks opt-in (disabled under --strict) and add a file-comparison command and sync-related features.
1 parent 3fd1cd8 commit 186e370

6 files changed

Lines changed: 377 additions & 26 deletions

File tree

langcodec-cli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Supported formats: .strings, .xml (Android), .xcstrings, .csv, .tsv. Custom JSON
126126
- Android plurals `<plurals>` are supported.
127127
- Language inference: `en.lproj/Localizable.strings`, `values-es/strings.xml`, base `values/``en` by default.
128128
- Globbing: use quotes for patterns in merge and edit (e.g., `'**/*.xml'`).
129+
- Global strict mode: add `--strict` before any subcommand to disable parser fallbacks and enforce stricter failures.
129130

130131
## License
131132

langcodec-cli/src/convert.rs

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ pub struct ConvertOptions {
1616
pub include_lang: Vec<String>,
1717
}
1818

19-
pub fn run_unified_convert_command(input: String, output: String, options: ConvertOptions) {
19+
pub fn run_unified_convert_command(
20+
input: String,
21+
output: String,
22+
options: ConvertOptions,
23+
strict: bool,
24+
) {
2025
// Special handling: when targeting xcstrings, ensure required metadata exists.
2126
// If source_language/version are missing, default to en/1.0 respectively.
2227
let wants_xcstrings = output.ends_with(".xcstrings")
@@ -26,7 +31,7 @@ pub fn run_unified_convert_command(input: String, output: String, options: Conve
2631
.is_some_and(|s| s.eq_ignore_ascii_case("xcstrings"));
2732
if wants_xcstrings {
2833
println!("Converting to xcstrings with default sourceLanguage if missing...");
29-
match read_resources_from_any_input(&input, options.input_format.as_ref()).and_then(
34+
match read_resources_from_any_input(&input, options.input_format.as_ref(), strict).and_then(
3035
|mut resources| {
3136
// Determine source_language priority: explicit flag > metadata > default
3237
let source_language = options
@@ -110,7 +115,7 @@ pub fn run_unified_convert_command(input: String, output: String, options: Conve
110115
"Converting input to .langcodec (Resource JSON array){}...",
111116
filter_msg
112117
);
113-
match read_resources_from_any_input(&input, options.input_format.as_ref()).and_then(
118+
match read_resources_from_any_input(&input, options.input_format.as_ref(), strict).and_then(
114119
|resources| {
115120
// Apply language filtering
116121
let filtered_resources = resources
@@ -179,6 +184,46 @@ pub fn run_unified_convert_command(input: String, output: String, options: Conve
179184
}
180185
}
181186

187+
if strict {
188+
if let (Some(input_fmt), Some(output_fmt)) = (
189+
options.input_format.as_deref(),
190+
options.output_format.as_deref(),
191+
) {
192+
println!("Strict mode: converting with explicit format hints only...");
193+
if let Err(e) = try_explicit_format_conversion(&input, &output, input_fmt, output_fmt) {
194+
println!("❌ Strict conversion failed");
195+
eprintln!("Error: {}", e);
196+
std::process::exit(1);
197+
}
198+
println!("✅ Successfully converted in strict mode");
199+
return;
200+
}
201+
202+
if input.ends_with(".json")
203+
|| input.ends_with(".yaml")
204+
|| input.ends_with(".yml")
205+
|| input.ends_with(".langcodec")
206+
{
207+
println!("Strict mode: converting custom format without fallback...");
208+
if let Err(e) = try_custom_format_conversion(&input, &output, &options.input_format) {
209+
println!("❌ Strict conversion failed");
210+
eprintln!("Error: {}", e);
211+
std::process::exit(1);
212+
}
213+
println!("✅ Successfully converted in strict mode");
214+
return;
215+
}
216+
217+
println!("Strict mode: converting using extension-based standard formats only...");
218+
if let Err(e) = convert_auto(&input, &output) {
219+
println!("❌ Strict conversion failed");
220+
eprintln!("Error: {}", e);
221+
std::process::exit(1);
222+
}
223+
println!("✅ Successfully converted in strict mode");
224+
return;
225+
}
226+
182227
// Strategy 1: Try standard lib crate conversion first
183228
println!("Trying standard format detection from file extensions...");
184229
if let Ok(()) = convert_auto(&input, &output) {
@@ -433,7 +478,67 @@ fn write_resources_as_langcodec(
433478
pub fn read_resources_from_any_input(
434479
input: &str,
435480
input_format_hint: Option<&String>,
481+
strict: bool,
436482
) -> Result<Vec<langcodec::Resource>, String> {
483+
if strict {
484+
if let Some(fmt) = input_format_hint {
485+
let fmt_lower = fmt.to_lowercase();
486+
let maybe_std = match fmt_lower.as_str() {
487+
"strings" => Some(langcodec::formats::FormatType::Strings(None)),
488+
"android" | "androidstrings" => {
489+
Some(langcodec::formats::FormatType::AndroidStrings(None))
490+
}
491+
"xcstrings" => Some(langcodec::formats::FormatType::Xcstrings),
492+
"csv" => Some(langcodec::formats::FormatType::CSV),
493+
"tsv" => Some(langcodec::formats::FormatType::TSV),
494+
_ => None,
495+
};
496+
497+
if let Some(std_fmt) = maybe_std {
498+
let mut codec = Codec::new();
499+
codec
500+
.read_file_by_type(input, std_fmt)
501+
.map_err(|e| format!("Failed to read input with explicit format: {}", e))?;
502+
return Ok(codec.resources);
503+
}
504+
505+
let custom_format = parse_custom_format(fmt)?;
506+
let resources = custom_format_to_resource(input.to_string(), custom_format)?;
507+
return Ok(resources);
508+
}
509+
510+
if input.ends_with(".strings")
511+
|| input.ends_with(".xml")
512+
|| input.ends_with(".xcstrings")
513+
|| input.ends_with(".csv")
514+
|| input.ends_with(".tsv")
515+
{
516+
let mut codec = Codec::new();
517+
codec
518+
.read_file_by_extension(input, None)
519+
.map_err(|e| format!("Failed to read input: {}", e))?;
520+
return Ok(codec.resources);
521+
}
522+
523+
if input.ends_with(".json")
524+
|| input.ends_with(".yaml")
525+
|| input.ends_with(".yml")
526+
|| input.ends_with(".langcodec")
527+
{
528+
validate_custom_format_file(input)?;
529+
let file_content = std::fs::read_to_string(input)
530+
.map_err(|e| format!("Error reading file {}: {}", input, e))?;
531+
let custom_format = formats::validate_custom_format_content(input, &file_content)?;
532+
let resources = custom_format_to_resource(input.to_string(), custom_format)?;
533+
return Ok(resources);
534+
}
535+
536+
return Err(format!(
537+
"Unsupported input format or file extension: '{}'. Supported formats: .strings, .xml, .xcstrings, .csv, .tsv, .json, .yaml, .yml, .langcodec",
538+
input
539+
));
540+
}
541+
437542
// First: if explicit input format is provided and is a standard format, use it
438543
if let Some(fmt) = input_format_hint {
439544
let fmt_lower = fmt.to_lowercase();

langcodec-cli/src/debug.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,32 @@ use std::fs::File;
66
use std::io::{self, Write};
77

88
/// Run the debug command: read a localization file and output as JSON.
9-
pub fn run_debug_command(input: String, lang: Option<String>, output: Option<String>) {
9+
pub fn run_debug_command(
10+
input: String,
11+
lang: Option<String>,
12+
output: Option<String>,
13+
strict: bool,
14+
) {
1015
// Read the input file
1116
eprintln!("Reading input file...");
1217
let mut codec = Codec::new();
13-
// Try standard format first
14-
if let Ok(()) = codec.read_file_by_extension(&input, lang.clone()) {
18+
let is_custom_ext =
19+
input.ends_with(".json") || input.ends_with(".yaml") || input.ends_with(".yml");
20+
if strict {
21+
if is_custom_ext {
22+
if let Err(e) = try_custom_format_debug(&input, lang.clone(), &mut codec) {
23+
eprintln!("❌ Error reading input file");
24+
eprintln!("Error reading {}: {}", input, e);
25+
std::process::exit(1);
26+
}
27+
} else if let Err(e) = codec.read_file_by_extension(&input, lang.clone()) {
28+
eprintln!("❌ Error reading input file");
29+
eprintln!("Error reading {}: {}", input, e);
30+
std::process::exit(1);
31+
}
32+
} else if let Ok(()) = codec.read_file_by_extension(&input, lang.clone()) {
1533
// Standard format succeeded
16-
} else if input.ends_with(".json") || input.ends_with(".yaml") || input.ends_with(".yml") {
34+
} else if is_custom_ext {
1735
// Try custom format for JSON/YAML files
1836
if let Err(e) = try_custom_format_debug(&input, lang.clone(), &mut codec) {
1937
eprintln!("❌ Error reading input file");

0 commit comments

Comments
 (0)