From ea340ef141b1d75509c44690bd48607fbb89db90 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 22 Jun 2026 00:24:34 +0200 Subject: [PATCH] fix(i18n): localize remaining Claude-pane strings (T-462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes i18n coverage of the conversation surface — the strings the T-464 migration left as "decorative" English: - running-indicator verbs (Pondering/Conjuring/… → catalog, Dutch added) - pane chrome title (primary / secondary N) and banner role - tool-card titles (Edit → Bewerken) via a tool.name.* key set, with command/proper-name tools (Bash, Grep, Glob, LS) falling back to the raw name - collapser step/edit/agent counters - the folded-activity ticker (result/error/thinking/image + tool name) English degrades to the placeholder where a key is absent, so untranslated or MCP tool names render unchanged. en_us and nl_nl stay at key parity. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 7 ++ assets/i18n/en_us/builtin.claude.json | 40 ++++++++++ assets/i18n/nl_nl/builtin.claude.json | 40 ++++++++++ lib/builtin/claude/src/claude_pane.dart | 20 ++++- lib/builtin/claude/src/conversation_view.dart | 77 +++++++++++++++---- lib/builtin/claude/src/running_indicator.dart | 9 ++- 6 files changed, 172 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c496ac22..ce26c393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,13 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. locale under `assets/i18n//`. A new language is a drop-in folder; en_US behaviour is unchanged. (T-462) +### Fixed + +- **Localized the remaining Claude-pane strings.** Running-indicator verbs, the + primary/secondary session titles and banner role, tool-card titles, the + step/edit/agent counters, and the folded-activity ticker now resolve through + the catalog — completing i18n coverage of the conversation surface. (T-462) + ## [2.7.1] — 2026-06-18 ### Changed diff --git a/assets/i18n/en_us/builtin.claude.json b/assets/i18n/en_us/builtin.claude.json index 1dec9037..a71e4458 100644 --- a/assets/i18n/en_us/builtin.claude.json +++ b/assets/i18n/en_us/builtin.claude.json @@ -6,13 +6,37 @@ "status.primary-exited": { "translation": "session exited — restart clide to retry" }, "banner.title": { "translation": "Claude" }, "banner.warmingUp": { "translation": "Warming up — your conversation will appear here." }, + "banner.role.primary": { "translation": "primary" }, + "banner.role.secondary": { "translation": "session {index}" }, "composer.hint": { "translation": "Message Claude… (Enter to send · Shift+Enter for newline)" }, "composer.stop": { "translation": "Stop ⎋" }, "composer.stop.hint": { "translation": "Interrupt the running turn (Escape)" }, "composer.removeAttachment": { "translation": "Remove {name}" }, + "pane.title.primary": { "translation": "claude — primary" }, + "pane.title.secondary": { "translation": "claude — secondary {index}" }, "pane.starting": { "translation": "starting…" }, "pane.modeBadge.semantics": { "translation": "permission mode: {mode}" }, "running.semantics": { "translation": "Claude is running" }, + "running.verb.pondering": { "translation": "Pondering" }, + "running.verb.conjuring": { "translation": "Conjuring" }, + "running.verb.brewing": { "translation": "Brewing" }, + "running.verb.tinkering": { "translation": "Tinkering" }, + "running.verb.noodling": { "translation": "Noodling" }, + "running.verb.percolating": { "translation": "Percolating" }, + "running.verb.computing": { "translation": "Computing" }, + "running.verb.wrangling": { "translation": "Wrangling" }, + "running.verb.untangling": { "translation": "Untangling" }, + "running.verb.synthesizing": { "translation": "Synthesizing" }, + "running.verb.cogitating": { "translation": "Cogitating" }, + "running.verb.whirring": { "translation": "Whirring" }, + "running.verb.mincing": { "translation": "Mincing" }, + "running.verb.boiling": { "translation": "Boiling" }, + "running.verb.humming": { "translation": "Humming" }, + "running.verb.buzzing": { "translation": "Buzzing" }, + "running.verb.magicking": { "translation": "Magicking" }, + "running.verb.cliding": { "translation": "Cliding" }, + "running.verb.zooming": { "translation": "Zooming" }, + "running.verb.bouncing": { "translation": "Bouncing" }, "conversation.empty": { "translation": "Waiting for Claude…" }, "conversation.label.you": { "translation": "you" }, "conversation.label.claude": { "translation": "claude" }, @@ -39,6 +63,12 @@ "conversation.bashTail.label": { "translation": "live tail" }, "conversation.cluster.activity": { "translation": "Activity" }, "conversation.cluster.edits": { "translation": "Edits" }, + "conversation.counter.step": { "translation": "1 step" }, + "conversation.counter.steps": { "translation": "{count} steps" }, + "conversation.counter.edit": { "translation": "1 edit" }, + "conversation.counter.edits": { "translation": "{count} edits" }, + "conversation.counter.starting": { "translation": "starting" }, + "conversation.counter.agents": { "translation": "{done}/{total} agents" }, "prompt.permission.allow": { "translation": "1. Allow" }, "prompt.permission.allowRemember": { "translation": "2. Allow & don't ask again" }, "prompt.permission.deny": { "translation": "{n}. Deny" }, @@ -62,6 +92,16 @@ "tool.edit.before": { "translation": "— before" }, "tool.edit.after": { "translation": "+ after" }, "tool.bash.background": { "translation": "background" }, + "tool.name.Read": { "translation": "Read" }, + "tool.name.Edit": { "translation": "Edit" }, + "tool.name.MultiEdit": { "translation": "MultiEdit" }, + "tool.name.Write": { "translation": "Write" }, + "tool.name.NotebookEdit": { "translation": "NotebookEdit" }, + "tool.name.WebFetch": { "translation": "WebFetch" }, + "tool.name.WebSearch": { "translation": "WebSearch" }, + "tool.name.Task": { "translation": "Task" }, + "tool.name.TodoWrite": { "translation": "TodoWrite" }, + "tool.name.ExitPlanMode": { "translation": "ExitPlanMode" }, "permissionControl.semantics": { "translation": "permission mode: {mode}. Activate to change." }, "permissionControl.tooltip": { "translation": "Permission mode: {mode} — change (Ctrl/Cmd+M cycles)" }, "permissionBadge.tooltip": { "translation": "Permission mode: {mode}. Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions." }, diff --git a/assets/i18n/nl_nl/builtin.claude.json b/assets/i18n/nl_nl/builtin.claude.json index 3bdc845d..861b7c73 100644 --- a/assets/i18n/nl_nl/builtin.claude.json +++ b/assets/i18n/nl_nl/builtin.claude.json @@ -6,13 +6,37 @@ "status.primary-exited": { "translation": "sessie beëindigd — herstart clide om opnieuw te proberen" }, "banner.title": { "translation": "Claude" }, "banner.warmingUp": { "translation": "Opstarten — je gesprek verschijnt hier." }, + "banner.role.primary": { "translation": "primair" }, + "banner.role.secondary": { "translation": "sessie {index}" }, "composer.hint": { "translation": "Bericht aan Claude… (Enter om te verzenden · Shift+Enter voor een nieuwe regel)" }, "composer.stop": { "translation": "Stop ⎋" }, "composer.stop.hint": { "translation": "Onderbreek de lopende beurt (Escape)" }, "composer.removeAttachment": { "translation": "{name} verwijderen" }, + "pane.title.primary": { "translation": "claude — primair" }, + "pane.title.secondary": { "translation": "claude — secundair {index}" }, "pane.starting": { "translation": "starten…" }, "pane.modeBadge.semantics": { "translation": "permissiemodus: {mode}" }, "running.semantics": { "translation": "Claude is bezig" }, + "running.verb.pondering": { "translation": "Peinzen" }, + "running.verb.conjuring": { "translation": "Toveren" }, + "running.verb.brewing": { "translation": "Brouwen" }, + "running.verb.tinkering": { "translation": "Knutselen" }, + "running.verb.noodling": { "translation": "Prutsen" }, + "running.verb.percolating": { "translation": "Pruttelen" }, + "running.verb.computing": { "translation": "Rekenen" }, + "running.verb.wrangling": { "translation": "Worstelen" }, + "running.verb.untangling": { "translation": "Ontwarren" }, + "running.verb.synthesizing": { "translation": "Synthetiseren" }, + "running.verb.cogitating": { "translation": "Overpeinzen" }, + "running.verb.whirring": { "translation": "Snorren" }, + "running.verb.mincing": { "translation": "Hakken" }, + "running.verb.boiling": { "translation": "Koken" }, + "running.verb.humming": { "translation": "Neuriën" }, + "running.verb.buzzing": { "translation": "Zoemen" }, + "running.verb.magicking": { "translation": "Goochelen" }, + "running.verb.cliding": { "translation": "Cliden" }, + "running.verb.zooming": { "translation": "Zoeven" }, + "running.verb.bouncing": { "translation": "Stuiteren" }, "conversation.empty": { "translation": "Wachten op Claude…" }, "conversation.label.you": { "translation": "jij" }, "conversation.label.claude": { "translation": "claude" }, @@ -39,6 +63,12 @@ "conversation.bashTail.label": { "translation": "live tail" }, "conversation.cluster.activity": { "translation": "Activiteit" }, "conversation.cluster.edits": { "translation": "Wijzigingen" }, + "conversation.counter.step": { "translation": "1 stap" }, + "conversation.counter.steps": { "translation": "{count} stappen" }, + "conversation.counter.edit": { "translation": "1 wijziging" }, + "conversation.counter.edits": { "translation": "{count} wijzigingen" }, + "conversation.counter.starting": { "translation": "starten" }, + "conversation.counter.agents": { "translation": "{done}/{total} agents" }, "prompt.permission.allow": { "translation": "1. Toestaan" }, "prompt.permission.allowRemember": { "translation": "2. Toestaan en niet meer vragen" }, "prompt.permission.deny": { "translation": "{n}. Weigeren" }, @@ -62,6 +92,16 @@ "tool.edit.before": { "translation": "— voor" }, "tool.edit.after": { "translation": "+ na" }, "tool.bash.background": { "translation": "achtergrond" }, + "tool.name.Read": { "translation": "Lezen" }, + "tool.name.Edit": { "translation": "Bewerken" }, + "tool.name.MultiEdit": { "translation": "Meervoudig bewerken" }, + "tool.name.Write": { "translation": "Schrijven" }, + "tool.name.NotebookEdit": { "translation": "Notebook bewerken" }, + "tool.name.WebFetch": { "translation": "Web ophalen" }, + "tool.name.WebSearch": { "translation": "Web zoeken" }, + "tool.name.Task": { "translation": "Taak" }, + "tool.name.TodoWrite": { "translation": "Takenlijst" }, + "tool.name.ExitPlanMode": { "translation": "Planmodus verlaten" }, "permissionControl.semantics": { "translation": "permissiemodus: {mode}. Activeer om te wijzigen." }, "permissionControl.tooltip": { "translation": "Permissiemodus: {mode} — wijzigen (Ctrl/Cmd+M wisselt)" }, "permissionBadge.tooltip": { "translation": "Permissiemodus: {mode}. Klik om te wisselen tussen default/acceptEdits/plan; Shift-klik voor bypassPermissions." }, diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index 4ce178c5..14f1e650 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -728,7 +728,15 @@ class _ClaudePaneState extends State { @override Widget build(BuildContext context) { - final title = widget.isPrimary ? 'claude — primary' : 'claude — secondary ${widget.secondaryIndex}'; + final title = widget.isPrimary + ? ClideSettings.i18n.string(context, 'pane.title.primary', namespace: 'builtin.claude', placeholder: 'claude — primary') + : ClideSettings.i18n.interpolated( + context, + 'pane.title.secondary', + namespace: 'builtin.claude', + placeholder: 'claude — secondary ${widget.secondaryIndex}', + replacers: [I18nReplacer(from: '{index}', replace: '${widget.secondaryIndex}')], + ); final tokens = ClideSettings.theme.of(context).surface; final Widget body; @@ -761,7 +769,15 @@ class _ClaudePaneState extends State { quietErrorToolUseIds: _session?.quietErrorToolUseIds ?? const {}, workflows: _session?.workflows ?? const {}, emptyState: ClaudeBanner( - role: widget.isPrimary ? 'primary' : 'session ${widget.secondaryIndex}', + role: widget.isPrimary + ? ClideSettings.i18n.string(context, 'banner.role.primary', namespace: 'builtin.claude', placeholder: 'primary') + : ClideSettings.i18n.interpolated( + context, + 'banner.role.secondary', + namespace: 'builtin.claude', + placeholder: 'session ${widget.secondaryIndex}', + replacers: [I18nReplacer(from: '{index}', replace: '${widget.secondaryIndex}')], + ), workspace: _repoRoot, statusLine: _statusLine, ), diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index d9165dcd..e49e6b5c 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -800,10 +800,10 @@ class _ConversationTurn extends StatelessWidget { final outcome = toolUseOutcomes[t.toolUseId]; final color = outcome == null ? tokens.globalFocus : (outcome ? tokens.statusSuccess : tokens.statusError); final collapser = ClideCollapserCard( - label: t.name, + label: _toolNameLabel(context, t.name), color: color, collapsedSummary: _toolUseSummary(t), - counter: '1 step', + counter: _stepsCounter(context, 1), status: _toolRunStatus(t), children: [_toolContentCard(context, t)], ); @@ -818,8 +818,8 @@ class _ConversationTurn extends StatelessWidget { padding: const EdgeInsets.only(left: 12), child: ClideCollapserCard( label: ClideSettings.i18n.string(context, 'conversation.label.agentRun', namespace: 'builtin.claude', placeholder: 'agent run'), - collapsedSummary: _summarizeActivity(runItems.last), - counter: runItems.length == 1 ? '1 step' : '${runItems.length} steps', + collapsedSummary: _summarizeActivity(context, runItems.last), + counter: _stepsCounter(context, runItems.length), children: [ for (final r in runItems) _ConversationTurn( @@ -849,7 +849,18 @@ class _ConversationTurn extends StatelessWidget { Widget _workflowCard(BuildContext context, AssistantToolUse t, WorkflowRun run) { final title = run.name ?? ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'); final color = run.done ? tokens.statusSuccess : tokens.globalFocus; - final counter = run.agentCount == 0 ? 'starting' : '${run.doneCount}/${run.agentCount} agents'; + final counter = run.agentCount == 0 + ? ClideSettings.i18n.string(context, 'conversation.counter.starting', namespace: 'builtin.claude', placeholder: 'starting') + : ClideSettings.i18n.interpolated( + context, + 'conversation.counter.agents', + namespace: 'builtin.claude', + placeholder: '${run.doneCount}/${run.agentCount} agents', + replacers: [ + I18nReplacer(from: '{done}', replace: '${run.doneCount}'), + I18nReplacer(from: '{total}', replace: '${run.agentCount}'), + ], + ); final detail = run.done ? (run.summary ?? run.description) : run.description; final collapsedSummary = (detail == null || detail == title) ? title : '$title · $detail'; return ClideCollapserCard( @@ -990,7 +1001,7 @@ class _ConversationTurn extends StatelessWidget { variant: ConversationCardVariant.bordered, accent: accent, borderColor: outcome == null ? null : accent, - label: t.name, + label: _toolNameLabel(context, t.name), copyText: const JsonEncoder.withIndent(' ').convert(t.input), status: status, body: toolInputBody(context, tokens, t.name, t.input, mono), @@ -1047,7 +1058,7 @@ class _ConversationTurn extends StatelessWidget { variant: ConversationCardVariant.bordered, accent: quiet ? tokens.globalTextMuted : accent, borderColor: quiet ? tokens.panelBorder : tokens.statusError, - label: paired != null ? '${paired.name} · $errLabel' : errLabel, + label: paired != null ? '${_toolNameLabel(context, paired.name)} · $errLabel' : errLabel, copyText: t.content, collapsible: quiet || multiline, collapsedByDefault: quiet, // genuine errors stay expanded; a denial folds @@ -1061,7 +1072,7 @@ class _ConversationTurn extends StatelessWidget { // For Write/Edit, the result is usually "OK" — keep it as plain text. final multiline = t.content.contains('\n'); final isOutputTool = paired != null && const {'Bash', 'Read', 'Grep', 'LS'}.contains(paired.name); - final resultLabel = paired != null ? '${paired.name} · $label' : label; + final resultLabel = paired != null ? '${_toolNameLabel(context, paired.name)} · $label' : label; return ConversationCard( variant: ConversationCardVariant.bordered, accent: accent, @@ -1127,8 +1138,8 @@ class _ActivityCard extends StatelessWidget { final count = items.length; return ClideCollapserCard( label: ClideSettings.i18n.string(context, 'conversation.cluster.activity', namespace: 'builtin.claude', placeholder: 'Activity'), - collapsedSummary: _summarizeActivity(items.last), - counter: count == 1 ? '1 step' : '$count steps', + collapsedSummary: _summarizeActivity(context, items.last), + counter: _stepsCounter(context, count), status: _runStatus(items, resultByToolUseId), children: [ for (final item in items) @@ -1150,6 +1161,35 @@ class _ActivityCard extends StatelessWidget { } } +/// Localized display label for a tool name (T-462). File/web/task operations +/// have natural translations; command/proper-name tools (Bash, Grep, Glob, LS) +/// have no catalog key and fall back to the raw name via the placeholder. +String _toolNameLabel(BuildContext context, String name) => + ClideSettings.i18n.string(context, 'tool.name.$name', namespace: 'builtin.claude', placeholder: name); + +/// Localized "N steps" counter for a collapser header (T-462). Singular and +/// plural are distinct catalog keys; the English forms double as the fallback. +String _stepsCounter(BuildContext context, int n) => n == 1 + ? ClideSettings.i18n.string(context, 'conversation.counter.step', namespace: 'builtin.claude', placeholder: '1 step') + : ClideSettings.i18n.interpolated( + context, + 'conversation.counter.steps', + namespace: 'builtin.claude', + placeholder: '$n steps', + replacers: [I18nReplacer(from: '{count}', replace: '$n')], + ); + +/// Localized "N edits" counter for the edit-run collapser header (T-462). +String _editsCounter(BuildContext context, int n) => n == 1 + ? ClideSettings.i18n.string(context, 'conversation.counter.edit', namespace: 'builtin.claude', placeholder: '1 edit') + : ClideSettings.i18n.interpolated( + context, + 'conversation.counter.edits', + namespace: 'builtin.claude', + placeholder: '$n edits', + replacers: [I18nReplacer(from: '{count}', replace: '$n')], + ); + /// Aggregate live status for a run's header tick (T-296): error if any tool in /// the run failed, else running while its last tool awaits a result, else /// success. Null (no tools) shows no indicator. @@ -1193,8 +1233,8 @@ class _EditRunCard extends StatelessWidget { final count = edits.length; return ClideCollapserCard( label: ClideSettings.i18n.string(context, 'conversation.cluster.edits', namespace: 'builtin.claude', placeholder: 'Edits'), - collapsedSummary: _summarizeActivity(edits.last), - counter: count == 1 ? '1 edit' : '$count edits', + collapsedSummary: _summarizeActivity(context, edits.last), + counter: _editsCounter(context, count), status: _runStatus(edits, resultByToolUseId), children: [ for (final item in edits) @@ -1216,22 +1256,25 @@ class _EditRunCard extends StatelessWidget { } /// One-line summary of a folded item for the collapsed ticker. -String _summarizeActivity(ConversationItem item) { +String _summarizeActivity(BuildContext context, ConversationItem item) { + String label(String key, String fallback) => ClideSettings.i18n.string(context, key, namespace: 'builtin.claude', placeholder: fallback); switch (item) { case AssistantToolUse(:final name, :final input): final raw = input['command'] ?? input['file_path'] ?? input['path'] ?? input['pattern'] ?? input['url']; final detail = raw is String ? raw.split('\n').first.trim() : ''; final clipped = detail.length > 72 ? '${detail.substring(0, 72)}…' : detail; - return clipped.isEmpty ? name : '$name $clipped'; + final toolName = _toolNameLabel(context, name); + return clipped.isEmpty ? toolName : '$toolName $clipped'; case ToolResultMessage(:final isError): - return isError ? '↳ result · error' : '↳ result'; + final result = label('conversation.label.result', 'result'); + return isError ? '↳ $result · ${label('conversation.label.error', 'error')}' : '↳ $result'; case AssistantThinkingMessage(): - return 'thinking…'; + return '${label('conversation.label.thinking', 'thinking')}…'; case UserMessage(:final text): return text; case AssistantTextMessage(:final text): return text; case ImageMessage(:final path): - return 'image $path'; + return '${label('conversation.label.image', 'image')} $path'; } } diff --git a/lib/builtin/claude/src/running_indicator.dart b/lib/builtin/claude/src/running_indicator.dart index c905e882..f484a6d6 100644 --- a/lib/builtin/claude/src/running_indicator.dart +++ b/lib/builtin/claude/src/running_indicator.dart @@ -90,17 +90,22 @@ class _RunningIndicatorState extends State with SingleTickerPr label: ClideSettings.i18n.string(context, 'running.semantics', namespace: 'builtin.claude', placeholder: 'Claude is running'), child: ExcludeSemantics( child: reduced - ? ClideText('${_verbs.first}…', color: claudeAccent, fontSize: clideFontMeta) + ? ClideText('${_verb(context, _verbs.first)}…', color: claudeAccent, fontSize: clideFontMeta) : AnimatedBuilder( animation: _c, builder: (ctx, _) { final elapsed = _c.value * _periodSeconds; final dots = '.' * (elapsed.floor() % 4); final word = _verbs[(elapsed ~/ _secondsPerWord) % _verbs.length]; - return ClideText('$word$dots', color: claudeAccent, fontSize: clideFontMeta); + return ClideText('${_verb(ctx, word)}$dots', color: claudeAccent, fontSize: clideFontMeta); }, ), ), ); } + + /// Localize a verb through the catalog; the English word doubles as the key + /// suffix and the fallback, so a missing translation degrades to English. + String _verb(BuildContext context, String word) => + ClideSettings.i18n.string(context, 'running.verb.${word.toLowerCase()}', namespace: 'builtin.claude', placeholder: word); }