Skip to content

Commit 6fa7e89

Browse files
committed
fix(cli): honor strict normalize and handle unmatched globs
1 parent ca8edd0 commit 6fa7e89

4 files changed

Lines changed: 71 additions & 17 deletions

File tree

langcodec-cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ fn main() {
645645
no_placeholders,
646646
key_style,
647647
continue_on_error,
648+
strict,
648649
};
649650
if let Err(e) = run_normalize_command(opts) {
650651
eprintln!("❌ Normalize failed: {}", e);

langcodec-cli/src/normalize.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::path_glob;
22
use crate::validation::{validate_file_path, validate_output_path};
33
use langcodec::{
4-
Codec, FormatType, KeyStyle, NormalizeOptions as EngineNormalizeOptions, normalize_codec,
4+
Codec, FormatType, KeyStyle, NormalizeOptions as EngineNormalizeOptions, ReadOptions,
5+
normalize_codec,
56
};
67
use std::collections::HashSet;
78
use std::path::Path;
@@ -15,6 +16,7 @@ pub struct NormalizeCliOptions {
1516
pub no_placeholders: bool,
1617
pub key_style: String,
1718
pub continue_on_error: bool,
19+
pub strict: bool,
1820
}
1921

2022
fn parse_key_style(input: &str) -> Result<KeyStyle, String> {
@@ -83,19 +85,20 @@ fn run_normalize_for_file(
8385
check: bool,
8486
no_placeholders: bool,
8587
key_style: &KeyStyle,
88+
strict: bool,
8689
) -> Result<bool, String> {
8790
validate_file_path(input)?;
8891

8992
let mut codec = Codec::new();
9093
codec
91-
.read_file_by_extension(input, None)
94+
.read_file_by_extension_with_options(input, &ReadOptions::new().with_strict(strict))
9295
.map_err(|e| format!("Failed to read input '{}': {}", input, e))?;
9396

9497
let report = normalize_codec(
9598
&mut codec,
9699
&EngineNormalizeOptions {
97100
normalize_placeholders: !no_placeholders,
98-
key_style: key_style.clone(),
101+
key_style: *key_style,
99102
},
100103
)
101104
.map_err(|e| e.to_string())?;
@@ -147,6 +150,10 @@ fn run_normalize_for_file(
147150
pub fn run_normalize_command(opts: NormalizeCliOptions) -> Result<(), String> {
148151
let expanded = path_glob::expand_input_globs(&opts.inputs)
149152
.map_err(|e| format!("Failed to expand input patterns: {}", e))?;
153+
let expanded: Vec<String> = expanded
154+
.into_iter()
155+
.filter(|path| !has_glob_meta(path) || Path::new(path).is_file())
156+
.collect();
150157
if expanded.is_empty() {
151158
return Err("No input files matched the provided patterns".to_string());
152159
}
@@ -193,6 +200,7 @@ pub fn run_normalize_command(opts: NormalizeCliOptions) -> Result<(), String> {
193200
opts.check,
194201
opts.no_placeholders,
195202
&key_style,
203+
opts.strict,
196204
) {
197205
Ok(changed) => {
198206
success_count += 1;

langcodec-cli/tests/normalize_cli_tests.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,54 @@ fn test_normalize_continue_on_error_counts_literal_missing_input_in_summary() {
347347
"expected coherent summary counts, got: {combined}"
348348
);
349349
}
350+
351+
#[test]
352+
fn test_normalize_strict_requires_language_for_single_language_formats() {
353+
let temp_dir = TempDir::new().unwrap();
354+
let input = temp_dir.path().join("Localizable.strings");
355+
fs::write(&input, "\"hello\" = \"Hello\";\n").unwrap();
356+
357+
let output = langcodec_cmd()
358+
.args(["--strict", "normalize", "-i", input.to_str().unwrap()])
359+
.output()
360+
.unwrap();
361+
362+
assert!(
363+
!output.status.success(),
364+
"expected strict normalize to fail without language inference"
365+
);
366+
let combined = format!(
367+
"{}{}",
368+
String::from_utf8_lossy(&output.stdout),
369+
String::from_utf8_lossy(&output.stderr)
370+
);
371+
assert!(
372+
combined.contains("missing language"),
373+
"expected strict missing-language error, got: {combined}"
374+
);
375+
}
376+
377+
#[test]
378+
fn test_normalize_reports_no_matches_for_glob_patterns() {
379+
let temp_dir = TempDir::new().unwrap();
380+
let missing_glob = temp_dir.path().join("missing").join("*.strings");
381+
382+
let output = langcodec_cmd()
383+
.args(["normalize", "-i", missing_glob.to_str().unwrap()])
384+
.output()
385+
.unwrap();
386+
387+
assert!(
388+
!output.status.success(),
389+
"expected normalize to fail when glob pattern matches no files"
390+
);
391+
let combined = format!(
392+
"{}{}",
393+
String::from_utf8_lossy(&output.stdout),
394+
String::from_utf8_lossy(&output.stderr)
395+
);
396+
assert!(
397+
combined.contains("No input files matched the provided patterns"),
398+
"expected no-match glob error, got: {combined}"
399+
);
400+
}

langcodec/src/normalize.rs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ fn normalize_codec_in_place(
5555
let transformed = transform_key_style(&entry.id, options.key_style);
5656
if let Some(existing) = seen.get(&transformed) {
5757
return Err(Error::validation_error(format!(
58-
"key-style collision in resource '{}' (domain '{}'): '{}' and '{}' both normalize to '{}'",
58+
"key-style collision in language '{}' (domain '{}'): '{}' and '{}' both normalize to '{}'",
5959
resource.metadata.language,
6060
resource.metadata.domain,
6161
existing,
@@ -85,20 +85,14 @@ fn normalize_codec_in_place(
8585
}
8686
}
8787

88-
let before_order: Vec<String> = resource
88+
let already_sorted = resource
8989
.entries
90-
.iter()
91-
.map(|entry| entry.id.clone())
92-
.collect();
93-
resource
94-
.entries
95-
.sort_by(|left, right| left.id.cmp(&right.id));
96-
let after_order: Vec<String> = resource
97-
.entries
98-
.iter()
99-
.map(|entry| entry.id.clone())
100-
.collect();
101-
if before_order != after_order {
90+
.windows(2)
91+
.all(|pair| pair[0].id <= pair[1].id);
92+
if !already_sorted {
93+
resource
94+
.entries
95+
.sort_by(|left, right| left.id.cmp(&right.id));
10296
changed = true;
10397
}
10498
}

0 commit comments

Comments
 (0)