Skip to content

Commit 9cb4c8b

Browse files
committed
Add sync subcommand and implementation
Introduce a new "sync" CLI command to update existing keys in a target localization file from a source file without adding new keys. Files changed: add langcodec-cli/src/sync.rs (core logic: matching by key, translation-based fallback via --match-lang, type checks, dry-run, stats, and write-back), update langcodec-cli/src/main.rs (register Sync subcommand, arg parsing, validation, and command dispatch), update README.md and langcodec-cli/README.md (document sync usage and behavior), adjust integration help test, and add langcodec-cli/tests/sync_cli_tests.rs (functional and dry-run tests). The command supports per-language filtering, infers match language when not provided, and handles multiple formats when writing output.
1 parent d85659a commit 9cb4c8b

6 files changed

Lines changed: 603 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Universal localization toolkit: library + CLI for Apple/Android/CSV/TSV.
44

55
- Library crate (`langcodec`): parse, write, convert, merge with a unified model
6-
- CLI crate (`langcodec-cli`): convert, merge, view, stats, debug, edit
6+
- CLI crate (`langcodec-cli`): convert, merge, sync, view, stats, debug, edit
77

88
---
99

@@ -58,6 +58,7 @@ This is a `0.6.4` release available on [crates.io](https://crates.io/crates/lang
5858

5959
- Convert: `langcodec convert -i input.strings -o strings.xml`
6060
- Edit (add/update/remove): `langcodec edit set -i 'locales/**/*.strings' -k welcome -v "Hello"` (use `--dry-run` to preview)
61+
- Sync existing keys only: `langcodec sync --source A.xcstrings --target B.xcstrings --match-lang en`
6162
- View: `langcodec view -i strings.xml --full`
6263
- Stats (JSON): `langcodec stats -i Localizable.xcstrings --json`
6364
- See full options: langcodec-cli/README.md#stats

langcodec-cli/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Universal CLI for converting, inspecting, merging, and editing localization files.
44

55
- Formats: Apple `.strings`, `.xcstrings`, Android `strings.xml`, CSV, TSV
6-
- Commands: convert, merge, view, stats, debug, edit
6+
- Commands: convert, merge, sync, view, stats, debug, edit
77

88
## Install
99

@@ -28,6 +28,21 @@ Auto-detects formats from extensions. For JSON/YAML custom formats, see `--input
2828
langcodec merge -i a.strings -i b.strings -o merged.strings --lang en --strategy last
2929
```
3030

31+
### sync
32+
33+
Sync values from source file A into existing keys in target file B.
34+
This command updates only keys that already exist in target.
35+
36+
```sh
37+
langcodec sync --source A.xcstrings --target B.xcstrings --match-lang en
38+
langcodec sync --source source.csv --target target.csv --output synced.csv --match-lang en
39+
```
40+
41+
Matching rules:
42+
- key-to-key match first
43+
- fallback: use `--match-lang` translation (default inferred/en) to match source entries
44+
- never adds new keys to target
45+
3146
### view
3247

3348
```sh

langcodec-cli/src/main.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod formats;
55
mod merge;
66
mod path_glob;
77
mod stats;
8+
mod sync;
89
mod transformers;
910
mod validation;
1011
mod view;
@@ -13,7 +14,8 @@ use crate::convert::{ConvertOptions, run_unified_convert_command, try_custom_for
1314
use crate::debug::run_debug_command;
1415
use crate::edit::{EditSetOptions, run_edit_set_command};
1516
use crate::merge::{ConflictStrategy, run_merge_command};
16-
use crate::validation::{ValidationContext, validate_context};
17+
use crate::sync::{SyncOptions, run_sync_command};
18+
use crate::validation::{ValidationContext, validate_context, validate_language_code};
1719
use crate::view::print_view;
1820
use clap::{CommandFactory, Parser, Subcommand};
1921
use clap_complete::{Shell, generate};
@@ -74,6 +76,40 @@ enum Commands {
7476
command: EditCommands,
7577
},
7678

79+
/// Sync existing entries from a source file into a target file.
80+
///
81+
/// Behavior:
82+
/// - Only updates entries that already exist in target
83+
/// - Never adds new keys to target
84+
/// - Matches by key first
85+
/// - Fallback matching by source-language translation (`--match-lang`, default: inferred/en)
86+
#[command(verbatim_doc_comment)]
87+
Sync {
88+
/// Source localization file (A): values are copied from here
89+
#[arg(short = 's', long)]
90+
source: String,
91+
92+
/// Target localization file (B): existing entries are updated here
93+
#[arg(short = 't', long)]
94+
target: String,
95+
96+
/// Optional output path (default: write back to --target)
97+
#[arg(short, long)]
98+
output: Option<String>,
99+
100+
/// Restrict updates to a single target language (e.g., "fr")
101+
#[arg(short, long)]
102+
lang: Option<String>,
103+
104+
/// Language used for translation-based fallback matching (e.g., "en")
105+
#[arg(long)]
106+
match_lang: Option<String>,
107+
108+
/// Preview changes without writing
109+
#[arg(long, default_value_t = false)]
110+
dry_run: bool,
111+
},
112+
77113
/// View localization files.
78114
View {
79115
/// The input file to view
@@ -281,6 +317,46 @@ fn main() {
281317
}
282318
}
283319
},
320+
Commands::Sync {
321+
source,
322+
target,
323+
output,
324+
lang,
325+
match_lang,
326+
dry_run,
327+
} => {
328+
let mut context = ValidationContext::new()
329+
.with_input_file(source.clone())
330+
.with_input_file(target.clone());
331+
if let Some(output_path) = &output {
332+
context = context.with_output_file(output_path.clone());
333+
}
334+
if let Some(lang_code) = &lang {
335+
context = context.with_language_code(lang_code.clone());
336+
}
337+
if let Err(e) = validate_context(&context) {
338+
eprintln!("❌ Validation failed: {}", e);
339+
std::process::exit(1);
340+
}
341+
if let Some(match_lang_code) = &match_lang
342+
&& let Err(e) = validate_language_code(match_lang_code)
343+
{
344+
eprintln!("❌ Validation failed: {}", e);
345+
std::process::exit(1);
346+
}
347+
348+
if let Err(e) = run_sync_command(SyncOptions {
349+
source,
350+
target,
351+
output,
352+
lang,
353+
match_lang,
354+
dry_run,
355+
}) {
356+
eprintln!("❌ Sync failed: {}", e);
357+
std::process::exit(1);
358+
}
359+
}
284360
Commands::View {
285361
input,
286362
lang,

0 commit comments

Comments
 (0)