use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn langcodec_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("langcodec"))
}
#[test]
fn test_edit_set_add_update_remove_strings_in_place() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("en.strings");
let initial = r#"/* Greeting */
"hello" = "Hello";
"#;
fs::write(&input_file, initial).unwrap();
let out_add = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome!",
])
.output()
.unwrap();
assert!(
out_add.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out_add.stderr)
);
let after_add = fs::read_to_string(&input_file).unwrap();
assert!(after_add.contains("\"hello\""));
assert!(after_add.contains("\"welcome\""));
assert!(after_add.contains("\"Welcome!\""));
let out_update = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome back!",
"--status",
"needs_review",
])
.output()
.unwrap();
assert!(
out_update.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out_update.stderr)
);
let after_update = fs::read_to_string(&input_file).unwrap();
assert!(after_update.contains("\"welcome\""));
assert!(after_update.contains("Welcome back!"));
let out_remove = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
])
.output()
.unwrap();
assert!(
out_remove.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out_remove.stderr)
);
let after_remove = fs::read_to_string(&input_file).unwrap();
assert!(!after_remove.contains("\"welcome\""));
}
#[test]
fn test_edit_set_dry_run_add_does_not_write() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("en.strings");
let initial = r#"/* Greeting */
"hello" = "Hello";
"#;
fs::write(&input_file, initial).unwrap();
let output = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome!",
"--dry-run",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("DRY-RUN"));
let after = fs::read_to_string(&input_file).unwrap();
assert_eq!(after, initial);
}
#[test]
fn test_edit_set_dry_run_update_does_not_write() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("en.strings");
let initial = r#"/* Greeting */
"hello" = "Hello";
"#;
fs::write(&input_file, initial).unwrap();
let _ = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome!",
])
.output()
.unwrap();
let before = fs::read_to_string(&input_file).unwrap();
let output = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome again!",
"--dry-run",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("DRY-RUN"));
let after = fs::read_to_string(&input_file).unwrap();
assert_eq!(after, before);
}
#[test]
fn test_edit_set_dry_run_remove_does_not_write() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("en.strings");
let initial = r#"/* Greeting */
"hello" = "Hello";
"welcome" = "Welcome!";
"#;
fs::write(&input_file, initial).unwrap();
let output = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"welcome",
"--dry-run",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("DRY-RUN"));
let after = fs::read_to_string(&input_file).unwrap();
assert_eq!(after, initial);
}
#[test]
fn test_edit_set_with_output_path() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("en.strings");
let output_file = temp_dir.path().join("out.strings");
let initial = r#"/* Greeting */
"hello" = "Hello";
"#;
fs::write(&input_file, initial).unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"-k",
"new_key",
"-v",
"Value",
"-o",
output_file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(output_file.exists());
let out_content = fs::read_to_string(&output_file).unwrap();
assert!(out_content.contains("\"new_key\""));
assert!(out_content.contains("\"Value\""));
}
#[test]
fn test_main_help_lists_edit() {
let output = langcodec_cmd().args(["--help"]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("edit"));
}
#[test]
fn test_edit_set_multiple_files() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("a.strings");
let file2 = temp_dir.path().join("b.strings");
fs::write(&file1, "\"hello\" = \"Hello\";\n").unwrap();
fs::write(&file2, "\"hello\" = \"Hello\";\n").unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
file1.to_str().unwrap(),
"-i",
file2.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome!",
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let c1 = fs::read_to_string(&file1).unwrap();
let c2 = fs::read_to_string(&file2).unwrap();
assert!(c1.contains("\"welcome\""));
assert!(c2.contains("\"welcome\""));
}
#[test]
fn test_edit_set_glob_pattern() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
let f1 = dir.join("a.strings");
let f2 = dir.join("b.strings");
fs::write(&f1, "\"hello\" = \"Hello\";\n").unwrap();
fs::write(&f2, "\"hello\" = \"Hello\";\n").unwrap();
let pattern = format!("{}/*.strings", dir.to_string_lossy());
let out = langcodec_cmd()
.args(["edit", "set", "-i", &pattern, "-k", "added", "-v", "Yes"])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(fs::read_to_string(&f1).unwrap().contains("\"added\""));
assert!(fs::read_to_string(&f2).unwrap().contains("\"added\""));
}
#[test]
fn test_edit_set_multiple_inputs_with_output_is_error() {
let temp_dir = TempDir::new().unwrap();
let f1 = temp_dir.path().join("a.strings");
let f2 = temp_dir.path().join("b.strings");
let out_file = temp_dir.path().join("out.strings");
fs::write(&f1, "\"hello\" = \"Hello\";\n").unwrap();
fs::write(&f2, "\"hello\" = \"Hello\";\n").unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
f1.to_str().unwrap(),
"-i",
f2.to_str().unwrap(),
"-k",
"x",
"-v",
"y",
"-o",
out_file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
!out.status.success(),
"expected failure when using --output with multiple inputs"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("cannot be used with multiple input files"));
}
#[test]
fn test_edit_set_continue_on_error() {
let temp_dir = TempDir::new().unwrap();
let good = temp_dir.path().join("good.strings");
let bad = temp_dir.path().join("missing.strings");
fs::write(&good, "\"hello\" = \"Hello\";\n").unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
good.to_str().unwrap(),
"-i",
bad.to_str().unwrap(),
"-k",
"welcome",
"-v",
"Welcome!",
"--continue-on-error",
])
.output()
.unwrap();
assert!(
!out.status.success(),
"expected non-zero exit when some files fail"
);
let updated = fs::read_to_string(&good).unwrap();
assert!(updated.contains("\"welcome\""));
}
#[test]
fn test_edit_set_xcstrings_with_lang_preserves_other_languages() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("Localizable.xcstrings");
let initial = r#"{
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"hello": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hello"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Bonjour"
}
}
}
},
"bye": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Bye"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Au revoir"
}
}
}
}
}
}
"#;
fs::write(&input_file, initial).unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"--lang",
"en",
"-k",
"hello",
"-v",
"Hello Updated",
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let content = fs::read_to_string(&input_file).unwrap();
let payload: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
payload["strings"]["hello"]["localizations"]["en"]["stringUnit"]["value"],
"Hello Updated"
);
assert_eq!(
payload["strings"]["hello"]["localizations"]["fr"]["stringUnit"]["value"],
"Bonjour"
);
assert_eq!(
payload["strings"]["bye"]["localizations"]["fr"]["stringUnit"]["value"],
"Au revoir"
);
}
#[test]
fn test_edit_set_xcstrings_preserves_is_comment_auto_generated() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("Localizable.xcstrings");
let initial = r#"{
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"hello": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hello"
}
}
},
"isCommentAutoGenerated": true
}
}
}
"#;
fs::write(&input_file, initial).unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"--lang",
"en",
"-k",
"hello",
"-v",
"Hello Updated",
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let content = fs::read_to_string(&input_file).unwrap();
let payload: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
payload["strings"]["hello"]["isCommentAutoGenerated"],
serde_json::Value::Bool(true)
);
}
#[test]
fn test_edit_set_xcstrings_preserves_non_translatable_entry_without_localizations() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("Localizable.xcstrings");
let initial = r#"{
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"game_title": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Game"
}
}
}
},
"Carrom": {
"comment": "The text label for the Carrom game button.",
"isCommentAutoGenerated": true,
"shouldTranslate": false
}
}
}
"#;
fs::write(&input_file, initial).unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"--lang",
"en",
"-k",
"game_title",
"-v",
"Game Updated",
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let content = fs::read_to_string(&input_file).unwrap();
let payload: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
payload["strings"]["Carrom"]["comment"],
"The text label for the Carrom game button."
);
assert_eq!(
payload["strings"]["Carrom"]["isCommentAutoGenerated"],
serde_json::Value::Bool(true)
);
assert_eq!(
payload["strings"]["Carrom"]["shouldTranslate"],
serde_json::Value::Bool(false)
);
assert_eq!(
payload["strings"]["game_title"]["localizations"]["en"]["stringUnit"]["value"],
"Game Updated"
);
}
#[test]
fn test_edit_set_xcstrings_preserves_translatable_metadata_only_entry() {
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("Localizable.xcstrings");
let initial = r#"{
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"The following rewards have been sent to your backpack.": {
"comment": "Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack.",
"isCommentAutoGenerated": true
}
}
}
"#;
fs::write(&input_file, initial).unwrap();
let out = langcodec_cmd()
.args([
"edit",
"set",
"-i",
input_file.to_str().unwrap(),
"--lang",
"en",
"-k",
"game_title",
"-v",
"Game Updated",
])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let content = fs::read_to_string(&input_file).unwrap();
let payload: serde_json::Value = serde_json::from_str(&content).unwrap();
let reward_key = "The following rewards have been sent to your backpack.";
assert_eq!(
payload["strings"][reward_key]["comment"],
"Text displayed in the tips view of the return user reward dialog, describing the rewards that have been sent to the user's backpack."
);
assert_eq!(
payload["strings"][reward_key]["isCommentAutoGenerated"],
serde_json::Value::Bool(true)
);
assert_eq!(
payload["strings"]["game_title"]["localizations"]["en"]["stringUnit"]["value"],
"Game Updated"
);
}