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
+2 -2
View File
@@ -34,7 +34,7 @@ pub fn generate(dir: &Path, force: bool, template: Option<&str>) -> Result<PathB
.collect::<Vec<_>>()
.join(", ");
let stack = if ctx.stack.is_empty() {
"aucune détectée".to_string()
crate::i18n::tr("aucune détectée").to_string()
} else {
ctx.stack.join(", ")
};
@@ -82,7 +82,7 @@ pub fn run(app: &App, force: bool, template: Option<&str>) -> Result<i32> {
"wrote {} (stack: {})",
path.display(),
if ctx.stack.is_empty() {
"aucune détectée".to_string()
crate::i18n::tr("aucune détectée").to_string()
} else {
ctx.stack.join(", ")
}
+10 -4
View File
@@ -109,16 +109,22 @@ pub fn run(app: &App) -> Result<i32> {
let scheduled = schedule_locked(&locked);
if !scheduled.is_empty() {
println!(
"{} {} entrée(s) verrouillée(s) — suppression planifiée au prochain redémarrage",
"{} {}",
"locked:".yellow(),
scheduled.len()
crate::tr_fmt!(
"{} entrée(s) verrouillée(s) — suppression planifiée au prochain redémarrage",
scheduled.len()
)
);
}
if scheduled.len() != locked.len() {
println!(
"{} {} entrée(s) verrouillée(s) — le processus différé les supprimera après la fermeture de am",
"{} {}",
"deferred:".cyan(),
locked.len() - scheduled.len()
crate::tr_fmt!(
"{} entrée(s) verrouillée(s) — le processus différé les supprimera après la fermeture de am",
locked.len() - scheduled.len()
)
);
}
}
+27 -7
View File
@@ -119,12 +119,25 @@ pub fn report_md(app: &App, days: u32) -> String {
}
let sessions = crate::sessions::load(app).map(|s| s.sessions.len()).unwrap_or(0);
let top_rows = top(app, None).unwrap_or_default();
let mut md = format!(
"# Rapport agent-manager — {days} derniers jours\n\n- Lancements : {total_launches}\n- Échecs : {failures}\n- Changements (install/update/uninstall) : {changes}\n- Durée cumulée : {}s\n- Sessions enregistrées : {sessions}\n",
duration
);
// Issue #74: the report follows the active language.
let md = if app.lang().is_en() {
format!(
"# agent-manager report — last {days} days\n\n- Launches: {total_launches}\n- Failures: {failures}\n- Changes (install/update/uninstall): {changes}\n- Cumulative duration: {}s\n- Recorded sessions: {sessions}\n",
duration
)
} else {
format!(
"# Rapport agent-manager — {days} derniers jours\n\n- Lancements : {total_launches}\n- Échecs : {failures}\n- Changements (install/update/uninstall) : {changes}\n- Durée cumulée : {}s\n- Sessions enregistrées : {sessions}\n",
duration
)
};
let mut md = md;
if !top_rows.is_empty() {
md.push_str("\n## Top agents\n\n| Agent | Lancements | Durée | Échecs |\n|---|---|---|---|\n");
md.push_str(if app.lang().is_en() {
"\n## Top agents\n\n| Agent | Launches | Duration | Failures |\n|---|---|---|---|\n"
} else {
"\n## Top agents\n\n| Agent | Lancements | Durée | Échecs |\n|---|---|---|---|\n"
});
for r in &top_rows {
md.push_str(&format!(
"| {} | {} | {}s | {} |\n",
@@ -137,8 +150,15 @@ pub fn report_md(app: &App, days: u32) -> String {
if !rows.is_empty() {
let t = crate::costs::totals(&rows);
md.push_str(&format!(
"\n## Coûts estimés\n\n- Sessions facturées : {}\n- Tokens : {} in / {} out\n- Coût estimé : ${:.4}\n",
t.sessions, t.tokens_in, t.tokens_out, t.cost_usd
"{}\n\n- {}: {}\n- {}: {} in / {} out\n- {}: ${:.4}\n",
if app.lang().is_en() { "\n## Estimated costs" } else { "\n## Coûts estimés" },
if app.lang().is_en() { "Billed sessions" } else { "Sessions facturées" },
t.sessions,
if app.lang().is_en() { "Tokens" } else { "Tokens" },
t.tokens_in,
t.tokens_out,
if app.lang().is_en() { "Estimated cost" } else { "Coût estimé" },
t.cost_usd
));
}
}
+7 -2
View File
@@ -111,13 +111,18 @@ pub fn run(app: &App, query: &str) -> Result<i32> {
));
} else {
app.log.info(&format!(
"no agent matches '{query}' — vouliez-vous dire : {} ?",
"{} — {} : {} ?",
crate::tr_fmt!("no agent matches '{}'", query),
crate::i18n::tr("vouliez-vous dire"),
suggestions.join(", ")
));
}
return Ok(0);
}
println!("suggestions for \"{query}\":");
println!(
"{} \"{query}\":",
crate::i18n::tr("suggestions for")
);
let mut table = crate::output::Table::new(vec!["AGENT", "CATEGORY", "WHY"]);
for r in &rows {
table.row(vec![
+34 -10
View File
@@ -435,7 +435,11 @@ pub fn render(app: &App, random: bool) -> String {
};
if random {
let (title, body) = random_tip();
return format!("💡 {} — {}\n", p.acc(title), body);
return format!(
"💡 {} — {}\n",
p.acc(crate::i18n::tr_in(app.lang(), title)),
crate::i18n::tr_in(app.lang(), body)
);
}
let mut out = String::new();
out.push_str(&header(app, &p));
@@ -444,7 +448,7 @@ pub fn render(app: &App, random: bool) -> String {
out.push_str(&section_rule(app, &p, s.icon, s.title));
out.push('\n');
for e in s.entries {
out.push_str(&entry(&p, e));
out.push_str(&entry(&p, app.lang(), e));
}
out.push('\n');
}
@@ -453,11 +457,18 @@ pub fn render(app: &App, random: bool) -> String {
let (title, body) = daily_tip();
out.push_str(&section_rule(app, &p, "💡", "Le saviez-vous ?"));
out.push('\n');
out.push_str(&format!(" {} — {}\n", p.acc(title), body));
out.push_str(&format!(
" {} — {}\n",
p.acc(crate::i18n::tr_in(app.lang(), title)),
crate::i18n::tr_in(app.lang(), body)
));
out.push('\n');
out.push_str(&format!(
" {}\n",
p.dim("am help <commande> pour le détail · am tip --random pour une astuce isolée · dans le shell am, tapez simplement 'tip'")
p.dim(crate::i18n::tr_in(
app.lang(),
"am help <commande> pour le détail · am tip --random pour une astuce isolée · dans le shell am, tapez simplement 'tip'"
))
));
out
}
@@ -528,17 +539,26 @@ fn header(app: &App, p: &Painter) -> String {
/// Section separator: " 🚀 Lancer & piloter ────────".
fn section_rule(app: &App, p: &Painter, icon: &str, title: &str) -> String {
let w = page_width(app);
let label = format!(" {icon} {title} ");
let label = format!(" {icon} {} ", crate::i18n::tr_in(app.lang(), title));
let dashes = "─".repeat(w.saturating_sub(label.chars().count()).max(1));
format!("{}{}", p.hdr(&label), p.frame(&dashes))
}
/// One cheat-sheet entry: usage line, options, example.
fn entry(p: &Painter, e: &TipEntry) -> String {
fn entry(p: &Painter, lang: crate::i18n::Lang, e: &TipEntry) -> String {
let mut out = String::new();
out.push_str(&format!(" {} {} {}\n", p.inf("▸"), p.acc(e.usage), e.about));
out.push_str(&format!(
" {} {} {}\n",
p.inf("▸"),
p.acc(e.usage),
crate::i18n::tr_in(lang, e.about)
));
for (flag, desc) in e.options {
out.push_str(&format!(" {} {}\n", p.dim(flag), desc));
out.push_str(&format!(
" {} {}\n",
p.dim(flag),
crate::i18n::tr_in(lang, desc)
));
}
if !e.example.is_empty() {
out.push_str(&format!(" {} {}\n", p.dim("❯"), p.val(e.example)));
@@ -593,8 +613,12 @@ fn most_used_section(app: &App, p: &Painter) -> String {
out.push('\n');
if top.is_empty() {
out.push_str(&format!(
" {} — lancez le shell am et tapez quelques commandes : vos habitudes apparaîtront ici.\n",
p.dim("aucune commande enregistrée")
" {} — {}\n",
p.dim(crate::i18n::tr_in(app.lang(), "aucune commande enregistrée")),
crate::i18n::tr_in(
app.lang(),
"lancez le shell am et tapez quelques commandes : vos habitudes apparaîtront ici"
)
));
return out;
}