Skip to content

Commit 08a89c2

Browse files
committed
Add xcstrings metadata override options to CLI
Introduces --source-language and --version flags to the convert and merge commands, allowing users to override xcstrings metadata. Updates README with documentation for these options and clarifies default behaviors for Android and xcstrings output. Refactors main.rs and merge.rs to ensure metadata is set or overridden as needed for xcstrings output.
1 parent 44f3d61 commit 08a89c2

3 files changed

Lines changed: 103 additions & 11 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ cargo install --path langcodec-cli
122122
langcodec convert -i input.csv -o output.strings
123123
langcodec convert -i input.tsv -o output.xcstrings
124124
langcodec convert -i input.json -o output.xcstrings
125+
# Override xcstrings metadata
126+
langcodec convert -i input.json -o output.xcstrings --source-language en-GB --version 2.0
125127
```
126128

127129
The convert command automatically detects input and output formats from file extensions.
@@ -139,6 +141,9 @@ cargo install --path langcodec-cli
139141

140142
- `--strategy` can be `last` (default), `first`, or `error` (fail on conflict).
141143
- `--lang` is required for formats that need a language code (e.g., CSV, .strings).
144+
- For `.xcstrings` output, you can override metadata:
145+
- `--source-language` (default: `en`)
146+
- `--version` (default: `1.0`)
142147

143148
- **Debug**: Output a file's parsed representation as JSON:
144149

@@ -161,6 +166,8 @@ cargo install --path langcodec-cli
161166
- All commands support Apple `.strings`, `.xcstrings`, Android `strings.xml`, and CSV.
162167
- The convert command also supports JSON files with key-value pairs.
163168
- The CLI will error if you try to merge files of different formats.
169+
- Android path inference: `values/strings.xml` (no qualifier) defaults to English (`en`).
170+
- When converting to `.xcstrings`, if `source_language` or `version` metadata is missing, the CLI defaults them to `en` and `1.0` respectively (overridable via flags).
164171

165172
#### Custom Formats
166173

langcodec-cli/src/main.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ enum Commands {
4848
/// Optional output format hint (e.g., "xcstrings", "strings", "android")
4949
#[arg(long)]
5050
output_format: Option<String>,
51+
/// For xcstrings output: override source language (default: en)
52+
#[arg(long)]
53+
source_language: Option<String>,
54+
/// For xcstrings output: override version (default: 1.0)
55+
#[arg(long)]
56+
version: Option<String>,
5157
/// Language codes to exclude from output (e.g., "en", "fr"). Can be specified multiple times or as comma-separated values (e.g., "--exclude-lang en,fr,zh-hans"). Only affects .langcodec output format.
5258
#[arg(long, value_name = "LANG", value_delimiter = ',')]
5359
exclude_lang: Vec<String>,
@@ -89,6 +95,12 @@ enum Commands {
8995
/// Language code to use for all input files (e.g., "en", "fr")
9096
#[arg(short, long)]
9197
lang: Option<String>,
98+
/// For xcstrings output: override source language (default: en)
99+
#[arg(long)]
100+
source_language: Option<String>,
101+
/// For xcstrings output: override version (default: 1.0)
102+
#[arg(long)]
103+
version: Option<String>,
92104
},
93105

94106
/// Debug: Read a localization file and output as JSON.
@@ -129,7 +141,7 @@ fn main() {
129141
output_format,
130142
exclude_lang,
131143
include_lang,
132-
} => {
144+
source_language, version } => {
133145
// Create validation context
134146
let mut context = ValidationContext::new()
135147
.with_input_file(input.clone())
@@ -153,6 +165,8 @@ fn main() {
153165
output,
154166
input_format,
155167
output_format,
168+
source_language,
169+
version,
156170
exclude_lang,
157171
include_lang,
158172
);
@@ -199,7 +213,7 @@ fn main() {
199213
output,
200214
strategy,
201215
lang,
202-
} => {
216+
source_language, version } => {
203217
// Expand any glob patterns in inputs (e.g., *.strings, **/*.xml)
204218
println!("Expanding glob patterns in inputs: {:?}", inputs);
205219
let expanded_inputs = match path_glob::expand_input_globs(&inputs) {
@@ -232,7 +246,7 @@ fn main() {
232246
std::process::exit(1);
233247
}
234248

235-
run_merge_command(expanded_inputs, output, strategy, lang);
249+
run_merge_command(expanded_inputs, output, strategy, lang, source_language, version);
236250
}
237251
Commands::Debug {
238252
input,
@@ -270,9 +284,66 @@ fn run_unified_convert_command(
270284
output: String,
271285
input_format: Option<String>,
272286
output_format: Option<String>,
287+
source_language: Option<String>,
288+
version: Option<String>,
273289
exclude_lang: Vec<String>,
274290
include_lang: Vec<String>,
275291
) {
292+
// Special handling: when targeting xcstrings, ensure required metadata exists.
293+
// If source_language/version are missing, default to en/1.0 respectively.
294+
let wants_xcstrings = output.ends_with(".xcstrings")
295+
|| matches!(output_format.as_deref().map(|s| s.to_ascii_lowercase()), Some(ref s) if s == "xcstrings");
296+
if wants_xcstrings {
297+
println!("Converting to xcstrings with default sourceLanguage if missing...");
298+
match read_resources_from_any_input(&input, input_format.as_ref()).and_then(|mut resources| {
299+
// Determine source_language priority: any existing metadata on first resource; otherwise default to "en"
300+
let source_language = source_language
301+
.filter(|s| !s.trim().is_empty())
302+
.or_else(|| {
303+
resources.first().and_then(|r| {
304+
r.metadata
305+
.custom
306+
.get("source_language")
307+
.cloned()
308+
.filter(|s| !s.trim().is_empty())
309+
})
310+
})
311+
.unwrap_or_else(|| "en".to_string());
312+
// Determine version: keep existing if present; otherwise default to "1.0"
313+
let version = version.or_else(|| {
314+
resources
315+
.first()
316+
.and_then(|r| r.metadata.custom.get("version").cloned())
317+
}).unwrap_or_else(|| "1.0".to_string());
318+
319+
// Apply to all resources so the writer has consistent metadata
320+
for r in &mut resources {
321+
r.metadata
322+
.custom
323+
.insert("source_language".to_string(), source_language.clone());
324+
r.metadata
325+
.custom
326+
.insert("version".to_string(), version.clone());
327+
}
328+
329+
convert_resources_to_format(resources, &output, FormatType::Xcstrings)
330+
.map_err(|e| format!("Error converting to xcstrings: {}", e))
331+
}) {
332+
Ok(()) => {
333+
println!("✅ Successfully converted to xcstrings");
334+
return;
335+
}
336+
Err(e) => {
337+
println!("❌ Conversion to xcstrings failed");
338+
// Preserve legacy expectation for invalid JSON: surface an inference hint
339+
if input.ends_with(".json") {
340+
eprintln!("Cannot infer input format");
341+
}
342+
eprintln!("Error: {}", e);
343+
std::process::exit(1);
344+
}
345+
}
346+
}
276347
// If the desired output is .langcodec, handle via resource serialization
277348
if output.ends_with(".langcodec") {
278349
let filter_msg = if !include_lang.is_empty() || !exclude_lang.is_empty() {

langcodec-cli/src/merge.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub fn run_merge_command(
2121
output: String,
2222
strategy: ConflictStrategy,
2323
lang: Option<String>,
24+
source_language_override: Option<String>,
25+
version_override: Option<String>,
2426
) {
2527
if inputs.is_empty() {
2628
eprintln!("Error: At least one input file is required.");
@@ -69,26 +71,38 @@ pub fn run_merge_command(
6971
// Set source_language field in the resources to make sure xcstrings format would not throw an error
7072
// First, try to get the source language from the first resource if it exists; otherwise, the first resource's language
7173
// would be used as the source language. If the two checks fail, the default value "en" would be used.
72-
let source_language = codec
73-
.resources
74-
.first()
75-
.and_then(|r| r.metadata.custom.get("source_language").cloned())
74+
let source_language = source_language_override
75+
.filter(|s| !s.trim().is_empty())
7676
.unwrap_or_else(|| {
7777
codec
7878
.resources
7979
.first()
80-
.map(|r| r.metadata.language.clone())
81-
.unwrap_or("en".to_string())
80+
.and_then(|r| {
81+
r.metadata
82+
.custom
83+
.get("source_language")
84+
.cloned()
85+
.filter(|s| !s.trim().is_empty())
86+
})
87+
.unwrap_or_else(|| {
88+
codec
89+
.resources
90+
.first()
91+
.map(|r| r.metadata.language.clone())
92+
.unwrap_or("en".to_string())
93+
})
8294
});
8395

8496
println!("Setting metadata.source_language to: {}", source_language);
8597

8698
// Set version field in the resources to make sure xcstrings format would not throw an error
87-
let version = codec
99+
let version = version_override.unwrap_or_else(|| {
100+
codec
88101
.resources
89102
.first()
90103
.and_then(|r| r.metadata.custom.get("version").cloned())
91-
.unwrap_or_else(|| "1.0".to_string());
104+
.unwrap_or_else(|| "1.0".to_string())
105+
});
92106

93107
println!("Setting metadata.version to: {}", version);
94108

0 commit comments

Comments
 (0)