feat: phase 2 — i18n messages EN/FR (--lang, AM_LANG/LANG, catalogue tr/tr_fmt, REPL lang) (closes #74)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
//! i18n (issue #74): locale selection, English output, stable JSON contracts.
|
||||
//!
|
||||
//! The tests spawn the real am binary (CARGO_BIN_EXE_am) so the assertions
|
||||
//! cover the actual command output, not just the library layer.
|
||||
|
||||
mod common;
|
||||
|
||||
use agent_manager::i18n::{self, Lang};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// Run the real am binary with args and a temp config; returns
|
||||
/// (stdout, stderr, exit code).
|
||||
fn am(args: &[&str], config: &PathBuf) -> (String, String, i32) {
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_am"));
|
||||
cmd.arg("--config").arg(config).args(args);
|
||||
let out = cmd.output().expect("am binary should run");
|
||||
(
|
||||
String::from_utf8_lossy(&out.stdout).to_string(),
|
||||
String::from_utf8_lossy(&out.stderr).to_string(),
|
||||
out.status.code().unwrap_or(-1),
|
||||
)
|
||||
}
|
||||
|
||||
/// A minimal valid config in a fresh temp dir (no user config leakage).
|
||||
fn temp_config(tag: &str) -> (PathBuf, PathBuf) {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"am-i18n-{tag}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.subsec_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let cfg = dir.join("config.yaml");
|
||||
std::fs::write(
|
||||
&cfg,
|
||||
"version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\nagents: []\n",
|
||||
)
|
||||
.unwrap();
|
||||
(cfg, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lang_flag_selects_english_for_tip_content() {
|
||||
let (cfg, _dir) = temp_config("flag");
|
||||
// Explicit French: the tip page is French.
|
||||
let (fr, _, _) = am(&["--lang", "fr", "tip"], &cfg);
|
||||
assert!(fr.contains("Lancer & piloter"), "fr tip missing: {fr}");
|
||||
// --lang en: the same page is translated.
|
||||
let (en, _, code) = am(&["--lang", "en", "tip"], &cfg);
|
||||
assert_eq!(code, 0);
|
||||
assert!(en.contains("Run & control"), "en tip missing: {en}");
|
||||
assert!(!en.contains("Lancer & piloter"), "untranslated section: {en}");
|
||||
// The flag works after the subcommand too (global clap flag).
|
||||
let (en2, _, _) = am(&["tip", "--lang", "en"], &cfg);
|
||||
assert!(en2.contains("Run & control"), "en tip (post) missing: {en2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn am_lang_env_selects_english() {
|
||||
let (cfg, _dir) = temp_config("env");
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_am"));
|
||||
cmd.arg("--config").arg(&cfg).arg("tip");
|
||||
cmd.env("AM_LANG", "en");
|
||||
let out = cmd.output().unwrap();
|
||||
let text = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
assert!(text.contains("Run & control"), "AM_LANG=en ignored: {text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lang_env_values_are_parsed_leniently() {
|
||||
// The host machine may export LANG/AM_LANG (git-bash sets LANG=en_GB):
|
||||
// clear them so the default resolution is exercised deterministically.
|
||||
std::env::remove_var("AM_LANG");
|
||||
std::env::remove_var("LANG");
|
||||
std::env::remove_var("LC_ALL");
|
||||
std::env::remove_var("LC_MESSAGES");
|
||||
assert_eq!(i18n::resolve(None), Lang::Fr); // no env -> default French
|
||||
std::env::set_var("AM_LANG", "en_US.UTF-8");
|
||||
assert_eq!(i18n::resolve(None), Lang::En);
|
||||
std::env::remove_var("AM_LANG");
|
||||
// The default follows the host LANG when set (spec: locale selon env).
|
||||
std::env::set_var("LANG", "fr_FR.UTF-8");
|
||||
assert_eq!(i18n::resolve(None), Lang::Fr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_remembers_the_selected_language() {
|
||||
let app = common::test_app("applang", &["--lang", "en", "list"]);
|
||||
assert_eq!(app.lang(), Lang::En);
|
||||
assert!(i18n::active().is_en());
|
||||
let app_fr = common::test_app("applangfr", &["--lang", "fr", "list"]);
|
||||
assert_eq!(app_fr.lang(), Lang::Fr);
|
||||
assert!(!i18n::active().is_en());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_contract_is_identical_across_languages() {
|
||||
// Issue #74 acceptance: --json field names and values are untouched by
|
||||
// the language switch — only display strings are translated.
|
||||
let (cfg, _dir) = temp_config("json");
|
||||
let (fr, _, code_fr) = am(&["list", "--json"], &cfg);
|
||||
let (en, _, code_en) = am(&["list", "--json", "--lang", "en"], &cfg);
|
||||
assert_eq!(code_fr, 0);
|
||||
assert_eq!(code_en, 0);
|
||||
assert_eq!(fr, en, "list --json differs across languages");
|
||||
// Same for status --json (structured detail).
|
||||
let (fr_s, _, _) = am(&["status", "--json"], &cfg);
|
||||
let (en_s, _, _) = am(&["status", "--json", "--lang", "en"], &cfg);
|
||||
assert_eq!(fr_s, en_s, "status --json differs across languages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_reports_stack_in_english() {
|
||||
let (cfg, dir) = temp_config("init");
|
||||
let work = dir.join("proj");
|
||||
std::fs::create_dir_all(&work).unwrap();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_am"))
|
||||
.arg("--config")
|
||||
.arg(&cfg)
|
||||
.args(["--lang", "en", "init", "--force"])
|
||||
.current_dir(&work)
|
||||
.output()
|
||||
.unwrap();
|
||||
let text = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(
|
||||
text.contains("none detected"),
|
||||
"EN init message missing: {text}"
|
||||
);
|
||||
}
|
||||
+3
-1
@@ -8,7 +8,9 @@ use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn tip_page_renders_header_sections_and_options() {
|
||||
let app = common::test_app("tip", &["tip"]);
|
||||
// Issue #74: the page follows the locale; force French here so the
|
||||
// assertions are deterministic whatever the host LANG says.
|
||||
let app = common::test_app("tip", &["--lang", "fr", "tip"]);
|
||||
let text = tip_cmd::render(&app, false);
|
||||
assert!(text.contains("TIP"), "header missing:\n{text}");
|
||||
assert!(text.contains("antisèche"), "subtitle missing:\n{text}");
|
||||
|
||||
@@ -45,6 +45,9 @@ fn report_md_contains_numbers_and_table() {
|
||||
];
|
||||
let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
|
||||
let app = app_with_events("report", &refs);
|
||||
// Issue #74: the report follows the app language; force French so the
|
||||
// assertions are deterministic whatever the host LANG says.
|
||||
app.set_lang(agent_manager::i18n::Lang::Fr);
|
||||
let md = stats_cmd::report_md(&app, 7);
|
||||
assert!(md.contains("# Rapport agent-manager"), "md: {md}");
|
||||
assert!(md.contains("Lancements : 2"));
|
||||
|
||||
Reference in New Issue
Block a user