agy-powerline 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.7.0] - 2026-10-24
9
+
10
+ ### Added
11
+
12
+ - `"language"` setting for the bar's own words (labels, `rem`, units), set with `init --language` or tried with `render --language`; a code without messages shows English (#102)
13
+ - The bar in French with `"language": "fr"`, and agy-powerline.com/fr/ shows its bars in French (#148)
14
+ - agy-powerline.com/.well-known/security.txt sends security reports to GitHub's private vulnerability reporting
15
+
16
+ ### Changed
17
+
18
+ - agy-powerline.com loads smaller images: each screenshot comes in smaller copies the browser picks from by the width it shows, the feature icons have a 56 px version, and the WebP images carry no color profile
19
+
8
20
  ## [1.6.0] - 2026-10-24
9
21
 
10
22
  ### Added
package/README.fr.md CHANGED
@@ -135,6 +135,16 @@ Prévisualisez tous les thèmes avec `npx agy-powerline theme --gallery`.
135
135
 
136
136
  </details>
137
137
 
138
+ ### Langue de la barre
139
+
140
+ Les mots de la barre elle-même (`Réinit`, `rest.`, les unités de `2h 47m`) peuvent s’afficher en français :
141
+
142
+ ```sh
143
+ npx agy-powerline init --language fr
144
+ ```
145
+
146
+ ou `"language": "fr"` dans `statusline.config.json` (touche `u` dans `configure`). Le nom du modèle, la branche et tout ce qu’envoie agy restent tels quels ; les commandes et `configure` restent en anglais. Détails dans la [documentation de configuration](docs/configuration.md#language).
147
+
138
148
  ## Widgets
139
149
 
140
150
  | Widget | Informations affichées |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-powerline",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "agy statusline for Google Antigravity CLI: live Gemini, Claude/GPT and Argon quota pacing, git, themes and an interactive TUI configurator",
5
5
  "bin": {
6
6
  "agy-powerline": "src/index.js"
@@ -24,6 +24,7 @@ const {
24
24
  } = require("../lib/shared");
25
25
  const { resolveInstallDir } = require("../lib/paths");
26
26
  const { WIDGET_TAGS } = require("../lib/widget-style");
27
+ const { SUPPORTED_LANGUAGES, resolveLanguage } = require("../lib/i18n");
27
28
  const { editFields: getEditFields, stepNumberField } = require("../lib/widget-options");
28
29
  const { loadEditableConfig } = require("./config-guard");
29
30
  const {
@@ -427,6 +428,10 @@ function previewParts(width) {
427
428
  if (preview.layout === "pills") {
428
429
  chips.unshift(ui.chip("joins", preview.powerline.enabled, key("j")));
429
430
  }
431
+ // Only worth a chip once there is a language to switch to.
432
+ if (SUPPORTED_LANGUAGES.length > 1) {
433
+ chips.push(ui.chip(`lang ${resolveLanguage(preview.language)}`, true, key("u")));
434
+ }
430
435
 
431
436
  return { title, bar: String(rendered).split("\n"), chips: chips.join(" ") };
432
437
  }
@@ -1629,6 +1634,15 @@ function onMain(key) {
1629
1634
  if (persist()) {
1630
1635
  flashMessage = `Pace marker: ${config.pacing ? "on" : "off"}`;
1631
1636
  }
1637
+ return;
1638
+ }
1639
+
1640
+ if (key.name === "u" && SUPPORTED_LANGUAGES.length > 1) {
1641
+ const next = SUPPORTED_LANGUAGES.indexOf(resolveLanguage(config.language)) + 1;
1642
+ config.language = SUPPORTED_LANGUAGES[next % SUPPORTED_LANGUAGES.length];
1643
+ if (persist()) {
1644
+ flashMessage = `Bar language: ${config.language}`;
1645
+ }
1632
1646
  }
1633
1647
  }
1634
1648
 
package/src/cli/doctor.js CHANGED
@@ -28,6 +28,7 @@ const { runStatuslineCommand } = require("../lib/run");
28
28
  const { detectColorDepth } = require("../lib/colors");
29
29
  const { readLog, getLogPath } = require("../lib/log");
30
30
  const { USAGE_PERIODS, usageAvailable } = require("../lib/usage");
31
+ const { matchLanguage, SUPPORTED_LANGUAGES } = require("../lib/i18n");
31
32
  const { version } = require("../../package.json");
32
33
 
33
34
  const DIM = "\x1b[2m";
@@ -78,7 +79,7 @@ function stripAnsi(text) {
78
79
  function renderOverridesInCommand(command) {
79
80
  const names = [];
80
81
  const pattern =
81
- /(?:^|\s)--(preset|layout|theme|caps|gradient)(?:(?:\s*=\s*|\s+)(?:"([^"]*)"|'([^']*)'|([^\s-][^\s]*)))?/g;
82
+ /(?:^|\s)--(preset|layout|theme|caps|gradient|language)(?:(?:\s*=\s*|\s+)(?:"([^"]*)"|'([^']*)'|([^\s-][^\s]*)))?/g;
82
83
 
83
84
  for (const match of String(command).matchAll(pattern)) {
84
85
  const value = match[2] ?? match[3] ?? match[4] ?? "(missing)";
@@ -193,6 +194,17 @@ async function doctorCommand(options = {}) {
193
194
  console.log(" tokensToday and tokensWeek stay hidden until Node is updated.");
194
195
  }
195
196
 
197
+ // Not a failure either: the bar shows English.
198
+ const language = configState.raw?.language;
199
+ if (language !== undefined && !matchLanguage(language)) {
200
+ const shown = String(language)
201
+ .replace(/[^A-Za-z0-9_-]/g, "?")
202
+ .slice(0, 32);
203
+ console.log();
204
+ console.log(`${YELLOW} language "${shown}" isn't available${RESET}, so the bar shows English.`);
205
+ console.log(` Available: ${SUPPORTED_LANGUAGES.join(", ")}`);
206
+ }
207
+
196
208
  const breaking = cmdBreakingChars(installDir);
197
209
  if (breaking.length) {
198
210
  failed += 1;
package/src/cli/init.js CHANGED
@@ -19,6 +19,7 @@ const {
19
19
  THEME_ORDER,
20
20
  canonicalThemeId,
21
21
  } = require("../lib/shared");
22
+ const { matchLanguage, SUPPORTED_LANGUAGES } = require("../lib/i18n");
22
23
  const {
23
24
  AGY_SETTINGS,
24
25
  RUNTIME_FILES,
@@ -89,7 +90,7 @@ function unknownChoice(kind, value, ids) {
89
90
  return null;
90
91
  }
91
92
 
92
- // --preset, --layout and --theme, checked before anything is written. Returns
93
+ // --preset, --layout, --theme and --language, checked before anything is written. Returns
93
94
  // the ids given ({} for none), or null after printing what's wrong.
94
95
  function readChoiceFlags(options) {
95
96
  const choices = {};
@@ -113,6 +114,16 @@ function readChoiceFlags(options) {
113
114
  }
114
115
  choices.theme = theme;
115
116
  }
117
+ if (options.language !== undefined) {
118
+ const language = matchLanguage(options.language);
119
+ if (!language) {
120
+ console.error(`${RED}error${RESET} Unknown language "${options.language}".`);
121
+ console.error(` Available languages: ${SUPPORTED_LANGUAGES.join(", ")}`);
122
+ process.exitCode = 1;
123
+ return null;
124
+ }
125
+ choices.language = language;
126
+ }
116
127
  return choices;
117
128
  }
118
129
 
@@ -124,6 +135,9 @@ function applyChoices(config, choices) {
124
135
  if (choices.theme) {
125
136
  next.theme = choices.theme;
126
137
  }
138
+ if (choices.language) {
139
+ next.language = choices.language;
140
+ }
127
141
  return next;
128
142
  }
129
143
 
package/src/index.js CHANGED
@@ -28,6 +28,7 @@ const {
28
28
  } = require("./lib/shared");
29
29
  const { STYLE_ORDER } = require("./lib/powerline");
30
30
  const { GRADIENT_ORDER } = require("./lib/themes");
31
+ const { SUPPORTED_LANGUAGES } = require("./lib/i18n");
31
32
  const { stripControl } = require("./lib/formatters");
32
33
  const { resolveInstallDir, isTrustedInstallDir } = require("./lib/paths");
33
34
  const { isLogEnabled, setLogDir, logError, jsonErrorMessage } = require("./lib/log");
@@ -75,6 +76,11 @@ const COMMANDS = [
75
76
  value: "<id>",
76
77
  help: "use this theme, or dark, light, colorblind (skips the question)",
77
78
  },
79
+ language: {
80
+ type: "string",
81
+ value: "<code>",
82
+ help: `show the bar in this language (${SUPPORTED_LANGUAGES.join(", ")})`,
83
+ },
78
84
  },
79
85
  run: (_arg, opts) =>
80
86
  initCommand({
@@ -232,6 +238,11 @@ const COMMANDS = [
232
238
  value: "<name|colors>",
233
239
  help: "use a gradient preset, hex stops, or off",
234
240
  },
241
+ language: {
242
+ type: "string",
243
+ value: "<code>",
244
+ help: `use a language for this render (${SUPPORTED_LANGUAGES.join(", ")})`,
245
+ },
235
246
  "dump-payload": {
236
247
  type: "string",
237
248
  value: "<path>",
@@ -384,6 +395,7 @@ function renderOverrideError(name, value) {
384
395
  preset: ["preset", `Available presets: ${PRESET_ORDER.join(", ")}`],
385
396
  caps: ["cap style", `Available styles: ${STYLE_ORDER.join(", ")}`],
386
397
  gradient: ["gradient", `Available gradients: ${GRADIENT_ORDER.join(", ")}, off`],
398
+ language: ["language", `Available languages: ${SUPPORTED_LANGUAGES.join(", ")}`],
387
399
  }[name];
388
400
  console.error(`${RED}error${RESET} Unknown ${what} "${stripControl(String(value))}".`);
389
401
  console.error(` ${list}`);
package/src/lib/config.js CHANGED
@@ -8,6 +8,7 @@ const { stripControl, normalizeClock } = require("./formatters");
8
8
  const { normalizePowerline, applyStyle, STYLES } = require("./powerline");
9
9
  const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
10
10
  const { isMeterStyle } = require("./widget-style");
11
+ const { resolveLanguage, matchLanguage } = require("./i18n");
11
12
 
12
13
  // Display text in widget configs is printed as-is, so strip terminal control
13
14
  // from it. cmd/command are left alone: they are executed, not printed.
@@ -126,6 +127,7 @@ function defaultConfig() {
126
127
  ascii: false,
127
128
  color: "auto",
128
129
  layout: "pills",
130
+ language: "en",
129
131
  pacing: true,
130
132
  minimalist: false,
131
133
  rowGap: false,
@@ -185,6 +187,8 @@ function mergeConfig(raw) {
185
187
  ascii: raw?.ascii === true,
186
188
  color: resolveColorModeId(raw?.color ?? fallback.color),
187
189
  layout: resolveLayoutId(raw?.layout ?? fallback.layout),
190
+ // A language without messages shows English (fr-FR uses fr when there is one).
191
+ language: resolveLanguage(raw?.language),
188
192
  pacing: raw?.pacing !== false,
189
193
  minimalist: raw?.minimalist === true,
190
194
  rowGap: raw?.rowGap === true,
@@ -237,7 +241,7 @@ function readConfigFile() {
237
241
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
238
242
  return { exists: true, config: defaultConfig(), error: "top-level value is not an object" };
239
243
  }
240
- return { exists: true, config: mergeConfig(raw) };
244
+ return { exists: true, config: mergeConfig(raw), raw };
241
245
  } catch (err) {
242
246
  // Newer Node quotes part of the file in the message; it may hold anything.
243
247
  return { exists: true, config: defaultConfig(), error: stripControl(err.message) };
@@ -475,6 +479,15 @@ function applyRenderOverrides(baseConfig, overrides = {}) {
475
479
  }
476
480
  }
477
481
 
482
+ if (overrides.language !== undefined) {
483
+ const language = matchLanguage(overrides.language);
484
+ if (language) {
485
+ config = mergeConfig({ ...config, language });
486
+ } else {
487
+ invalid.push("language");
488
+ }
489
+ }
490
+
478
491
  return { config, invalid };
479
492
  }
480
493
 
package/src/lib/exec.js CHANGED
@@ -8,6 +8,7 @@ const { execSync } = require("child_process");
8
8
  const { stripControl, sanitizeDeep } = require("./formatters");
9
9
  const { RESET, getActiveDepth } = require("./colors");
10
10
  const { logWarning } = require("./log");
11
+ const { t } = require("./i18n");
11
12
  const { PRIVATE_FILE_MODE } = require("./paths");
12
13
 
13
14
  const DEFAULT_TIMEOUT_MS = 500;
@@ -15,10 +16,6 @@ const DEFAULT_INTERVAL_SEC = 10;
15
16
  const MAX_CACHE_AGE_SEC = 3600;
16
17
  const MAX_OUTPUT_CHARS = 80;
17
18
 
18
- // Placeholders shown instead of running the command.
19
- const EXEC_PREVIEW_TEXT = "[cmd]";
20
- const EXEC_BLOCKED_TEXT = "[blocked]";
21
-
22
19
  const memoryCache = new Map();
23
20
 
24
21
  function getCacheFilePath() {
@@ -233,7 +230,8 @@ function execWidgetOutput(widget, session = {}) {
233
230
  return output;
234
231
  }
235
232
 
236
- return session.execMode === "blocked" ? EXEC_BLOCKED_TEXT : EXEC_PREVIEW_TEXT;
233
+ // Placeholders shown instead of running the command.
234
+ return t(session.execMode === "blocked" ? "cmd_blocked" : "cmd_preview", session.lang);
237
235
  }
238
236
 
239
237
  function runExecWidget(widget, session = {}) {
@@ -349,6 +347,4 @@ module.exports = {
349
347
  saveDiskCache,
350
348
  DEFAULT_TIMEOUT_MS,
351
349
  DEFAULT_INTERVAL_SEC,
352
- EXEC_PREVIEW_TEXT,
353
- EXEC_BLOCKED_TEXT,
354
350
  };
@@ -3,12 +3,13 @@
3
3
  const os = require("os");
4
4
  const path = require("path");
5
5
  const { rgb } = require("./colors");
6
+ const { t } = require("./i18n");
6
7
 
7
- function formatReset(seconds) {
8
+ function formatReset(seconds, lang) {
8
9
  seconds = Number(seconds);
9
10
 
10
11
  if (!Number.isFinite(seconds) || seconds <= 0) {
11
- return "now";
12
+ return t("now", lang);
12
13
  }
13
14
 
14
15
  const totalMinutes = Math.floor(seconds / 60);
@@ -17,23 +18,23 @@ function formatReset(seconds) {
17
18
  const minutes = totalMinutes % 60;
18
19
 
19
20
  if (days > 0) {
20
- return `${days}d ${hours}h`;
21
+ return `${days}${t("unit_d", lang)} ${hours}${t("unit_h", lang)}`;
21
22
  }
22
23
 
23
24
  if (hours > 0) {
24
- return `${hours}hr ${minutes}m`;
25
+ return `${hours}${t("unit_hr", lang)} ${minutes}${t("unit_m", lang)}`;
25
26
  }
26
27
 
27
- return `${Math.max(1, minutes)}m`;
28
+ return `${Math.max(1, minutes)}${t("unit_m", lang)}`;
28
29
  }
29
30
 
30
- function formatSessionTime(startMs, nowMs = Date.now()) {
31
+ function formatSessionTime(startMs, nowMs = Date.now(), lang) {
31
32
  const start = Number(startMs);
32
33
  const now = Number(nowMs);
33
34
  if (!Number.isFinite(start) || start <= 0 || !Number.isFinite(now) || now - start < 60000) {
34
35
  return "";
35
36
  }
36
- return formatReset((now - start) / 1000);
37
+ return formatReset((now - start) / 1000, lang);
37
38
  }
38
39
 
39
40
  const MONTHS_SHORT = [
@@ -274,22 +275,22 @@ function shortModelName(name, ascii = false) {
274
275
  }
275
276
 
276
277
  // Session input and output tokens: "88.2K↑ 61.1K↓", or "88.2K in 61.1K out".
277
- function formatTokenSplit(input, output, ascii = false) {
278
+ function formatTokenSplit(input, output, ascii = false, lang) {
278
279
  return ascii
279
- ? `${formatTokens(input)} in ${formatTokens(output)} out`
280
+ ? `${formatTokens(input)} ${t("tokens_in", lang)} ${formatTokens(output)} ${t("tokens_out", lang)}`
280
281
  : `${formatTokens(input)}↑ ${formatTokens(output)}↓`;
281
282
  }
282
283
 
283
284
  // The usage widgets' text: "1.2M", or "1.0M↑ 245.3K↓" with "split", plus the
284
285
  // share of a "budget" when one is set ("1.2M (60%)"). null with no tokens.
285
- function formatUsage(tokens, widget = {}, ascii = false) {
286
+ function formatUsage(tokens, widget = {}, ascii = false, lang) {
286
287
  const input = Number(tokens?.input) || 0;
287
288
  const output = Number(tokens?.output) || 0;
288
289
  const total = input + output;
289
290
  if (!(total > 0)) {
290
291
  return null;
291
292
  }
292
- const value = widget?.split ? formatTokenSplit(input, output, ascii) : formatTokens(total);
293
+ const value = widget?.split ? formatTokenSplit(input, output, ascii, lang) : formatTokens(total);
293
294
  const budget = widget?.budget;
294
295
  const pct =
295
296
  typeof budget === "number" && Number.isFinite(budget) && budget > 0
@@ -620,9 +621,11 @@ function formatGitOperation(op) {
620
621
  }
621
622
 
622
623
  // "2 subagents, 1 task", or "" when nothing runs.
623
- function formatTasks(subagents, tasks) {
624
- const part = (n, word) => (n > 0 ? `${n} ${word}${n === 1 ? "" : "s"}` : "");
625
- return [part(subagents, "subagent"), part(tasks, "task")].filter(Boolean).join(", ");
624
+ function formatTasks(subagents, tasks, lang) {
625
+ const part = (n, one, many) => (n > 0 ? `${n} ${t(n === 1 ? one : many, lang)}` : "");
626
+ return [part(subagents, "subagent", "subagents"), part(tasks, "task", "tasks")]
627
+ .filter(Boolean)
628
+ .join(", ");
626
629
  }
627
630
 
628
631
  // "accept-edits, NORMAL", or "" in the default mode with Vim editing off.
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ // The bar's own words: widget tags, captions and units. Payload text (model,
4
+ // branch, paths, agent state), git operation names, numbers and dates are
5
+ // never translated; dates follow clock.locale instead.
6
+ const MESSAGES = {
7
+ en: {
8
+ tag_model: "Model",
9
+ tag_quota: "Quota",
10
+ tag_5h: "5h",
11
+ tag_7d: "7d",
12
+ tag_reset: "Reset",
13
+ tag_3p_5h: "3P 5h",
14
+ tag_3p_7d: "3P 7d",
15
+ tag_argon_7d: "Argon 7d",
16
+ tag_argon_reset: "Argon reset",
17
+ tag_context: "Context",
18
+ tag_dir: "Dir",
19
+ tag_project: "Project",
20
+ tag_host: "Host",
21
+ tag_lines: "Lines",
22
+ tag_files: "Files",
23
+ tag_commit: "Commit",
24
+ tag_stash: "Stash",
25
+ tag_worktree: "Worktree",
26
+ tag_plan: "Plan",
27
+ tag_agent: "Agent",
28
+ tag_artifacts: "Artifacts",
29
+ tag_session: "Session",
30
+ tag_ram: "RAM",
31
+ tag_cache_read: "Cache read",
32
+ tag_cache_create: "Cache create",
33
+ tag_cache_hit: "Cache hit",
34
+ tag_tokens: "Tokens",
35
+ tag_clock: "Clock",
36
+ tag_today: "Today",
37
+
38
+ rem: "rem",
39
+ over_200k: ">200k",
40
+ now: "now",
41
+ unit_d: "d",
42
+ unit_h: "h",
43
+ unit_hr: "hr",
44
+ unit_m: "m",
45
+ subagent: "subagent",
46
+ subagents: "subagents",
47
+ task: "task",
48
+ tasks: "tasks",
49
+ tokens_in: "in",
50
+ tokens_out: "out",
51
+ sandbox_net: "sandbox (net)",
52
+ sandbox_isolated: "sandbox (isolated)",
53
+ cmd_preview: "[cmd]",
54
+ cmd_blocked: "[blocked]",
55
+ },
56
+ fr: {
57
+ tag_model: "Modèle",
58
+ tag_quota: "Quota",
59
+ tag_5h: "5h",
60
+ tag_7d: "7j",
61
+ tag_reset: "Réinit",
62
+ tag_3p_5h: "Tiers 5h",
63
+ tag_3p_7d: "Tiers 7j",
64
+ tag_argon_7d: "Argon 7j",
65
+ tag_argon_reset: "Réinit Argon",
66
+ tag_context: "Contexte",
67
+ tag_dir: "Dossier",
68
+ tag_project: "Projet",
69
+ tag_host: "Hôte",
70
+ tag_lines: "Lignes",
71
+ tag_files: "Fichiers",
72
+ tag_commit: "Commit",
73
+ tag_stash: "Stash",
74
+ tag_worktree: "Worktree",
75
+ tag_plan: "Offre",
76
+ tag_agent: "Agent",
77
+ tag_artifacts: "Artefacts",
78
+ tag_session: "Session",
79
+ tag_ram: "RAM",
80
+ tag_cache_read: "Cache lu",
81
+ tag_cache_create: "Cache écrit",
82
+ tag_cache_hit: "Succès cache",
83
+ tag_tokens: "Jetons",
84
+ tag_clock: "Heure",
85
+ tag_today: "Aujourd'hui",
86
+
87
+ rem: "rest.",
88
+ over_200k: ">200k",
89
+ now: "maintenant",
90
+ unit_d: "j",
91
+ unit_h: "h",
92
+ unit_hr: "h",
93
+ unit_m: "m",
94
+ subagent: "sous-agent",
95
+ subagents: "sous-agents",
96
+ task: "tâche",
97
+ tasks: "tâches",
98
+ tokens_in: "entrée",
99
+ tokens_out: "sortie",
100
+ sandbox_net: "sandbox (réseau)",
101
+ sandbox_isolated: "sandbox (isolée)",
102
+ cmd_preview: "[cmd]",
103
+ cmd_blocked: "[bloqué]",
104
+ },
105
+ };
106
+
107
+ const DEFAULT_LANGUAGE = "en";
108
+ const SUPPORTED_LANGUAGES = Object.keys(MESSAGES);
109
+
110
+ // The supported code a setting asks for: the code itself (any case), then its
111
+ // base language (fr-FR -> fr), or null.
112
+ function matchLanguage(code) {
113
+ if (typeof code !== "string") {
114
+ return null;
115
+ }
116
+ const wanted = code.trim().toLowerCase();
117
+ const base = wanted.split("-")[0];
118
+ return (
119
+ SUPPORTED_LANGUAGES.find((id) => id.toLowerCase() === wanted) ??
120
+ SUPPORTED_LANGUAGES.find((id) => id.toLowerCase() === base) ??
121
+ null
122
+ );
123
+ }
124
+
125
+ function resolveLanguage(code) {
126
+ return matchLanguage(code) ?? DEFAULT_LANGUAGE;
127
+ }
128
+
129
+ function t(key, lang = DEFAULT_LANGUAGE) {
130
+ return MESSAGES[lang]?.[key] ?? MESSAGES[DEFAULT_LANGUAGE][key] ?? key;
131
+ }
132
+
133
+ module.exports = {
134
+ MESSAGES,
135
+ DEFAULT_LANGUAGE,
136
+ SUPPORTED_LANGUAGES,
137
+ matchLanguage,
138
+ resolveLanguage,
139
+ t,
140
+ };
@@ -56,6 +56,7 @@ const {
56
56
  RAM_DANGER,
57
57
  TITLE_MAX,
58
58
  } = require("../widget-style");
59
+ const { t } = require("../i18n");
59
60
 
60
61
  const LINES = NAMED_COLORS;
61
62
 
@@ -202,7 +203,7 @@ function rampColor(at, palette) {
202
203
  function buildLinesSegment(id, session, palette, emojis, ascii = false, widget = {}, config = {}) {
203
204
  // Raw widgets keep only their value: no icon, no label.
204
205
  const raw = isRaw(widget, config);
205
- const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii });
206
+ const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii, lang: session.lang });
206
207
  // Meters: 25 cells, or 12 when the bar is fitted to a narrow terminal;
207
208
  // Classic draws the same cells as blocks.
208
209
  const classic = config.layout === "classic";
@@ -214,7 +215,8 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
214
215
  classic,
215
216
  elapsedPct: isPaced(widget, config) ? quota.elapsedPct : null,
216
217
  });
217
- const over200k = session.exceeds200k ? " >200k" : "";
218
+ const over200k = session.exceeds200k ? ` ${t("over_200k", session.lang)}` : "";
219
+ const remWord = t("rem", session.lang);
218
220
 
219
221
  function quotaState(quota) {
220
222
  const thresh = resolveQuotaThresholds(widget, config);
@@ -235,14 +237,16 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
235
237
  renderDotMeter(rem, dots, ascii, palette, true, pace(quota, thresh)) +
236
238
  " " +
237
239
  fg(color) +
238
- `${rem.toFixed(1)}% rem` +
240
+ `${rem.toFixed(1)}% ${remWord}` +
239
241
  RESET
240
242
  );
241
243
  }
242
244
 
243
245
  function quotaPct(quota) {
244
246
  const { rem, blinkSeq, color } = quotaState(quota);
245
- return blinkSeq + fg(palette.text) + tag() + fg(color) + `${rem.toFixed(1)}% rem` + RESET;
247
+ return (
248
+ blinkSeq + fg(palette.text) + tag() + fg(color) + `${rem.toFixed(1)}% ${remWord}` + RESET
249
+ );
246
250
  }
247
251
 
248
252
  function quotaReset(quota, weekly) {
@@ -307,7 +311,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
307
311
  tag() +
308
312
  `${quota.label} ` +
309
313
  fg(color) +
310
- `${rem.toFixed(1)}% rem` +
314
+ `${rem.toFixed(1)}% ${remWord}` +
311
315
  fg(palette.text) +
312
316
  reset +
313
317
  RESET
@@ -404,7 +408,8 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
404
408
  const dirname = limitWidth(
405
409
  shortenPath(homePath(session.cwd), widget?.depth, ascii),
406
410
  widget,
407
- ascii
411
+ ascii,
412
+ session.lang
408
413
  );
409
414
  if (!dirname) {
410
415
  return null;
@@ -518,7 +523,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
518
523
  return fg(palette.text) + tag() + String(session.artifacts) + RESET;
519
524
 
520
525
  case "tasks": {
521
- const text = formatTasks(session.subagents, session.tasks);
526
+ const text = formatTasks(session.subagents, session.tasks, session.lang);
522
527
  if (!text) {
523
528
  return null;
524
529
  }
@@ -543,7 +548,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
543
548
  return fg(palette.text) + tag() + limitWidth(session.title, widget, ascii, TITLE_MAX) + RESET;
544
549
 
545
550
  case "sessionTime": {
546
- const text = formatSessionTime(session.sessionStartMs);
551
+ const text = formatSessionTime(session.sessionStartMs, Date.now(), session.lang);
547
552
  return text ? fg(palette.text) + tag() + text + RESET : null;
548
553
  }
549
554
 
@@ -553,7 +558,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
553
558
  }
554
559
  const net = session.sandboxNet;
555
560
  const color = net ? palette.quotaMed || palette.orange || LINES.orange : palette.git;
556
- const text = net ? "sandbox (net)" : "sandbox (isolated)";
561
+ const text = t(net ? "sandbox_net" : "sandbox_isolated", session.lang);
557
562
  return fg(color) + tag(net ? "🌐" : undefined) + text + RESET;
558
563
  }
559
564
 
@@ -607,7 +612,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
607
612
  fg(palette.text) +
608
613
  tag() +
609
614
  (widget?.split
610
- ? formatTokenSplit(session.inputTokens, session.outputTokens, ascii)
615
+ ? formatTokenSplit(session.inputTokens, session.outputTokens, ascii, session.lang)
611
616
  : formatTokens(session.totalTokens)) +
612
617
  RESET
613
618
  );
@@ -617,7 +622,8 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
617
622
  const usage = formatUsage(
618
623
  session.usage?.[id === "tokensToday" ? "today" : "week"],
619
624
  widget,
620
- ascii
625
+ ascii,
626
+ session.lang
621
627
  );
622
628
  if (!usage) {
623
629
  return null;
@@ -62,6 +62,7 @@ const {
62
62
  RAM_DANGER,
63
63
  TITLE_MAX,
64
64
  } = require("../widget-style");
65
+ const { t } = require("../i18n");
65
66
 
66
67
  const WHITE = [255, 255, 255];
67
68
 
@@ -174,14 +175,15 @@ function pill(color, text, blink = false) {
174
175
  function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, config = {}) {
175
176
  // Raw widgets keep only their value: no icon, no label.
176
177
  const raw = isRaw(widget, config);
177
- const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii });
178
+ const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii, lang: session.lang });
178
179
  const lbl = (value, icon) => ` ${tag(icon)}${value} `;
179
180
  // The pill frames a colored meter; plain-text meters (ASCII) get brackets.
180
181
  const open = ascii ? "[" : "";
181
182
  const close = ascii ? "]" : "";
182
183
  const meterLabel = () => ` ${tag()}${open}`;
183
184
  const pace = (quota) => (isPaced(widget, config) ? quota.elapsedPct : null);
184
- const over200k = session.exceeds200k ? " >200k" : "";
185
+ const over200k = session.exceeds200k ? ` ${t("over_200k", session.lang)}` : "";
186
+ const remWord = t("rem", session.lang);
185
187
 
186
188
  if (hasNoData(id, session) || hiddenByThreshold(id, widget, session)) {
187
189
  return null;
@@ -222,7 +224,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
222
224
  warnColor: colors.caution,
223
225
  });
224
226
  const reset = quota.reset ? `${ascii ? ", " : " · "}${quota.reset}` : "";
225
- return pill(color, lbl(`${quota.label} ${rem.toFixed(1)}% rem${reset}`), blink);
227
+ return pill(color, lbl(`${quota.label} ${rem.toFixed(1)}% ${remWord}${reset}`), blink);
226
228
  }
227
229
 
228
230
  case "fiveHourBar": {
@@ -243,7 +245,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
243
245
  color,
244
246
  rem,
245
247
  meterLabel(),
246
- `${close} ${rem.toFixed(1)}% rem `,
248
+ `${close} ${rem.toFixed(1)}% ${remWord} `,
247
249
  true,
248
250
  blink,
249
251
  pace(session.fiveHour)
@@ -264,7 +266,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
264
266
  critColor: colors.warn,
265
267
  warnColor: colors.caution,
266
268
  });
267
- return pill(color, lbl(`${rem.toFixed(1)}% rem`), blink);
269
+ return pill(color, lbl(`${rem.toFixed(1)}% ${remWord}`), blink);
268
270
  }
269
271
 
270
272
  case "fiveHourReset": {
@@ -300,7 +302,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
300
302
  color,
301
303
  rem,
302
304
  meterLabel(),
303
- `${close} ${rem.toFixed(1)}% rem `,
305
+ `${close} ${rem.toFixed(1)}% ${remWord} `,
304
306
  true,
305
307
  blink,
306
308
  pace(session.weekly)
@@ -321,7 +323,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
321
323
  critColor: colors.warn,
322
324
  warnColor: colors.caution,
323
325
  });
324
- return pill(color, lbl(`${rem.toFixed(1)}% rem`), blink);
326
+ return pill(color, lbl(`${rem.toFixed(1)}% ${remWord}`), blink);
325
327
  }
326
328
 
327
329
  case "weeklyReset": {
@@ -358,7 +360,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
358
360
  color,
359
361
  rem,
360
362
  meterLabel(),
361
- `${close} ${rem.toFixed(1)}% rem `,
363
+ `${close} ${rem.toFixed(1)}% ${remWord} `,
362
364
  true,
363
365
  blink,
364
366
  pace(session.thirdPartyFiveHour)
@@ -383,7 +385,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
383
385
  color,
384
386
  rem,
385
387
  meterLabel(),
386
- `${close} ${rem.toFixed(1)}% rem `,
388
+ `${close} ${rem.toFixed(1)}% ${remWord} `,
387
389
  true,
388
390
  blink,
389
391
  pace(session.thirdPartyWeekly)
@@ -410,7 +412,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
410
412
  color,
411
413
  rem,
412
414
  meterLabel(),
413
- `${close} ${rem.toFixed(1)}% rem `,
415
+ `${close} ${rem.toFixed(1)}% ${remWord} `,
414
416
  true,
415
417
  blink,
416
418
  pace(session.argonWeekly)
@@ -497,7 +499,8 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
497
499
  const name = limitWidth(
498
500
  shortenPath(homePath(session.cwd), widget?.depth, ascii),
499
501
  widget,
500
- ascii
502
+ ascii,
503
+ session.lang
501
504
  );
502
505
  if (!name) {
503
506
  return null;
@@ -603,7 +606,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
603
606
  return pill(colors.meta, lbl(String(session.artifacts)));
604
607
 
605
608
  case "tasks": {
606
- const text = formatTasks(session.subagents, session.tasks);
609
+ const text = formatTasks(session.subagents, session.tasks, session.lang);
607
610
  if (!text) {
608
611
  return null;
609
612
  }
@@ -627,7 +630,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
627
630
  return pill(colors.meta, lbl(limitWidth(session.title, widget, ascii, TITLE_MAX)));
628
631
 
629
632
  case "sessionTime": {
630
- const text = formatSessionTime(session.sessionStartMs);
633
+ const text = formatSessionTime(session.sessionStartMs, Date.now(), session.lang);
631
634
  return text ? pill(colors.meta, lbl(text)) : null;
632
635
  }
633
636
 
@@ -653,7 +656,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
653
656
  return null;
654
657
  }
655
658
  const net = session.sandboxNet;
656
- const text = net ? "sandbox (net)" : "sandbox (isolated)";
659
+ const text = t(net ? "sandbox_net" : "sandbox_isolated", session.lang);
657
660
  const color = net ? colors.warn : colors.git;
658
661
  return pill(color, lbl(text, net ? "🌐" : undefined));
659
662
  }
@@ -683,7 +686,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
683
686
  colors.meta,
684
687
  lbl(
685
688
  widget?.split
686
- ? formatTokenSplit(session.inputTokens, session.outputTokens, ascii)
689
+ ? formatTokenSplit(session.inputTokens, session.outputTokens, ascii, session.lang)
687
690
  : formatTokens(session.totalTokens)
688
691
  )
689
692
  );
@@ -693,7 +696,8 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
693
696
  const usage = formatUsage(
694
697
  session.usage?.[id === "tokensToday" ? "today" : "week"],
695
698
  widget,
696
- ascii
699
+ ascii,
700
+ session.lang
697
701
  );
698
702
  if (!usage) {
699
703
  return null;
package/src/lib/paths.js CHANGED
@@ -28,6 +28,7 @@ const RUNTIME_FILES = [
28
28
  "log.js",
29
29
  "payload-dump.js",
30
30
  "usage.js",
31
+ "i18n.js",
31
32
  path.join("layouts", "lines.js"),
32
33
  path.join("layouts", "pills.js"),
33
34
  ];
@@ -8,20 +8,21 @@ const { resolveGit, resolveGitDetails } = require("./git");
8
8
  const { resolveUsage, conversationsDir } = require("./usage");
9
9
  const { isWsl, resolveInstallDir, writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
10
10
  const { logWarning } = require("./log");
11
+ const { resolveLanguage, t } = require("./i18n");
11
12
 
12
13
  const FIVE_HOURS = 5 * 3600;
13
14
  const SEVEN_DAYS = 7 * 86400;
14
15
  const ARGON_BUCKET = "gemini-argon-wtu-weekly";
15
- // The buckets Antigravity sends, with the short names the bar shows.
16
+ // The buckets Antigravity sends, with the message key of the short name the bar shows.
16
17
  const QUOTA_BUCKETS = {
17
- "gemini-5h": { label: "5h", window: FIVE_HOURS },
18
- "gemini-weekly": { label: "7d", window: SEVEN_DAYS },
18
+ "gemini-5h": { key: "tag_5h", window: FIVE_HOURS },
19
+ "gemini-weekly": { key: "tag_7d", window: SEVEN_DAYS },
19
20
  // Gemini 4 Argon on Google AI Pro: a weekly quota of its own, with no 5-hour
20
21
  // window, sent whatever model is in use. On Ultra Argon counts toward the
21
22
  // Gemini buckets and this one isn't sent.
22
- [ARGON_BUCKET]: { label: "Argon 7d", window: SEVEN_DAYS },
23
- "3p-5h": { label: "3P 5h", window: FIVE_HOURS },
24
- "3p-weekly": { label: "3P 7d", window: SEVEN_DAYS },
23
+ [ARGON_BUCKET]: { key: "tag_argon_7d", window: SEVEN_DAYS },
24
+ "3p-5h": { key: "tag_3p_5h", window: FIVE_HOURS },
25
+ "3p-weekly": { key: "tag_3p_7d", window: SEVEN_DAYS },
25
26
  };
26
27
  // No quota window lasts a year: a reset further out is a broken value.
27
28
  const MAX_RESET_SECONDS = 366 * 86400;
@@ -189,7 +190,7 @@ function percent(value) {
189
190
  return Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
190
191
  }
191
192
 
192
- function readQuota(data, key, windowSeconds) {
193
+ function readQuota(data, key, windowSeconds, lang) {
193
194
  const quota = data?.quota?.[key];
194
195
 
195
196
  if (!quota || typeof quota !== "object") {
@@ -221,7 +222,7 @@ function readQuota(data, key, windowSeconds) {
221
222
 
222
223
  return {
223
224
  remaining,
224
- reset: hasReset ? formatReset(resetSeconds) : "",
225
+ reset: hasReset ? formatReset(resetSeconds, lang) : "",
225
226
  resetSeconds,
226
227
  elapsedPct,
227
228
  resetTime: resetValid && quota.reset_time ? String(quota.reset_time) : "",
@@ -245,15 +246,15 @@ function bucketGroup(key) {
245
246
  // (third-party) buckets for the others. Argon falls back to the Gemini
246
247
  // buckets, where its usage counts when there is no Argon bucket. Without a
247
248
  // matching bucket every bucket counts. Disabled buckets never do.
248
- function tightestQuota(data) {
249
+ function tightestQuota(data, lang) {
249
250
  const buckets = data?.quota && typeof data.quota === "object" ? data.quota : {};
250
251
  const usable = [];
251
252
 
252
253
  for (const key of Object.keys(buckets)) {
253
254
  const known = QUOTA_BUCKETS[key];
254
- const quota = readQuota(data, key, known ? known.window : 0);
255
+ const quota = readQuota(data, key, known ? known.window : 0, lang);
255
256
  if (quota && !quota.disabled) {
256
- usable.push({ ...quota, key, label: known ? known.label : key.slice(0, 20) });
257
+ usable.push({ ...quota, key, label: known ? t(known.key, lang) : key.slice(0, 20) });
257
258
  }
258
259
  }
259
260
 
@@ -303,6 +304,8 @@ function contextUsedPercent(data) {
303
304
  // options.readUsage: the live statusline may read conversation metadata and
304
305
  // databases; previews use the usage cache and a synthetic sample start time.
305
306
  function extractSession(data, config, options = {}) {
307
+ // The bar's own words follow config.language; payload text never changes.
308
+ const lang = resolveLanguage(config?.language);
306
309
  const git = resolveGit(data, config);
307
310
  const gitDetails = resolveGitDetails(data, config);
308
311
  // Once git status has been read for gitFiles, the branch's dirty marker can
@@ -361,6 +364,7 @@ function extractSession(data, config, options = {}) {
361
364
  }
362
365
 
363
366
  return {
367
+ lang,
364
368
  os: osName,
365
369
  ramPct,
366
370
  username: username || "",
@@ -380,12 +384,12 @@ function extractSession(data, config, options = {}) {
380
384
  exceeds200k: data.exceeds_200k_tokens === true,
381
385
  cacheRead: count(data.context_window?.current_usage?.cache_read_input_tokens),
382
386
  cacheCreate: count(data.context_window?.current_usage?.cache_creation_input_tokens),
383
- fiveHour: readQuota(data, "gemini-5h", FIVE_HOURS),
384
- weekly: readQuota(data, "gemini-weekly", SEVEN_DAYS),
385
- thirdPartyFiveHour: readQuota(data, "3p-5h", FIVE_HOURS),
386
- thirdPartyWeekly: readQuota(data, "3p-weekly", SEVEN_DAYS),
387
- argonWeekly: readQuota(data, ARGON_BUCKET, SEVEN_DAYS),
388
- tightestQuota: tightestQuota(data),
387
+ fiveHour: readQuota(data, "gemini-5h", FIVE_HOURS, lang),
388
+ weekly: readQuota(data, "gemini-weekly", SEVEN_DAYS, lang),
389
+ thirdPartyFiveHour: readQuota(data, "3p-5h", FIVE_HOURS, lang),
390
+ thirdPartyWeekly: readQuota(data, "3p-weekly", SEVEN_DAYS, lang),
391
+ argonWeekly: readQuota(data, ARGON_BUCKET, SEVEN_DAYS, lang),
392
+ tightestQuota: tightestQuota(data, lang),
389
393
  cwd: firstText(data.cwd, data.workspace?.current_dir),
390
394
  project: firstText(data.workspace?.project_dir),
391
395
  plan: firstText(data.plan_tier),
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { parseColor } = require("./colors");
4
4
  const { clipText, stripControl, visibleWidth } = require("./formatters");
5
+ const { DEFAULT_LANGUAGE, t } = require("./i18n");
5
6
 
6
7
  // Basic names the exec widget's color option always accepted; the Lines
7
8
  // layout also uses them as its fallback palette.
@@ -82,51 +83,52 @@ function isPaced(widget, config) {
82
83
  }
83
84
 
84
85
  // Each widget's icon and label, shared by both layouts so Pills and Lines
85
- // read the same. iconOnly: with emojis on, the icon stands in for the label
86
+ // read the same. name is the English label (configure shows it), key its
87
+ // message in i18n.js. iconOnly: with emojis on, the icon stands in for the label
86
88
  // (a meter or the model name already says what it is).
87
89
  const WIDGET_TAGS = {
88
- model: { icon: "🤖", name: "Model", iconOnly: true },
89
- quota: { icon: "⏳", name: "Quota" },
90
- fiveHourBar: { icon: "⏳", name: "5h", iconOnly: true },
91
- fiveHourPct: { icon: "⏳", name: "5h" },
92
- fiveHourReset: { icon: "🔄", name: "Reset" },
93
- weeklyBar: { icon: "📅", name: "7d", iconOnly: true },
94
- weeklyPct: { icon: "📅", name: "7d" },
95
- weeklyReset: { icon: "🔄", name: "Reset" },
96
- thirdPartyFiveHour: { icon: "🧪", name: "3P 5h" },
97
- thirdPartyWeekly: { icon: "🧪", name: "3P 7d" },
98
- argonWeekly: { icon: "🔮", name: "Argon 7d" },
99
- argonReset: { icon: "🔄", name: "Argon reset" },
100
- context: { icon: "💾", name: "Context", iconOnly: true },
101
- contextPct: { icon: "💾", name: "Context" },
102
- contextTokens: { icon: "💾", name: "Context" },
103
- cwd: { icon: "📂", name: "Dir" },
104
- project: { icon: "📦", name: "Project" },
90
+ model: { icon: "🤖", name: "Model", key: "tag_model", iconOnly: true },
91
+ quota: { icon: "⏳", name: "Quota", key: "tag_quota" },
92
+ fiveHourBar: { icon: "⏳", name: "5h", key: "tag_5h", iconOnly: true },
93
+ fiveHourPct: { icon: "⏳", name: "5h", key: "tag_5h" },
94
+ fiveHourReset: { icon: "🔄", name: "Reset", key: "tag_reset" },
95
+ weeklyBar: { icon: "📅", name: "7d", key: "tag_7d", iconOnly: true },
96
+ weeklyPct: { icon: "📅", name: "7d", key: "tag_7d" },
97
+ weeklyReset: { icon: "🔄", name: "Reset", key: "tag_reset" },
98
+ thirdPartyFiveHour: { icon: "🧪", name: "3P 5h", key: "tag_3p_5h" },
99
+ thirdPartyWeekly: { icon: "🧪", name: "3P 7d", key: "tag_3p_7d" },
100
+ argonWeekly: { icon: "🔮", name: "Argon 7d", key: "tag_argon_7d" },
101
+ argonReset: { icon: "🔄", name: "Argon reset", key: "tag_argon_reset" },
102
+ context: { icon: "💾", name: "Context", key: "tag_context", iconOnly: true },
103
+ contextPct: { icon: "💾", name: "Context", key: "tag_context" },
104
+ contextTokens: { icon: "💾", name: "Context", key: "tag_context" },
105
+ cwd: { icon: "📂", name: "Dir", key: "tag_dir" },
106
+ project: { icon: "📦", name: "Project", key: "tag_project" },
105
107
  os: { icon: "💻", name: "" },
106
- host: { icon: "👤", name: "Host", iconOnly: true },
108
+ host: { icon: "👤", name: "Host", key: "tag_host", iconOnly: true },
107
109
  git: { icon: "🌿", name: "" },
108
- gitChanges: { icon: "📝", name: "Lines" },
109
- gitFiles: { icon: "📄", name: "Files" },
110
- gitSha: { icon: "📌", name: "Commit" },
111
- gitStash: { icon: "💼", name: "Stash" },
112
- gitWorktree: { icon: "🌳", name: "Worktree" },
113
- plan: { icon: "⭐", name: "Plan" },
114
- agentState: { icon: "🟣", name: "Agent" },
115
- artifacts: { icon: "📎", name: "Artifacts" },
110
+ gitChanges: { icon: "📝", name: "Lines", key: "tag_lines" },
111
+ gitFiles: { icon: "📄", name: "Files", key: "tag_files" },
112
+ gitSha: { icon: "📌", name: "Commit", key: "tag_commit" },
113
+ gitStash: { icon: "💼", name: "Stash", key: "tag_stash" },
114
+ gitWorktree: { icon: "🌳", name: "Worktree", key: "tag_worktree" },
115
+ plan: { icon: "⭐", name: "Plan", key: "tag_plan" },
116
+ agentState: { icon: "🟣", name: "Agent", key: "tag_agent" },
117
+ artifacts: { icon: "📎", name: "Artifacts", key: "tag_artifacts" },
116
118
  tasks: { icon: "👥", name: "" },
117
119
  mode: { icon: "🧭", name: "" },
118
120
  title: { icon: "💬", name: "" },
119
- sessionTime: { icon: "⏱️", name: "Session" },
120
- ram: { icon: "💻", name: "RAM" },
121
+ sessionTime: { icon: "⏱️", name: "Session", key: "tag_session" },
122
+ ram: { icon: "💻", name: "RAM", key: "tag_ram" },
121
123
  version: { icon: "🔖", name: "" },
122
124
  sandbox: { icon: "🔒", name: "" },
123
- cacheRead: { icon: "📥", name: "Cache read" },
124
- cacheCreate: { icon: "📤", name: "Cache create" },
125
- cacheHit: { icon: "⚡", name: "Cache hit" },
126
- totalTokens: { icon: "🪙", name: "Tokens" },
127
- clock: { icon: "🕒", name: "Clock", iconOnly: true },
128
- tokensToday: { icon: "📈", name: "Today" },
129
- tokensWeek: { icon: "📊", name: "7d" },
125
+ cacheRead: { icon: "📥", name: "Cache read", key: "tag_cache_read" },
126
+ cacheCreate: { icon: "📤", name: "Cache create", key: "tag_cache_create" },
127
+ cacheHit: { icon: "⚡", name: "Cache hit", key: "tag_cache_hit" },
128
+ totalTokens: { icon: "🪙", name: "Tokens", key: "tag_tokens" },
129
+ clock: { icon: "🕒", name: "Clock", key: "tag_clock", iconOnly: true },
130
+ tokensToday: { icon: "📈", name: "Today", key: "tag_today" },
131
+ tokensWeek: { icon: "📊", name: "7d", key: "tag_7d" },
130
132
  customText: { icon: "🏷️", name: "" },
131
133
  };
132
134
 
@@ -155,13 +157,16 @@ function customIcon(widget) {
155
157
  // The text before a widget's value: "🔄 Reset: ", "Reset: ", or nothing for
156
158
  // raw widgets. icon overrides the table's for state icons (idle agent,
157
159
  // sandbox with network); the user's own label and icon win over both.
158
- function widgetTag(id, { emojis = true, raw = false, icon, widget, ascii = false } = {}) {
160
+ function widgetTag(
161
+ id,
162
+ { emojis = true, raw = false, icon, widget, ascii = false, lang = DEFAULT_LANGUAGE } = {}
163
+ ) {
159
164
  const tag = WIDGET_TAGS[id];
160
165
  if (raw || !tag) {
161
166
  return "";
162
167
  }
163
168
  const label = customLabel(widget, ascii);
164
- const name = label ?? tag.name;
169
+ const name = label ?? (tag.key ? t(tag.key, lang) : tag.name);
165
170
  const named = name ? `${name}: ` : "";
166
171
  if (!emojis) {
167
172
  return named;
@@ -31,6 +31,7 @@ const RENDER_OPTIONS = {
31
31
  theme: { type: "string" },
32
32
  caps: { type: "string" },
33
33
  gradient: { type: "string" },
34
+ language: { type: "string" },
34
35
  };
35
36
 
36
37
  // The config decides whether problems are logged, so it is read first. A