feat: phase 2 — i18n messages EN/FR (--lang, AM_LANG/LANG, catalogue tr/tr_fmt, REPL lang) (closes #74)

This commit is contained in:
2026-08-18 15:46:51 -04:00
parent 4c7866a25a
commit 7f4d74f811
19 changed files with 852 additions and 47 deletions
+79 -15
View File
@@ -120,6 +120,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("plugins", "list the event plugins and test one (JSON contract on stdin/stdout)"),
("shell", "show or switch the system shell"),
("theme", "show or switch the color theme"),
("lang", "show or switch the output language (fr/en)"),
("tip", "cheat sheet of the most useful commands"),
("ls", "list files and directories (Nushell-style)"),
("dir", "list files and directories (same as ls)"),
@@ -229,7 +230,7 @@ impl AmCompleter {
commands: vec![
"list", "status", "sessions", "stats", "top", "report", "projects", "timeline", "log", "logs", "history", "init", "alias", "secret", "open", "watch", "search", "info", "install", "uninstall", "update",
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "theme",
"self-update", "self-uninstall", "export", "import", "shell", "theme", "lang",
"tip", "dashboard", "favorite", "unfavorite", "note", "tag", "untag", "tags",
"profile", "man", "models", "catalog", "suggest", "audit",
"service", "schedule", "monitor", "sync", "migrate", "playbook", "lab", "plugins",
@@ -296,6 +297,9 @@ impl AmCompleter {
.map(|s| s.to_string())
.collect();
}
"lang" => {
pool = vec!["fr".to_string(), "en".to_string()];
}
"cd" | "chdir" => {
// Complete directories only — dive one level per Tab.
let cwd = current_dir();
@@ -637,6 +641,7 @@ pub fn banner_box(
session: &ShellSession,
session_id: &str,
cwd: &str,
lang: &str,
theme: &Theme,
color: bool,
) -> String {
@@ -791,7 +796,7 @@ pub fn banner_box(
" system self-update · self-uninstall · export · import".to_string(),
));
rows.push(inner(
" shell ls · dir · cd · ps · where · get · shell · theme · help · version · exit"
" shell ls · dir · cd · ps · where · get · shell · theme · lang · help · version · exit"
.to_string(),
));
@@ -799,9 +804,10 @@ pub fn banner_box(
rows.push(blank());
rows.push(inner(dim(
&format!(
"am v{version} · {agents} agents · {external} external · {installed} managed · shell {} · theme {} · /help for commands",
"am v{version} · {agents} agents · {external} external · {installed} managed · shell {} · theme {} · lang {} · /help for commands",
session.current.name,
theme.name
theme.name,
lang,
),
theme,
color,
@@ -831,6 +837,7 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
session,
session_id,
&current_dir_string(),
app.lang().code(),
theme,
color,
);
@@ -1255,6 +1262,10 @@ fn handle_input(
theme_command(app, None);
return Ok(false);
}
"lang" | "/lang" => {
lang_command(app, None);
return Ok(false);
}
_ => {}
}
// History expansion: !! !N !prefix ^old^new (re-execution, issue #27).
@@ -1274,6 +1285,7 @@ fn handle_input(
match cmd {
"shell" => shell_command(session, arg),
"theme" => theme_command(app, arg),
"lang" => lang_command(app, arg),
"help" => match arg {
Some(topic) => {
if let Err(e) = crate::help::print_command(app, topic) {
@@ -1284,7 +1296,7 @@ fn handle_input(
},
_ => app
.log
.error(&format!("unknown command '/{cmd}' — type '/help'")),
.error(crate::tr_fmt!("unknown command '/{}' — type '/help'", cmd)),
}
return Ok(false);
}
@@ -1354,6 +1366,11 @@ fn handle_line(
theme_command(app, rest.first().map(|s| s.as_str()));
return Ok(false);
}
// lang | lang <fr|en>: show the output language or switch it (issue #74).
if cmd == "lang" {
lang_command(app, rest.first().map(|s| s.as_str()));
return Ok(false);
}
// Session directory change: kept in the am process, not a child shell.
if matches!(cmd, "cd" | "chdir") {
@@ -1450,7 +1467,7 @@ fn handle_line(
}),
Some("list") | None => Command::Secret(crate::cli::SecretCmd::List),
_ => {
app.log.error("usage: secret set NAME --agent A --value V | secret unset NAME --agent A | secret list");
app.log.error(crate::i18n::tr("usage: secret set NAME --agent A --value V | secret unset NAME --agent A | secret list"));
return Ok(false);
}
},
@@ -1467,13 +1484,13 @@ fn handle_line(
}),
Some("list") => Command::Catalog(crate::cli::CatalogCmd::List),
_ => {
app.log.error("usage: catalog update | catalog add <url> | catalog list");
app.log.error(crate::i18n::tr("usage: catalog update | catalog add <url> | catalog list"));
return Ok(false);
}
},
"suggest" => {
if rest.is_empty() {
app.log.error("usage: suggest <requête> — ex: suggest un agent pour du Python");
app.log.error(crate::i18n::tr("usage: suggest <requête> — ex: suggest un agent pour du Python"));
return Ok(false);
}
Command::Suggest {
@@ -1490,7 +1507,7 @@ fn handle_line(
}),
Some("list") | None => Command::Alias(crate::cli::AliasCmd::List),
_ => {
app.log.error("usage: alias add <name> <target> | alias remove <name> | alias list");
app.log.error(crate::i18n::tr("usage: alias add <name> <target> | alias remove <name> | alias list"));
return Ok(false);
}
},
@@ -1525,7 +1542,8 @@ fn handle_line(
Some("list") | None => Command::Profile(crate::cli::ProfileCmd::List),
other => {
app.log.error(&format!(
"usage: profile list | profile show <name>{}",
"{}{}",
crate::i18n::tr("usage: profile list | profile show <name>"),
other.map(|o| format!(" (unknown 'profile {o}')")).unwrap_or_default()
));
return Ok(false);
@@ -1611,7 +1629,7 @@ fn handle_line(
},
"playbook" => {
if rest.is_empty() {
app.log.error("usage: playbook <file> — ex: playbook deploy.yaml");
app.log.error(crate::i18n::tr("usage: playbook <file> — ex: playbook deploy.yaml"));
return Ok(false);
}
Command::Playbook {
@@ -1747,7 +1765,7 @@ fn handle_line(
id: rest[1].clone(),
}),
_ => {
app.log.error("usage: schedule add <cmd...> | schedule list | schedule remove <id>");
app.log.error(crate::i18n::tr("usage: schedule add <cmd...> | schedule list | schedule remove <id>"));
return Ok(false);
}
},
@@ -1793,7 +1811,7 @@ fn handle_line(
},
other => {
app.log
.error(&format!("unknown command '{other}' — type 'help'"));
.error(crate::tr_fmt!("unknown command '{}' — type 'help'", other));
return Ok(false);
}
};
@@ -1808,7 +1826,7 @@ fn handle_line(
fn is_shell_command(word: &str) -> bool {
matches!(
word,
"ls" | "dir" | "ps" | "where" | "get" | "cd" | "shell" | "theme" | "help" | "version" | "exit"
"ls" | "dir" | "ps" | "where" | "get" | "cd" | "shell" | "theme" | "lang" | "help" | "version" | "exit"
)
}
@@ -2032,7 +2050,7 @@ fn run_system(app: &App, session: &ShellSession, line: &str) -> Result<()> {
let code = session.run(line)?;
if code != 0 {
app.log
.error(&format!("command exited with code {code}"));
.error(crate::tr_fmt!("command exited with code {}", code));
}
app.emit(
&Event::now(EventKind::Shell)
@@ -2130,6 +2148,48 @@ fn print_theme_status(app: &App) {
);
}
/// Show or switch the output language (issue #74). With no argument the
/// current locale is shown; with 'fr' or 'en' it is switched for the rest
/// of the session (the i18n global follows immediately).
fn lang_command(app: &App, arg: Option<&str>) {
match arg.map(str::trim).filter(|s| !s.is_empty()) {
None => print_lang_status(app),
Some(code) => match code.to_ascii_lowercase().as_str() {
"fr" | "french" | "français" => {
app.set_lang(crate::i18n::Lang::Fr);
println!("language: {}", crate::i18n::Lang::Fr.label());
}
"en" | "english" | "anglais" => {
app.set_lang(crate::i18n::Lang::En);
println!("language: {}", crate::i18n::Lang::En.label());
}
other => app.log.error(format!(
"unknown language '{other}' — use 'lang fr' or 'lang en'"
)),
},
}
}
/// Show the active language and the switch hint.
fn print_lang_status(app: &App) {
let lang = app.lang();
let color = app.color();
let theme = app.theme();
let hdr = |s: &str| -> String {
if color {
theme.hdr(s)
} else {
s.to_string()
}
};
println!("{}", hdr("Language"));
println!(" ◉ {} ({})", theme.acc(lang.label()), lang.code());
println!(
" {}",
theme.dimmed("'lang <fr|en>' to switch (this session) · 'am --lang <fr|en>' at startup")
);
}
fn print_shell_status(session: &ShellSession) {
let others: Vec<&str> = session
.available
@@ -2296,6 +2356,7 @@ mod tests {
&session,
"20260815_123456_abcdef",
"/home/alice/dev/agent-manager",
"fr",
crate::theme::default_theme(),
false,
);
@@ -2333,6 +2394,7 @@ mod tests {
&session,
"20260815_123456_abcdef",
"/home/alice/dev/agent-manager",
"fr",
crate::theme::default_theme(),
false,
);
@@ -2360,6 +2422,7 @@ mod tests {
&session,
"20260815_123456_abcdef",
"/home/alice/dev/agent-manager",
"fr",
ocean,
true,
);
@@ -2463,6 +2526,7 @@ mod tests {
&session,
"20260815_123456_abcdef",
"/home/alice/dev/agent-manager",
"fr",
ocean,
true,
);