From 16fe71cb3f200da32d656f24798dd7c16ea7bd6f Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 18 Jun 2026 21:19:19 +0200 Subject: [PATCH] refactor(i18n): route claude pane labels through the catalog (T-464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the hardcoded user-facing strings across builtin.claude — composer, conversation cards/segments, permission + AskUserQuestion prompts, task dock, meta-sidebar (activity/config/team/roster), session/model pickers — to ClideSettings.i18n.string/.interpolated, English kept as the placeholder; builtin.claude_en_us.json extended to cover them. Context threaded into the context-free render helpers (toolInputBody chain, _ConversationTurn) the same way the mono family already is. No en_US behaviour change (D-21). Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 7 + lib/builtin/claude/src/claude_banner.dart | 18 +- lib/builtin/claude/src/claude_composer.dart | 41 ++++- lib/builtin/claude/src/claude_pane.dart | 12 +- lib/builtin/claude/src/claude_task_dock.dart | 64 ++++++- lib/builtin/claude/src/conversation_card.dart | 12 +- lib/builtin/claude/src/conversation_view.dart | 116 ++++++++---- lib/builtin/claude/src/image_thumbnail.dart | 8 +- .../claude/src/meta_sidebar/activity_tab.dart | 149 +++++++++++---- .../claude/src/meta_sidebar/config_tab.dart | 95 +++++++--- .../src/meta_sidebar/permission_badge.dart | 20 ++- .../claude/src/meta_sidebar/roster_row.dart | 47 +++-- .../claude/src/meta_sidebar/tab_strip.dart | 25 ++- .../claude/src/meta_sidebar/task_row.dart | 2 +- .../claude/src/meta_sidebar/team_tab.dart | 12 +- lib/builtin/claude/src/model_picker_card.dart | 6 +- .../claude/src/permission_mode_control.dart | 16 +- lib/builtin/claude/src/prompt_card.dart | 170 ++++++++++++++---- lib/builtin/claude/src/running_indicator.dart | 2 +- lib/builtin/claude/src/session_picker.dart | 12 +- lib/builtin/claude/src/team_chat_sidebar.dart | 64 +++++-- .../i18n/catalog/builtin.claude_en_us.json | 164 ++++++++++++++++- 23 files changed, 870 insertions(+), 193 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 3a2523af..fa860475 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5939,3 +5939,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.931', '2026-06-18 13:52:52.931', NULL, '4d4b842c99b6ff11a2820f9bccd0e73c', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.931', '2026-06-18 13:52:52.931', NULL, '8a7fef08c302d2119d32ceb18a58346e', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 13:52:52', '2026-06-18 13:52:52.932', '2026-06-18 13:52:52.932', NULL, 'f0b77fdf0ef68d872155912ef3882cb1', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'status', 'in_progress', 'done', NULL, '2026-06-18 19:14:41', '2026-06-18 19:14:41.116', '2026-06-18 19:14:41.116', NULL, '2bf6c6fbef7a2e950fc1f31541f8dc22', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 792e04b5..ebf476c0 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7328,3 +7328,10 @@ Several have no catalog file yet — create lib/kernel/src/i18n/catalog/builtin. Can be split into smaller tasks if a single PR is too large; grouped here to keep the long tail in one tracked place. Acceptance: no hardcoded user-facing literals remain in any of these extensions; each has a catalog file with the keys it uses; no orphaned keys left in editor/files/terminal.', 'in_progress', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:41.092', '2026-06-18 13:52:52.932', NULL, '3313d03f3d8428d61aa3528c2f4371db', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDA0WCPG458GF5FXTXM9VY98', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate builtin.claude UI labels to the catalog (~35 strings)', 'Largest single offender in the audit: ~35 hardcoded user-facing strings in builtin.claude bypass i18n, despite builtin.claude_en_us.json already existing (currently tab.title + 4 orphaned status.* keys). + +Route the hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(key, namespace: ''builtin.claude'', placeholder: ), and add the keys to the catalog. Hotspots from the audit include lib/builtin/claude/src/prompt_card.dart (e.g. :314 ''Review your answers'', :372 ''Review'') and the permission/AskUserQuestion interaction widgets — sweep the whole extension, not just those lines. + +Watch the interaction-zone widgets (D-78): keep them display-only; the i18n change is label-only, no behavioural change. + +Acceptance: no hardcoded user-facing string literals remain in lib/builtin/claude/ (single-char glyphs like ''<'' / ''+'' used as icons are exempt — note any left); all new keys present in the catalog; existing orphaned status.* keys either wired up or removed.', 'done', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:20.180', '2026-06-18 19:14:41.116', NULL, 'f231ab99f6066bf5bf45085d0c7b02eb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/lib/builtin/claude/src/claude_banner.dart b/lib/builtin/claude/src/claude_banner.dart index bc328389..2d6b591f 100644 --- a/lib/builtin/claude/src/claude_banner.dart +++ b/lib/builtin/claude/src/claude_banner.dart @@ -35,7 +35,12 @@ class ClaudeBanner extends StatelessWidget { children: [ const ClideSvgView.asset('assets/logo/logo.svg', width: 60, height: 60), const SizedBox(height: 18), - ClideText('Claude', fontSize: clideFontDialogTitle, color: claudeAccent, fontWeight: FontWeight.w500), + ClideText( + ClideSettings.i18n.string(context, 'banner.title', namespace: 'builtin.claude', placeholder: 'Claude'), + fontSize: clideFontDialogTitle, + color: claudeAccent, + fontWeight: FontWeight.w500, + ), const SizedBox(height: 2), ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), const SizedBox(height: 16), @@ -45,7 +50,16 @@ class ClaudeBanner extends StatelessWidget { ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)), ], const SizedBox(height: 16), - const ClideText('Warming up — your conversation will appear here.', fontSize: clideFontSmall, muted: true), + ClideText( + ClideSettings.i18n.string( + context, + 'banner.warmingUp', + namespace: 'builtin.claude', + placeholder: 'Warming up — your conversation will appear here.', + ), + fontSize: clideFontSmall, + muted: true, + ), ], ), ); diff --git a/lib/builtin/claude/src/claude_composer.dart b/lib/builtin/claude/src/claude_composer.dart index 8e696cfb..342d5cf5 100644 --- a/lib/builtin/claude/src/claude_composer.dart +++ b/lib/builtin/claude/src/claude_composer.dart @@ -44,7 +44,7 @@ class ClaudeComposer extends StatefulWidget { super.key, required this.onSubmit, this.enabled = true, - this.hint = 'Message Claude… (Enter to send · Shift+Enter for newline)', + this.hint, this.pasteResolver, this.slashCommandsResolver, this.onInterrupt, @@ -63,7 +63,11 @@ class ClaudeComposer extends StatefulWidget { final void Function(String text) onSubmit; final bool enabled; - final String hint; + + /// Placeholder shown when the composer is empty. Null falls back to the + /// i18n catalog default (`composer.hint`), resolved at render so it honours + /// the live locale (D-21). + final String? hint; /// Optional override of paste handling: returns the attachments on the /// clipboard (files / images), or an empty list to fall back to the @@ -417,6 +421,14 @@ class _ClaudeComposerState extends State { final theme = ClideSettings.theme.of(context).surface; final hasText = _controller.text.isNotEmpty; final fg = widget.enabled ? theme.globalForeground : theme.globalTextMuted; + final hint = + widget.hint ?? + ClideSettings.i18n.string( + context, + 'composer.hint', + namespace: 'builtin.claude', + placeholder: 'Message Claude… (Enter to send · Shift+Enter for newline)', + ); return Padding( padding: const EdgeInsets.fromLTRB(10, 6, 10, 10), @@ -444,10 +456,15 @@ class _ClaudeComposerState extends State { const RunningIndicator(), const Spacer(), ClideButton( - label: 'Stop ⎋', + label: ClideSettings.i18n.string(context, 'composer.stop', namespace: 'builtin.claude', placeholder: 'Stop ⎋'), variant: ClideButtonVariant.primary, onPressed: widget.onInterrupt, - semanticHint: 'Interrupt the running turn (Escape)', + semanticHint: ClideSettings.i18n.string( + context, + 'composer.stop.hint', + namespace: 'builtin.claude', + placeholder: 'Interrupt the running turn (Escape)', + ), ), ], ), @@ -455,14 +472,14 @@ class _ClaudeComposerState extends State { if (_attachments.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 8), - child: Wrap(spacing: 6, runSpacing: 6, children: [for (final a in _attachments) _chip(theme, a)]), + child: Wrap(spacing: 6, runSpacing: 6, children: [for (final a in _attachments) _chip(context, theme, a)]), ), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: Semantics( - label: widget.hint, + label: hint, textField: true, child: Shortcuts( shortcuts: const { @@ -486,7 +503,7 @@ class _ClaudeComposerState extends State { }, child: Stack( children: [ - if (!hasText) Positioned(left: 0, top: 0, right: 0, child: ClideText(widget.hint, muted: true, fontSize: clideFontBody)), + if (!hasText) Positioned(left: 0, top: 0, right: 0, child: ClideText(hint, muted: true, fontSize: clideFontBody)), EditableText( controller: _controller, focusNode: _focus, @@ -521,7 +538,7 @@ class _ClaudeComposerState extends State { /// One attachment chip: a thumbnail (images) or file icon (other types), /// the filename, and a remove × that cancels the attachment before send. - Widget _chip(SurfaceTokens theme, ComposerAttachment a) { + Widget _chip(BuildContext context, SurfaceTokens theme, ComposerAttachment a) { return Container( constraints: const BoxConstraints(maxWidth: 220), decoration: BoxDecoration( @@ -541,7 +558,13 @@ class _ClaudeComposerState extends State { const SizedBox(width: 4), Semantics( button: true, - label: 'Remove ${a.fileName}', + label: ClideSettings.i18n.interpolated( + context, + 'composer.removeAttachment', + namespace: 'builtin.claude', + placeholder: 'Remove ${a.fileName}', + replacers: [I18nReplacer(from: '{name}', replace: a.fileName)], + ), child: GestureDetector( key: ValueKey('composer-remove-${a.path}'), onTap: () => _removeAttachment(a), diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index 6ad2e6d4..4ce178c5 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -833,7 +833,9 @@ class _ClaudePaneState extends State { }, ); } else { - body = const Center(child: ClideText('starting…', muted: true)); + body = Center( + child: ClideText(ClideSettings.i18n.string(context, 'pane.starting', namespace: 'builtin.claude', placeholder: 'starting…'), muted: true), + ); } final content = widget.showChrome ? ClidePaneChrome(title: title, subtitle: _error ?? _statusLine, child: body) : body; @@ -856,7 +858,13 @@ class _ModeBadge extends StatelessWidget { @override Widget build(BuildContext context) { return Semantics( - label: 'permission mode: ${permissionModeLabel(mode)}', + label: ClideSettings.i18n.interpolated( + context, + 'pane.modeBadge.semantics', + namespace: 'builtin.claude', + placeholder: 'permission mode: ${permissionModeLabel(mode)}', + replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(mode))], + ), excludeSemantics: true, child: ClideText( permissionModeLabel(mode), diff --git a/lib/builtin/claude/src/claude_task_dock.dart b/lib/builtin/claude/src/claude_task_dock.dart index 502b97bb..0d897771 100644 --- a/lib/builtin/claude/src/claude_task_dock.dart +++ b/lib/builtin/claude/src/claude_task_dock.dart @@ -33,13 +33,28 @@ class _ClaudeTaskDockState extends State { final done = tasks.where((t) => t.status == TaskStatus.completed).length; final inProgress = tasks.where((t) => t.status == TaskStatus.inProgress); final current = inProgress.isEmpty ? null : inProgress.first.text; - final summary = '${tasks.length} task${tasks.length == 1 ? '' : 's'} · $done done'; + final taskWord = tasks.length == 1 + ? ClideSettings.i18n.string(context, 'taskDock.task.singular', namespace: 'builtin.claude', placeholder: 'task') + : ClideSettings.i18n.string(context, 'taskDock.task.plural', namespace: 'builtin.claude', placeholder: 'tasks'); + final summary = ClideSettings.i18n.interpolated( + context, + 'taskDock.summary', + namespace: 'builtin.claude', + placeholder: '${tasks.length} $taskWord · $done done', + replacers: [ + I18nReplacer(from: '{count}', replace: '${tasks.length}'), + I18nReplacer(from: '{tasks}', replace: taskWord), + I18nReplacer(from: '{done}', replace: '$done'), + ], + ); return Padding( padding: const EdgeInsets.fromLTRB(10, 0, 10, 6), child: ClideTappable( onTap: () => setState(() => _expanded = !_expanded), - tooltip: _expanded ? 'Collapse tasks' : 'Expand tasks', + tooltip: _expanded + ? ClideSettings.i18n.string(context, 'taskDock.collapse', namespace: 'builtin.claude', placeholder: 'Collapse tasks') + : ClideSettings.i18n.string(context, 'taskDock.expand', namespace: 'builtin.claude', placeholder: 'Expand tasks'), builder: (context, hovered, focused) => Container( decoration: BoxDecoration( color: (hovered || focused) ? tokens.listItemHoverBackground : tokens.listItemBackground, @@ -53,7 +68,21 @@ class _ClaudeTaskDockState extends State { // (its inner text is announced via the label, so exclude it). Semantics( button: true, - label: 'Claude task list, $summary, ${_expanded ? 'expanded' : 'collapsed'}', + label: ClideSettings.i18n.interpolated( + context, + 'taskDock.semantics', + namespace: 'builtin.claude', + placeholder: 'Claude task list, $summary, ${_expanded ? 'expanded' : 'collapsed'}', + replacers: [ + I18nReplacer(from: '{summary}', replace: summary), + I18nReplacer( + from: '{state}', + replace: _expanded + ? ClideSettings.i18n.string(context, 'taskDock.state.expanded', namespace: 'builtin.claude', placeholder: 'expanded') + : ClideSettings.i18n.string(context, 'taskDock.state.collapsed', namespace: 'builtin.claude', placeholder: 'collapsed'), + ), + ], + ), excludeSemantics: true, child: _summaryRow(tokens, summary, current), ), @@ -91,14 +120,35 @@ class _ClaudeTaskDockState extends State { Widget _taskRow(SurfaceTokens tokens, TaskItem t) { final (String glyph, Color color, String word) = switch (t.status) { - TaskStatus.completed => ('check-circle', tokens.statusSuccess, 'done'), - TaskStatus.inProgress => ('circle-half', tokens.globalFocus, 'in progress'), - TaskStatus.pending => ('circle', tokens.globalTextMuted, 'pending'), + TaskStatus.completed => ( + 'check-circle', + tokens.statusSuccess, + ClideSettings.i18n.string(context, 'taskDock.status.done', namespace: 'builtin.claude', placeholder: 'done'), + ), + TaskStatus.inProgress => ( + 'circle-half', + tokens.globalFocus, + ClideSettings.i18n.string(context, 'taskDock.status.inProgress', namespace: 'builtin.claude', placeholder: 'in progress'), + ), + TaskStatus.pending => ( + 'circle', + tokens.globalTextMuted, + ClideSettings.i18n.string(context, 'taskDock.status.pending', namespace: 'builtin.claude', placeholder: 'pending'), + ), }; return Padding( padding: const EdgeInsets.only(top: 4), child: Semantics( - label: '${t.text}, $word', + label: ClideSettings.i18n.interpolated( + context, + 'taskDock.row.semantics', + namespace: 'builtin.claude', + placeholder: '${t.text}, $word', + replacers: [ + I18nReplacer(from: '{text}', replace: t.text), + I18nReplacer(from: '{status}', replace: word), + ], + ), container: true, excludeSemantics: true, child: Row( diff --git a/lib/builtin/claude/src/conversation_card.dart b/lib/builtin/claude/src/conversation_card.dart index 26c2925a..828a5753 100644 --- a/lib/builtin/claude/src/conversation_card.dart +++ b/lib/builtin/claude/src/conversation_card.dart @@ -270,7 +270,9 @@ class _ConversationCardState extends State { } Widget _caret(SurfaceTokens tokens) { - final label = _collapsed ? 'Expand' : 'Collapse'; + final label = _collapsed + ? ClideSettings.i18n.string(context, 'card.expand', namespace: 'builtin.claude', placeholder: 'Expand') + : ClideSettings.i18n.string(context, 'card.collapse', namespace: 'builtin.claude', placeholder: 'Collapse'); return Semantics( button: true, label: label, @@ -298,11 +300,11 @@ class _ConversationCardState extends State { case ConversationCardStatus.success: icon = const CheckIcon(); color = tokens.statusSuccess; - label = 'succeeded'; + label = ClideSettings.i18n.string(context, 'card.succeeded', namespace: 'builtin.claude', placeholder: 'succeeded'); case ConversationCardStatus.error: icon = const CloseIcon(); color = tokens.statusError; - label = 'failed'; + label = ClideSettings.i18n.string(context, 'card.failed', namespace: 'builtin.claude', placeholder: 'failed'); case ConversationCardStatus.none: return const SizedBox.shrink(); } @@ -331,7 +333,9 @@ class _ConversationCardState extends State { List _actions(SurfaceTokens tokens) { final items = <_ActionItem>[]; - if (widget.copyText != null) items.add(_ActionItem('copy', _copy)); + if (widget.copyText != null) { + items.add(_ActionItem(ClideSettings.i18n.string(context, 'card.copy', namespace: 'builtin.claude', placeholder: 'copy'), _copy)); + } for (final a in widget.actions) { items.add(_ActionItem(a.label, a.onInvoke)); } diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index bc5b929a..d9165dcd 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -303,7 +303,14 @@ class _ConversationViewState extends State { if (items.isEmpty) { return ColoredBox( color: tokens.panelBackground, - child: widget.emptyState ?? const Center(child: ClideText('Waiting for Claude…', muted: true)), + child: + widget.emptyState ?? + Center( + child: ClideText( + ClideSettings.i18n.string(context, 'conversation.empty', namespace: 'builtin.claude', placeholder: 'Waiting for Claude…'), + muted: true, + ), + ), ); } @@ -530,12 +537,20 @@ class _BashLiveTailState extends State<_BashLiveTail> { Widget build(BuildContext context) { final term = _terminal; if (term == null) { - return ClideText('no independent source to follow', muted: true, fontSize: clideFontMeta); + return ClideText( + ClideSettings.i18n.string(context, 'conversation.bashTail.empty', namespace: 'builtin.claude', placeholder: 'no independent source to follow'), + muted: true, + fontSize: clideFontMeta, + ); } return SizedBox( height: 160, child: ClipRect( - child: ClidePtyView(terminal: term, label: 'live tail', fontSize: clideFontMeta), + child: ClidePtyView( + terminal: term, + label: ClideSettings.i18n.string(context, 'conversation.bashTail.label', namespace: 'builtin.claude', placeholder: 'live tail'), + fontSize: clideFontMeta, + ), ), ); } @@ -614,7 +629,9 @@ class _ConversationTurn extends StatelessWidget { // carded tool calls. variant: ConversationCardVariant.bordered, accent: tokens.globalTextMuted, - label: i.isSidechain ? 'agent prompt' : 'context', + label: i.isSidechain + ? ClideSettings.i18n.string(context, 'conversation.label.agentPrompt', namespace: 'builtin.claude', placeholder: 'agent prompt') + : ClideSettings.i18n.string(context, 'conversation.label.context', namespace: 'builtin.claude', placeholder: 'context'), copyText: i.text, collapsible: true, collapsedByDefault: true, @@ -624,7 +641,7 @@ class _ConversationTurn extends StatelessWidget { ), UserMessage() => ConversationCard( accent: tokens.globalFocus, - label: 'you', + label: ClideSettings.i18n.string(context, 'conversation.label.you', namespace: 'builtin.claude', placeholder: 'you'), copyText: i.text, margin: _childMargin, // Pasted-image @path tokens render as inline thumbnails that open the @@ -644,7 +661,7 @@ class _ConversationTurn extends StatelessWidget { AssistantTextMessage() when i.synthetic => ConversationCard( variant: ConversationCardVariant.bordered, accent: tokens.globalTextMuted, - label: 'clide', + label: ClideSettings.i18n.string(context, 'conversation.label.clide', namespace: 'builtin.claude', placeholder: 'clide'), copyText: i.text, margin: _childMargin, body: ClideText(i.text, muted: true, fontSize: clideFontMeta), @@ -654,7 +671,9 @@ class _ConversationTurn extends StatelessWidget { // coral claudeAccent is reserved for the real main-thread Claude. AssistantTextMessage() => ConversationCard( accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent, - label: i.isSidechain ? 'agent' : 'claude', + label: i.isSidechain + ? ClideSettings.i18n.string(context, 'conversation.label.agent', namespace: 'builtin.claude', placeholder: 'agent') + : ClideSettings.i18n.string(context, 'conversation.label.claude', namespace: 'builtin.claude', placeholder: 'claude'), copyText: i.text, margin: _childMargin, body: ClideMarkdown( @@ -669,7 +688,9 @@ class _ConversationTurn extends StatelessWidget { // Framed + muted like the context card (T-306). variant: ConversationCardVariant.bordered, accent: tokens.globalTextMuted, - label: i.isSidechain ? 'agent thinking' : 'thinking', + label: i.isSidechain + ? ClideSettings.i18n.string(context, 'conversation.label.agentThinking', namespace: 'builtin.claude', placeholder: 'agent thinking') + : ClideSettings.i18n.string(context, 'conversation.label.thinking', namespace: 'builtin.claude', placeholder: 'thinking'), copyText: i.thinking, collapsible: true, collapsedByDefault: true, @@ -677,8 +698,8 @@ class _ConversationTurn extends StatelessWidget { margin: _childMargin, body: ClideText(i.thinking, muted: true, fontSize: clideFontMeta), ), - AssistantToolUse() => collapseTools ? _toolUseCollapser(i) : _toolContentCard(i), - ToolResultMessage() => _toolResult(i), + AssistantToolUse() => collapseTools ? _toolUseCollapser(context, i) : _toolContentCard(context, i), + ToolResultMessage() => _toolResult(context, i), ImageMessage() => _image(context, i), }; } @@ -692,7 +713,7 @@ class _ConversationTurn extends StatelessWidget { final caption = m.caption; return ConversationCard( accent: tokens.globalTextMuted, - label: 'image', + label: ClideSettings.i18n.string(context, 'conversation.label.image', namespace: 'builtin.claude', placeholder: 'image'), copyText: m.path, body: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -712,7 +733,7 @@ class _ConversationTurn extends StatelessWidget { image: ClideFileImage(m.path), fit: BoxFit.contain, alignment: Alignment.centerLeft, - errorBuilder: (_, _, _) => _imagePlaceholder(m.path), + errorBuilder: (_, _, _) => _imagePlaceholder(context, m.path), ), ), ), @@ -728,12 +749,12 @@ class _ConversationTurn extends StatelessWidget { ClideKernel.of(context).dialog.show( (ctx, dismiss) => ClideLightbox( onDismiss: dismiss, - child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(path)), + child: Image(image: ClideFileImage(path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _imagePlaceholder(ctx, path)), ), ); } - Widget _imagePlaceholder(String path) => Container( + Widget _imagePlaceholder(BuildContext context, String path) => Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: tokens.panelBorder), @@ -745,7 +766,18 @@ class _ConversationTurn extends StatelessWidget { ClideIcon(PhosphorIcons.byName('image'), size: 16, color: tokens.globalTextMuted), const SizedBox(width: 8), Flexible( - child: ClideText('could not load $path', fontSize: clideFontMeta, color: tokens.globalTextMuted, maxLines: 1), + child: ClideText( + ClideSettings.i18n.interpolated( + context, + 'conversation.imagePlaceholder', + namespace: 'builtin.claude', + placeholder: 'could not load $path', + replacers: [I18nReplacer(from: '{path}', replace: path)], + ), + fontSize: clideFontMeta, + color: tokens.globalTextMuted, + maxLines: 1, + ), ), ], ), @@ -757,13 +789,13 @@ class _ConversationTurn extends StatelessWidget { /// — pushed up from the item; the inner card holds the call body + segments /// and its own per-item mark. An Agent/Task call also nests its visible /// sub-agent run in a second collapser below (T-264). - Widget _toolUseCollapser(AssistantToolUse t) { + Widget _toolUseCollapser(BuildContext context, AssistantToolUse t) { // A Workflow tool-use with a live run (T-416) renders the dedicated run // card — phases, agent rows, status — instead of the generic tool card. No // run yet (pre-progress, or on reload where the system events are gone) // falls through to the generic collapser below. if (t.name == 'Workflow' && workflows[t.toolUseId] != null) { - return _workflowCard(t, workflows[t.toolUseId]!); + return _workflowCard(context, t, workflows[t.toolUseId]!); } final outcome = toolUseOutcomes[t.toolUseId]; final color = outcome == null ? tokens.globalFocus : (outcome ? tokens.statusSuccess : tokens.statusError); @@ -773,7 +805,7 @@ class _ConversationTurn extends StatelessWidget { collapsedSummary: _toolUseSummary(t), counter: '1 step', status: _toolRunStatus(t), - children: [_toolContentCard(t)], + children: [_toolContentCard(context, t)], ); final runItems = _isAgentTool(t.name) ? (runByToolUseId[t.toolUseId] ?? const []) : const []; @@ -785,7 +817,7 @@ class _ConversationTurn extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 12), child: ClideCollapserCard( - label: 'agent run', + 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', children: [ @@ -814,23 +846,23 @@ class _ConversationTurn extends StatelessWidget { /// while running, check when done) and a `done/total agents` counter; the body /// lists each fanned-out agent — grouped under phase headers when the workflow /// declared phases — plus the run's usage and the orchestration script. - Widget _workflowCard(AssistantToolUse t, WorkflowRun run) { - final title = run.name ?? 'workflow'; + 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 detail = run.done ? (run.summary ?? run.description) : run.description; final collapsedSummary = (detail == null || detail == title) ? title : '$title · $detail'; return ClideCollapserCard( - label: 'workflow', + label: ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'), color: color, collapsedSummary: collapsedSummary, counter: counter, status: run.done ? ClideRunStatus.success : ClideRunStatus.running, - children: [_workflowBody(t, run)], + children: [_workflowBody(context, t, run)], ); } - Widget _workflowBody(AssistantToolUse t, WorkflowRun run) { + Widget _workflowBody(BuildContext context, AssistantToolUse t, WorkflowRun run) { final agents = run.orderedAgents; final phases = run.orderedPhases; final rows = []; @@ -851,25 +883,31 @@ class _ConversationTurn extends StatelessWidget { rows.addAll(agents.where((a) => a.phaseIndex == null).map(_workflowAgentRow)); } if (rows.isEmpty) { - rows.add(ClideText('Launching…', muted: true, fontSize: clideFontMeta)); + rows.add( + ClideText( + ClideSettings.i18n.string(context, 'conversation.workflow.launching', namespace: 'builtin.claude', placeholder: 'Launching…'), + muted: true, + fontSize: clideFontMeta, + ), + ); } final script = t.input['script']; return ConversationCard( variant: ConversationCardVariant.bordered, accent: run.done ? tokens.statusSuccess : tokens.globalFocus, - label: run.name ?? 'workflow', + label: run.name ?? ClideSettings.i18n.string(context, 'conversation.label.workflow', namespace: 'builtin.claude', placeholder: 'workflow'), copyText: script is String ? script : const JsonEncoder.withIndent(' ').convert(t.input), body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: rows), extraSegments: [ if (run.totalTokens != null && run.totalTokens! > 0) CardSegment( - label: 'usage', + label: ClideSettings.i18n.string(context, 'conversation.segment.usage', namespace: 'builtin.claude', placeholder: 'usage'), child: ClideText('${run.totalTokens} tokens${run.durationMs != null ? ' · ${run.durationMs} ms' : ''}', muted: true, fontSize: clideFontMeta), ), if (script is String) CardSegment( - label: 'script', + label: ClideSettings.i18n.string(context, 'conversation.segment.script', namespace: 'builtin.claude', placeholder: 'script'), child: ClideCodeBlock(source: script, language: 'javascript'), ), ], @@ -906,7 +944,7 @@ class _ConversationTurn extends StatelessWidget { /// CALL/PROMPT/RESULT segments + its own per-item status mark, with NO own /// collapse caret — the enclosing collapser owns collapse. Used both as a /// run/edit child and as the single child of a standalone tool's collapser. - Widget _toolContentCard(AssistantToolUse t) { + Widget _toolContentCard(BuildContext context, AssistantToolUse t) { // T-262: fold the paired result into this card. A successful result becomes // a "result" segment below the call + a green header check; a failed result // stamps a red header cross. No result yet (in-flight) → no mark, no segment. @@ -926,12 +964,12 @@ class _ConversationTurn extends StatelessWidget { final segments = [ for (final p in promptsByToolUseId[t.toolUseId] ?? const []) CardSegment( - label: 'prompt', + label: ClideSettings.i18n.string(context, 'conversation.segment.prompt', namespace: 'builtin.claude', placeholder: 'prompt'), child: ClideText(p.text, muted: true, fontSize: clideFontMeta), ), if (succeeded && !(isAgent && hasRun)) CardSegment( - label: 'result', + label: ClideSettings.i18n.string(context, 'conversation.segment.result', namespace: 'builtin.claude', placeholder: 'result'), child: ClideCodeBlock(source: result.content, language: _resultLanguage(t)), ), // T-325: a Bash card that follows a file (`tail -f …`) gets a live, @@ -939,7 +977,7 @@ class _ConversationTurn extends StatelessWidget { // while the card is expanded (the collapser builds segments on expand). if (t.name == 'Bash' && t.input['command'] is String && bashHasTailIntent(t.input['command'] as String)) CardSegment( - label: 'live tail', + label: ClideSettings.i18n.string(context, 'conversation.segment.liveTail', namespace: 'builtin.claude', placeholder: 'live tail'), child: _BashLiveTail(command: t.input['command'] as String), ), ]; @@ -955,7 +993,7 @@ class _ConversationTurn extends StatelessWidget { label: t.name, copyText: const JsonEncoder.withIndent(' ').convert(t.input), status: status, - body: toolInputBody(tokens, t.name, t.input, mono), + body: toolInputBody(context, tokens, t.name, t.input, mono), extraSegments: segments, // Inside a collapser the surrounding padding is even on all sides // (T-305): a matching bottom margin is the canvas's bottom inset and the @@ -987,10 +1025,12 @@ class _ConversationTurn extends StatelessWidget { } } - Widget _toolResult(ToolResultMessage t) { + Widget _toolResult(BuildContext context, ToolResultMessage t) { final paired = toolUseById[t.toolUseId]; final accent = t.isError ? tokens.statusError : tokens.globalTextMuted; - final label = t.isError ? 'error' : 'result'; + final label = t.isError + ? ClideSettings.i18n.string(context, 'conversation.label.error', namespace: 'builtin.claude', placeholder: 'error') + : ClideSettings.i18n.string(context, 'conversation.label.result', namespace: 'builtin.claude', placeholder: 'result'); // Error result: render the error message prominently (T-168). If we have // the paired tool_use, show the tool name as a sub-label so the user can @@ -1002,7 +1042,7 @@ class _ConversationTurn extends StatelessWidget { if (t.isError) { final quiet = quietErrorToolUseIds.contains(t.toolUseId); final multiline = t.content.contains('\n'); - final errLabel = quiet ? 'denied' : label; + final errLabel = quiet ? ClideSettings.i18n.string(context, 'conversation.label.denied', namespace: 'builtin.claude', placeholder: 'denied') : label; return ConversationCard( variant: ConversationCardVariant.bordered, accent: quiet ? tokens.globalTextMuted : accent, @@ -1086,7 +1126,7 @@ class _ActivityCard extends StatelessWidget { Widget build(BuildContext context) { final count = items.length; return ClideCollapserCard( - label: 'Activity', + label: ClideSettings.i18n.string(context, 'conversation.cluster.activity', namespace: 'builtin.claude', placeholder: 'Activity'), collapsedSummary: _summarizeActivity(items.last), counter: count == 1 ? '1 step' : '$count steps', status: _runStatus(items, resultByToolUseId), @@ -1152,7 +1192,7 @@ class _EditRunCard extends StatelessWidget { Widget build(BuildContext context) { final count = edits.length; return ClideCollapserCard( - label: 'Edits', + label: ClideSettings.i18n.string(context, 'conversation.cluster.edits', namespace: 'builtin.claude', placeholder: 'Edits'), collapsedSummary: _summarizeActivity(edits.last), counter: count == 1 ? '1 edit' : '$count edits', status: _runStatus(edits, resultByToolUseId), diff --git a/lib/builtin/claude/src/image_thumbnail.dart b/lib/builtin/claude/src/image_thumbnail.dart index f488a81f..f96c93e7 100644 --- a/lib/builtin/claude/src/image_thumbnail.dart +++ b/lib/builtin/claude/src/image_thumbnail.dart @@ -49,7 +49,13 @@ class ImageThumbnail extends StatelessWidget { final t = ClideSettings.theme.of(context).surface; return Semantics( button: true, - label: 'Image $_fileName', + label: ClideSettings.i18n.interpolated( + context, + 'image.semantics', + namespace: 'builtin.claude', + placeholder: 'Image $_fileName', + replacers: [I18nReplacer(from: '{name}', replace: _fileName)], + ), excludeSemantics: true, child: ClideTappable( onTap: () => openImageLightbox(context, path), diff --git a/lib/builtin/claude/src/meta_sidebar/activity_tab.dart b/lib/builtin/claude/src/meta_sidebar/activity_tab.dart index dbe93130..9b30af0c 100644 --- a/lib/builtin/claude/src/meta_sidebar/activity_tab.dart +++ b/lib/builtin/claude/src/meta_sidebar/activity_tab.dart @@ -49,21 +49,37 @@ class ActivityTabView extends StatelessWidget { final latest = stats.latest; final u = usage; final sections = [ - ..._workflowSection(tokens), + ..._workflowSection(context, tokens), if (u != null) - MetaSection('USAGE', [ - if (u.session != null) MetaRow('session', u.session!), - if (u.week != null) MetaRow('week (all)', u.week!), - if (u.weekSonnet != null) MetaRow('week (sonnet)', u.weekSonnet!), + MetaSection(ClideSettings.i18n.string(context, 'activity.section.usage', namespace: 'builtin.claude', placeholder: 'USAGE'), [ + if (u.session != null) + MetaRow(ClideSettings.i18n.string(context, 'activity.row.session', namespace: 'builtin.claude', placeholder: 'session'), u.session!), + if (u.week != null) + MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekAll', namespace: 'builtin.claude', placeholder: 'week (all)'), u.week!), + if (u.weekSonnet != null) + MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekSonnet', namespace: 'builtin.claude', placeholder: 'week (sonnet)'), u.weekSonnet!), ]), if (latest != null) - MetaSection('TODAY', [ - MetaRow('messages', '${latest.messageCount}'), - MetaRow('sessions', '${latest.sessionCount}'), - MetaRow('tool calls', '${latest.toolCallCount}'), + MetaSection(ClideSettings.i18n.string(context, 'activity.section.today', namespace: 'builtin.claude', placeholder: 'TODAY'), [ + MetaRow(ClideSettings.i18n.string(context, 'activity.row.messages', namespace: 'builtin.claude', placeholder: 'messages'), '${latest.messageCount}'), + MetaRow(ClideSettings.i18n.string(context, 'activity.row.sessions', namespace: 'builtin.claude', placeholder: 'sessions'), '${latest.sessionCount}'), + MetaRow( + ClideSettings.i18n.string(context, 'activity.row.toolCalls', namespace: 'builtin.claude', placeholder: 'tool calls'), + '${latest.toolCallCount}', + ), ]), - if (latest != null) MetaSection('LIFETIME', [MetaRow('messages', '${stats.lifetimeMessages}'), MetaRow('sessions', '${stats.lifetimeSessions}')]), - ..._runtimeSection(tokens), + if (latest != null) + MetaSection(ClideSettings.i18n.string(context, 'activity.section.lifetime', namespace: 'builtin.claude', placeholder: 'LIFETIME'), [ + MetaRow( + ClideSettings.i18n.string(context, 'activity.row.messages', namespace: 'builtin.claude', placeholder: 'messages'), + '${stats.lifetimeMessages}', + ), + MetaRow( + ClideSettings.i18n.string(context, 'activity.row.sessions', namespace: 'builtin.claude', placeholder: 'sessions'), + '${stats.lifetimeSessions}', + ), + ]), + ..._runtimeSection(context, tokens), ]; return ListView( @@ -73,20 +89,57 @@ class ActivityTabView extends StatelessWidget { // the builtin.claude/command bus — identical to typing the command. Padding( padding: const EdgeInsets.only(bottom: 8), - child: ClideText('SESSION', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader), + child: ClideText( + ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION'), + fontSize: clideFontSmall, + color: tokens.sidebarSectionHeader, + ), ), Row( children: [ - _control(context, tokens, 'clear', 'trash', '/clear'), - _control(context, tokens, 'compact', 'arrows-in-simple', '/compact'), - _control(context, tokens, 'fork', 'git-branch', '/fork'), - _control(context, tokens, 'resume', 'clock-counter-clockwise', '/resume'), + _control( + context, + tokens, + ClideSettings.i18n.string(context, 'activity.control.clear', namespace: 'builtin.claude', placeholder: 'clear'), + 'trash', + '/clear', + ), + _control( + context, + tokens, + ClideSettings.i18n.string(context, 'activity.control.compact', namespace: 'builtin.claude', placeholder: 'compact'), + 'arrows-in-simple', + '/compact', + ), + _control( + context, + tokens, + ClideSettings.i18n.string(context, 'activity.control.fork', namespace: 'builtin.claude', placeholder: 'fork'), + 'git-branch', + '/fork', + ), + _control( + context, + tokens, + ClideSettings.i18n.string(context, 'activity.control.resume', namespace: 'builtin.claude', placeholder: 'resume'), + 'clock-counter-clockwise', + '/resume', + ), const Spacer(), - _control(context, tokens, 'refresh usage', 'arrow-clockwise', '/usage'), + _control( + context, + tokens, + ClideSettings.i18n.string(context, 'activity.control.refreshUsage', namespace: 'builtin.claude', placeholder: 'refresh usage'), + 'arrow-clockwise', + '/usage', + ), ], ), const SizedBox(height: 6), - if (sections.isEmpty) metaPlaceholder('No activity recorded yet.') else ...metaTableChildren(tokens, sections), + if (sections.isEmpty) + metaPlaceholder(ClideSettings.i18n.string(context, 'activity.empty', namespace: 'builtin.claude', placeholder: 'No activity recorded yet.')) + else + ...metaTableChildren(tokens, sections), ], ); } @@ -94,11 +147,26 @@ class ActivityTabView extends StatelessWidget { Widget _control(BuildContext context, SurfaceTokens tokens, String label, String glyph, String command) { return Semantics( button: true, - label: '$label session', + label: ClideSettings.i18n.interpolated( + context, + 'activity.control.semantics', + namespace: 'builtin.claude', + placeholder: '$label session', + replacers: [I18nReplacer(from: '{label}', replace: label)], + ), excludeSemantics: true, onTap: () => _command(context, command), child: ClideTappable( - tooltip: '$label · $command', + tooltip: ClideSettings.i18n.interpolated( + context, + 'activity.control.tooltip', + namespace: 'builtin.claude', + placeholder: '$label · $command', + replacers: [ + I18nReplacer(from: '{label}', replace: label), + I18nReplacer(from: '{command}', replace: command), + ], + ), onTap: () => _command(context, command), builder: (ctx, hovered, _) => Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), @@ -111,31 +179,50 @@ class ActivityTabView extends StatelessWidget { /// An aggregate WORKFLOWS section while one or more workflow runs exist this /// session (T-416): a row per run — its name and `done/total agents`, tinted /// focus while running and success once complete. - List _workflowSection(SurfaceTokens tokens) { + List _workflowSection(BuildContext context, SurfaceTokens tokens) { final runs = workflows.values.toList(); if (runs.isEmpty) return const []; + final done = ClideSettings.i18n.string(context, 'activity.workflow.done', namespace: 'builtin.claude', placeholder: 'done'); + final starting = ClideSettings.i18n.string(context, 'activity.workflow.starting', namespace: 'builtin.claude', placeholder: 'starting'); + final fallback = ClideSettings.i18n.string(context, 'activity.workflow.fallback', namespace: 'builtin.claude', placeholder: 'workflow'); return [ - MetaSection('WORKFLOWS', [ + MetaSection(ClideSettings.i18n.string(context, 'activity.section.workflows', namespace: 'builtin.claude', placeholder: 'WORKFLOWS'), [ for (final r in runs) MetaRow( - r.name ?? r.taskId ?? 'workflow', - r.agentCount == 0 ? (r.done ? 'done' : 'starting') : '${r.doneCount}/${r.agentCount} agents${r.done ? ' ✓' : ''}', + r.name ?? r.taskId ?? fallback, + r.agentCount == 0 ? (r.done ? done : starting) : '${r.doneCount}/${r.agentCount} agents${r.done ? ' ✓' : ''}', valueColor: r.done ? tokens.statusSuccess : tokens.globalFocus, ), ]), ]; } - List _runtimeSection(SurfaceTokens tokens) { + List _runtimeSection(BuildContext context, SurfaceTokens tokens) { final st = primaryStatus; final skills = config?.skills.length; final rows = [ - if (st?.model != null) MetaRow('model', shortModelLabel(st!.model!), valueColor: tokens.globalFocus), - if (st?.effort != null) MetaRow('effort', st!.effort!), - if (st?.contextTokens != null) MetaRow('context', '${formatTokenCount(st!.contextTokens!)} ctx'), - if (st?.permissionMode != null) MetaRow('mode', permissionModeLabel(st!.permissionMode!)), - if (skills != null) MetaRow('skills', '$skills'), + if (st?.model != null) + MetaRow( + ClideSettings.i18n.string(context, 'activity.row.model', namespace: 'builtin.claude', placeholder: 'model'), + shortModelLabel(st!.model!), + valueColor: tokens.globalFocus, + ), + if (st?.effort != null) + MetaRow(ClideSettings.i18n.string(context, 'activity.row.effort', namespace: 'builtin.claude', placeholder: 'effort'), st!.effort!), + if (st?.contextTokens != null) + MetaRow( + ClideSettings.i18n.string(context, 'activity.row.context', namespace: 'builtin.claude', placeholder: 'context'), + '${formatTokenCount(st!.contextTokens!)} ctx', + ), + if (st?.permissionMode != null) + MetaRow( + ClideSettings.i18n.string(context, 'activity.row.mode', namespace: 'builtin.claude', placeholder: 'mode'), + permissionModeLabel(st!.permissionMode!), + ), + if (skills != null) MetaRow(ClideSettings.i18n.string(context, 'activity.row.skills', namespace: 'builtin.claude', placeholder: 'skills'), '$skills'), ]; - return rows.isEmpty ? const [] : [MetaSection('RUNTIME · primary', rows)]; + return rows.isEmpty + ? const [] + : [MetaSection(ClideSettings.i18n.string(context, 'activity.section.runtime', namespace: 'builtin.claude', placeholder: 'RUNTIME · primary'), rows)]; } } diff --git a/lib/builtin/claude/src/meta_sidebar/config_tab.dart b/lib/builtin/claude/src/meta_sidebar/config_tab.dart index cc6511ea..da586f33 100644 --- a/lib/builtin/claude/src/meta_sidebar/config_tab.dart +++ b/lib/builtin/claude/src/meta_sidebar/config_tab.dart @@ -44,7 +44,7 @@ class ConfigTabView extends StatelessWidget { final tokens = ClideSettings.theme.of(context).surface; final cfg = config; if (cfg == null) { - return metaPlaceholder('Claude environment not loaded.'); + return metaPlaceholder(ClideSettings.i18n.string(context, 'config.empty', namespace: 'builtin.claude', placeholder: 'Claude environment not loaded.')); } final settings = cfg.settings; final model = status?.model ?? cfg.probe?.model ?? settings['model']?.toString() ?? 'default'; @@ -56,26 +56,40 @@ class ConfigTabView extends StatelessWidget { // Pinned SETTINGS control panel — not collapsible. Padding( padding: const EdgeInsets.only(bottom: 8), - child: ClideText('SETTINGS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader), + child: ClideText( + ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS'), + fontSize: clideFontSmall, + color: tokens.sidebarSectionHeader, + ), ), SettingControlRow( - label: 'model', + label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'), value: model, valueColor: tokens.globalFocus, options: (models == null || models!.isEmpty) ? kFallbackModels : models!, isActive: (o) => o.value == model || model.toLowerCase().contains(o.value.toLowerCase()), command: 'model', ), - SettingControlRow(label: 'effort', value: effort, options: kEffortLevels, isActive: (o) => o.value == effort, command: 'effort'), SettingControlRow( - label: 'permission mode', + label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'), + value: effort, + options: kEffortLevels, + isActive: (o) => o.value == effort, + command: 'effort', + ), + SettingControlRow( + label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'), value: permissionModeLabel(mode), options: kPermissionModes, isActive: (o) => o.value == mode, command: 'permissions', ), - _configRow(tokens, 'output style', outputStyle), - _configRow(tokens, 'source', '~/.claude + .claude'), + _configRow(tokens, ClideSettings.i18n.string(context, 'config.row.outputStyle', namespace: 'builtin.claude', placeholder: 'output style'), outputStyle), + _configRow( + tokens, + ClideSettings.i18n.string(context, 'config.row.source', namespace: 'builtin.claude', placeholder: 'source'), + ClideSettings.i18n.string(context, 'config.row.source.value', namespace: 'builtin.claude', placeholder: '~/.claude + .claude'), + ), // ---- Accordion sections ---- for (final section in ConfigSection.values) _accordion(context, tokens, cfg, section), @@ -83,7 +97,16 @@ class ConfigTabView extends StatelessWidget { // Footer hint. Padding( padding: const EdgeInsets.only(top: 12), - child: ClideText('expand a list to see all · click a skill/agent/command → opens its .md', muted: true, fontSize: clideFontSmall), + child: ClideText( + ClideSettings.i18n.string( + context, + 'config.footer', + namespace: 'builtin.claude', + placeholder: 'expand a list to see all · click a skill/agent/command → opens its .md', + ), + muted: true, + fontSize: clideFontSmall, + ), ), ]; @@ -109,13 +132,13 @@ class ConfigTabView extends StatelessWidget { ); } - String _sectionLabel(ConfigSection section) => switch (section) { - ConfigSection.skills => 'SKILLS', - ConfigSection.agents => 'AGENTS', - ConfigSection.commands => 'COMMANDS', - ConfigSection.hooks => 'HOOKS', - ConfigSection.permissions => 'PERMISSIONS', - ConfigSection.mcpServers => 'MCP SERVERS', + String _sectionLabel(BuildContext context, ConfigSection section) => switch (section) { + ConfigSection.skills => ClideSettings.i18n.string(context, 'config.section.skills', namespace: 'builtin.claude', placeholder: 'SKILLS'), + ConfigSection.agents => ClideSettings.i18n.string(context, 'config.section.agents', namespace: 'builtin.claude', placeholder: 'AGENTS'), + ConfigSection.commands => ClideSettings.i18n.string(context, 'config.section.commands', namespace: 'builtin.claude', placeholder: 'COMMANDS'), + ConfigSection.hooks => ClideSettings.i18n.string(context, 'config.section.hooks', namespace: 'builtin.claude', placeholder: 'HOOKS'), + ConfigSection.permissions => ClideSettings.i18n.string(context, 'config.section.permissions', namespace: 'builtin.claude', placeholder: 'PERMISSIONS'), + ConfigSection.mcpServers => ClideSettings.i18n.string(context, 'config.section.mcpServers', namespace: 'builtin.claude', placeholder: 'MCP SERVERS'), }; int _sectionCount(ClaudeConfig config, ConfigSection section) => switch (section) { @@ -131,7 +154,7 @@ class ConfigTabView extends StatelessWidget { final isExpanded = expanded.contains(section); final children = isExpanded ? _sectionChildren(context, tokens, config, section) : const []; return ClideAccordion( - label: _sectionLabel(section), + label: _sectionLabel(context, section), count: _sectionCount(config, section), expanded: isExpanded, onToggle: () => onToggleSection(section), @@ -166,7 +189,7 @@ class ConfigTabView extends StatelessWidget { ), ]; case ConfigSection.permissions: - return _permissionRows(tokens, config.permissions); + return _permissionRows(context, tokens, config.permissions); case ConfigSection.mcpServers: return [ for (final srv in config.mcpServers) @@ -205,7 +228,7 @@ class ConfigTabView extends StatelessWidget { } /// Renders grouped allow/ask/deny permission rows, colour-coded by kind. - List _permissionRows(SurfaceTokens tokens, ClaudePermissions perms) { + List _permissionRows(BuildContext context, SurfaceTokens tokens, ClaudePermissions perms) { // allow → statusSuccess, ask → statusWarning, deny → statusError Color kindColor(ConfigPermKind k) => switch (k) { ConfigPermKind.allow => tokens.statusSuccess, @@ -214,9 +237,9 @@ class ConfigTabView extends StatelessWidget { }; String kindLabel(ConfigPermKind k) => switch (k) { - ConfigPermKind.allow => 'allow', - ConfigPermKind.ask => 'ask', - ConfigPermKind.deny => 'deny', + ConfigPermKind.allow => ClideSettings.i18n.string(context, 'config.perm.allow', namespace: 'builtin.claude', placeholder: 'allow'), + ConfigPermKind.ask => ClideSettings.i18n.string(context, 'config.perm.ask', namespace: 'builtin.claude', placeholder: 'ask'), + ConfigPermKind.deny => ClideSettings.i18n.string(context, 'config.perm.deny', namespace: 'builtin.claude', placeholder: 'deny'), }; final groups = [(ConfigPermKind.allow, perms.allow), (ConfigPermKind.ask, perms.ask), (ConfigPermKind.deny, perms.deny)]; @@ -320,18 +343,42 @@ class _SettingControlRowState extends State { ClideMenuItem( label: o.description.isEmpty ? o.displayName : '${o.displayName} — ${o.description}', active: widget.isActive(o), - semanticLabel: '${widget.label}: ${o.displayName}', + semanticLabel: ClideSettings.i18n.interpolated( + context, + 'config.control.option.semantics', + namespace: 'builtin.claude', + placeholder: '${widget.label}: ${o.displayName}', + replacers: [ + I18nReplacer(from: '{label}', replace: widget.label), + I18nReplacer(from: '{name}', replace: o.displayName), + ], + ), onSelect: () => _pick(o.value), ), ], ), anchor: Semantics( button: true, - label: '${widget.label}: ${widget.value}. Click to change.', + label: ClideSettings.i18n.interpolated( + context, + 'config.control.semantics', + namespace: 'builtin.claude', + placeholder: '${widget.label}: ${widget.value}. Click to change.', + replacers: [ + I18nReplacer(from: '{label}', replace: widget.label), + I18nReplacer(from: '{value}', replace: widget.value), + ], + ), excludeSemantics: true, onTap: _overlay.toggle, child: ClideTappable( - tooltip: 'change ${widget.label}', + tooltip: ClideSettings.i18n.interpolated( + context, + 'config.control.tooltip', + namespace: 'builtin.claude', + placeholder: 'change ${widget.label}', + replacers: [I18nReplacer(from: '{label}', replace: widget.label)], + ), onTap: _overlay.toggle, builder: (ctx, hovered, _) => DecoratedBox( decoration: BoxDecoration(color: hovered ? tokens.listItemHoverBackground : null, borderRadius: BorderRadius.circular(4)), diff --git a/lib/builtin/claude/src/meta_sidebar/permission_badge.dart b/lib/builtin/claude/src/meta_sidebar/permission_badge.dart index cb6a8b53..b9309644 100644 --- a/lib/builtin/claude/src/meta_sidebar/permission_badge.dart +++ b/lib/builtin/claude/src/meta_sidebar/permission_badge.dart @@ -40,15 +40,27 @@ class PermissionModeBadge extends StatelessWidget { final isBypass = mode == 'bypassPermissions'; final badgeColor = isBypass ? const Color(0xFFF06C6F) : tokens.globalFocus; - final tooltip = - 'Permission mode: ${permissionModeLabel(mode)}. ' - 'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.'; + final tooltip = ClideSettings.i18n.interpolated( + context, + 'permissionBadge.tooltip', + namespace: 'builtin.claude', + placeholder: + 'Permission mode: ${permissionModeLabel(mode)}. ' + 'Click to cycle default/acceptEdits/plan; Shift-click for bypassPermissions.', + replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(mode))], + ); return Padding( padding: const EdgeInsets.only(top: 3), child: Semantics( button: true, - label: 'Permission mode: $label', + label: ClideSettings.i18n.interpolated( + context, + 'permissionBadge.semantics', + namespace: 'builtin.claude', + placeholder: 'Permission mode: $label', + replacers: [I18nReplacer(from: '{label}', replace: label)], + ), excludeSemantics: true, onTap: () { if (HardwareKeyboard.instance.isShiftPressed) { diff --git a/lib/builtin/claude/src/meta_sidebar/roster_row.dart b/lib/builtin/claude/src/meta_sidebar/roster_row.dart index 854a5167..35e1f258 100644 --- a/lib/builtin/claude/src/meta_sidebar/roster_row.dart +++ b/lib/builtin/claude/src/meta_sidebar/roster_row.dart @@ -154,27 +154,40 @@ class _AgentRosterRowState extends State { child: Row( children: [ Expanded( - child: ClideText('Enable bypassPermissions? All tool calls will be auto-allowed.', fontSize: clideFontSmall, color: tokens.globalTextMuted), + child: ClideText( + ClideSettings.i18n.string( + context, + 'roster.bypass.confirmBody', + namespace: 'builtin.claude', + placeholder: 'Enable bypassPermissions? All tool calls will be auto-allowed.', + ), + fontSize: clideFontSmall, + color: tokens.globalTextMuted, + ), ), const SizedBox(width: 4), // Confirm Semantics( button: true, - label: 'Confirm bypass', + label: ClideSettings.i18n.string(context, 'roster.bypass.confirm.semantics', namespace: 'builtin.claude', placeholder: 'Confirm bypass'), excludeSemantics: true, onTap: () { setState(() => _confirmingBypass = false); widget.onSetPermissionMode(widget.member.name, 'bypassPermissions'); }, child: ClideTappable( - tooltip: 'Confirm', + tooltip: ClideSettings.i18n.string(context, 'roster.bypass.confirm.tooltip', namespace: 'builtin.claude', placeholder: 'Confirm'), onTap: () { setState(() => _confirmingBypass = false); widget.onSetPermissionMode(widget.member.name, 'bypassPermissions'); }, builder: (ctx, hovered, _) => Padding( padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2), - child: ClideText('OK', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalFocus), + child: ClideText( + ClideSettings.i18n.string(context, 'roster.bypass.ok', namespace: 'builtin.claude', placeholder: 'OK'), + fontSize: clideFontSmall, + color: hovered ? tokens.globalForeground : tokens.globalFocus, + ), ), ), ), @@ -182,15 +195,19 @@ class _AgentRosterRowState extends State { // Cancel Semantics( button: true, - label: 'Cancel bypass', + label: ClideSettings.i18n.string(context, 'roster.bypass.cancel.semantics', namespace: 'builtin.claude', placeholder: 'Cancel bypass'), excludeSemantics: true, onTap: () => setState(() => _confirmingBypass = false), child: ClideTappable( - tooltip: 'Cancel', + tooltip: ClideSettings.i18n.string(context, 'roster.bypass.cancel.tooltip', namespace: 'builtin.claude', placeholder: 'Cancel'), onTap: () => setState(() => _confirmingBypass = false), builder: (ctx, hovered, _) => Padding( padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2), - child: ClideText('Cancel', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), + child: ClideText( + ClideSettings.i18n.string(context, 'roster.bypass.cancel', namespace: 'builtin.claude', placeholder: 'Cancel'), + fontSize: clideFontSmall, + color: hovered ? tokens.globalForeground : tokens.globalTextMuted, + ), ), ), ), @@ -206,7 +223,9 @@ class _AgentRosterRowState extends State { // Show / hide MetaIconButton( painter: isVisible ? PhosphorIcons.byName('eye') : PhosphorIcons.byName('eye-slash'), - tooltip: isVisible ? 'Hide pane' : 'Show pane', + tooltip: isVisible + ? ClideSettings.i18n.string(context, 'roster.hidePane', namespace: 'builtin.claude', placeholder: 'Hide pane') + : ClideSettings.i18n.string(context, 'roster.showPane', namespace: 'builtin.claude', placeholder: 'Show pane'), color: tokens.globalTextMuted, onTap: () => isVisible ? widget.orchestrator!.hide(managed.id) : widget.orchestrator!.show(managed.id), ), @@ -216,28 +235,30 @@ class _AgentRosterRowState extends State { // NOTE: We use eye/eyeSlash as stand-ins until a dedicated speaker // icon is added to PhosphorIcons (no speaker codepoint yet). // The semantic tooltip still says mute/unmute so AT users are clear. - tooltip: isMuted ? 'Unmute messages' : 'Mute messages', + tooltip: isMuted + ? ClideSettings.i18n.string(context, 'roster.unmute', namespace: 'builtin.claude', placeholder: 'Unmute messages') + : ClideSettings.i18n.string(context, 'roster.mute', namespace: 'builtin.claude', placeholder: 'Mute messages'), color: isMuted ? tokens.globalFocus : tokens.globalTextMuted, onTap: () => isMuted ? widget.orchestrator!.unmute(managed.id) : widget.orchestrator!.mute(managed.id), ), // Inject message MetaIconButton( painter: PhosphorIcons.byName('chat-circle'), - tooltip: 'Inject message', + tooltip: ClideSettings.i18n.string(context, 'roster.inject', namespace: 'builtin.claude', placeholder: 'Inject message'), color: isInjecting ? tokens.globalFocus : tokens.globalTextMuted, onTap: () => widget.onToggleInject(widget.member.name), ), // Fork session (T-172): branch into a new pane without touching the original. MetaIconButton( painter: PhosphorIcons.byName('git-branch'), - tooltip: 'Fork session', + tooltip: ClideSettings.i18n.string(context, 'roster.fork', namespace: 'builtin.claude', placeholder: 'Fork session'), color: tokens.globalTextMuted, onTap: () => widget.onFork(widget.member.name), ), // Close session MetaIconButton( painter: PhosphorIcons.byName('x'), - tooltip: 'Close session', + tooltip: ClideSettings.i18n.string(context, 'roster.close', namespace: 'builtin.claude', placeholder: 'Close session'), color: tokens.globalTextMuted, onTap: () => widget.onClose(widget.member.name), ), @@ -262,7 +283,7 @@ class _AgentRosterRowState extends State { const SizedBox(width: 4), MetaIconButton( painter: PhosphorIcons.byName('x'), - tooltip: 'Cancel', + tooltip: ClideSettings.i18n.string(context, 'roster.inject.cancel', namespace: 'builtin.claude', placeholder: 'Cancel'), color: tokens.globalTextMuted, onTap: () => widget.onToggleInject(widget.member.name), ), diff --git a/lib/builtin/claude/src/meta_sidebar/tab_strip.dart b/lib/builtin/claude/src/meta_sidebar/tab_strip.dart index febad9e0..68585be3 100644 --- a/lib/builtin/claude/src/meta_sidebar/tab_strip.dart +++ b/lib/builtin/claude/src/meta_sidebar/tab_strip.dart @@ -29,7 +29,7 @@ class SidebarTabStrip extends StatelessWidget { child: Semantics( button: true, selected: t == current, - label: _label(t), + label: _label(context, t), excludeSemantics: true, onTap: () => onPick(t), child: ClideTappable( @@ -39,7 +39,11 @@ class SidebarTabStrip extends StatelessWidget { decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t == current ? tokens.globalFocus : const Color(0x00000000), width: 2)), ), - child: ClideText(_label(t), fontSize: clideFontSmall, color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted), + child: ClideText( + _label(context, t), + fontSize: clideFontSmall, + color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted, + ), ), ), ), @@ -49,9 +53,18 @@ class SidebarTabStrip extends StatelessWidget { ); } - String _label(SidebarTab t) => switch (t) { - SidebarTab.activity => 'Activity', - SidebarTab.team => memberCount == 0 ? 'Team' : 'Team · $memberCount', - SidebarTab.config => 'Config', + String _label(BuildContext context, SidebarTab t) => switch (t) { + SidebarTab.activity => ClideSettings.i18n.string(context, 'tabStrip.activity', namespace: 'builtin.claude', placeholder: 'Activity'), + SidebarTab.team => + memberCount == 0 + ? ClideSettings.i18n.string(context, 'tabStrip.team', namespace: 'builtin.claude', placeholder: 'Team') + : ClideSettings.i18n.interpolated( + context, + 'tabStrip.team.count', + namespace: 'builtin.claude', + placeholder: 'Team · $memberCount', + replacers: [I18nReplacer(from: '{count}', replace: '$memberCount')], + ), + SidebarTab.config => ClideSettings.i18n.string(context, 'tabStrip.config', namespace: 'builtin.claude', placeholder: 'Config'), }; } diff --git a/lib/builtin/claude/src/meta_sidebar/task_row.dart b/lib/builtin/claude/src/meta_sidebar/task_row.dart index 58c00a3a..d0451b61 100644 --- a/lib/builtin/claude/src/meta_sidebar/task_row.dart +++ b/lib/builtin/claude/src/meta_sidebar/task_row.dart @@ -54,7 +54,7 @@ class TaskRow extends StatelessWidget { if (broker != null && broker!.members.length > 1) MetaIconButton( painter: PhosphorIcons.byName('arrow-clockwise'), - tooltip: 'Reassign task', + tooltip: ClideSettings.i18n.string(context, 'taskRow.reassign', namespace: 'builtin.claude', placeholder: 'Reassign task'), color: tokens.globalTextMuted, onTap: () => _reassign(context), ), diff --git a/lib/builtin/claude/src/meta_sidebar/team_tab.dart b/lib/builtin/claude/src/meta_sidebar/team_tab.dart index a4cded66..48a2617d 100644 --- a/lib/builtin/claude/src/meta_sidebar/team_tab.dart +++ b/lib/builtin/claude/src/meta_sidebar/team_tab.dart @@ -50,7 +50,7 @@ class TeamTabView extends StatelessWidget { Widget build(BuildContext context) { final tokens = ClideSettings.theme.of(context).surface; if (members.isEmpty) { - return metaPlaceholder('No team active.'); + return metaPlaceholder(ClideSettings.i18n.string(context, 'team.empty', namespace: 'builtin.claude', placeholder: 'No team active.')); } final children = [ for (final m in members) @@ -71,7 +71,7 @@ class TeamTabView extends StatelessWidget { if (tasks.isNotEmpty) { children.add(const SizedBox(height: 12)); - children.add(_taskSection(tokens)); + children.add(_taskSection(context, tokens)); } // MESSAGES section (T-180): live broker chat feed + quick-post composer. @@ -85,11 +85,15 @@ class TeamTabView extends StatelessWidget { return ListView(padding: const EdgeInsets.all(12), children: children); } - Widget _taskSection(SurfaceTokens tokens) { + Widget _taskSection(BuildContext context, SurfaceTokens tokens) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('TASKS', fontSize: clideFontSmall, color: tokens.globalTextMuted), + ClideText( + ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS'), + fontSize: clideFontSmall, + color: tokens.globalTextMuted, + ), const SizedBox(height: 4), for (final t in tasks) TaskRow(task: t, members: members, broker: orchestrator?.broker), ], diff --git a/lib/builtin/claude/src/model_picker_card.dart b/lib/builtin/claude/src/model_picker_card.dart index 5198aa97..5fa10067 100644 --- a/lib/builtin/claude/src/model_picker_card.dart +++ b/lib/builtin/claude/src/model_picker_card.dart @@ -162,7 +162,11 @@ class _ModelPickerCardState extends State { Row( children: [ const Spacer(), - ClideButton(label: 'cancel', variant: ClideButtonVariant.subtle, onPressed: widget.onCancel), + ClideButton( + label: ClideSettings.i18n.string(context, 'modelPicker.cancel', namespace: 'builtin.claude', placeholder: 'cancel'), + variant: ClideButtonVariant.subtle, + onPressed: widget.onCancel, + ), ], ), ], diff --git a/lib/builtin/claude/src/permission_mode_control.dart b/lib/builtin/claude/src/permission_mode_control.dart index cfd56bb9..c0cdbde8 100644 --- a/lib/builtin/claude/src/permission_mode_control.dart +++ b/lib/builtin/claude/src/permission_mode_control.dart @@ -101,11 +101,23 @@ class _PermissionModeControlState extends State { final open = _overlay.isOpen; return Semantics( button: true, - label: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.', + label: ClideSettings.i18n.interpolated( + context, + 'permissionControl.semantics', + namespace: 'builtin.claude', + placeholder: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.', + replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(widget.mode))], + ), excludeSemantics: true, child: ClideTappable( onTap: _overlay.toggle, - tooltip: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)', + tooltip: ClideSettings.i18n.interpolated( + context, + 'permissionControl.tooltip', + namespace: 'builtin.claude', + placeholder: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)', + replacers: [I18nReplacer(from: '{mode}', replace: permissionModeLabel(widget.mode))], + ), builder: (ctx, hovered, _) => Container( width: 28, height: 28, diff --git a/lib/builtin/claude/src/prompt_card.dart b/lib/builtin/claude/src/prompt_card.dart index 3a7ddb91..69eee4ba 100644 --- a/lib/builtin/claude/src/prompt_card.dart +++ b/lib/builtin/claude/src/prompt_card.dart @@ -244,7 +244,13 @@ class _ToolPromptCardState extends State { final canRemember = p.permissionSuggestions.isNotEmpty; return ( tokens.statusWarning, - 'permission · ${p.displayName}', + ClideSettings.i18n.interpolated( + context, + 'prompt.permission.label', + namespace: 'builtin.claude', + placeholder: 'permission · ${p.displayName}', + replacers: [I18nReplacer(from: '{name}', replace: p.displayName)], + ), [ // Claude's description for Write/Edit is often just the file_path, // which the body already shows via _pathLine — suppress in that case @@ -265,25 +271,66 @@ class _ToolPromptCardState extends State { spacing: 8, runSpacing: 8, children: [ - ClideButton(label: '1. Allow', variant: ClideButtonVariant.primary, onPressed: () => _permAllow()), - if (canRemember) ClideButton(label: "2. Allow & don't ask again", onPressed: () => _permAllow(remember: true)), - ClideButton(label: '${canRemember ? '3' : '2'}. Deny', onPressed: _permDeny), ClideButton( - label: '${canRemember ? '4' : '3'}. Deny & simplify', - tooltip: 'Deny and ask Claude to retry this action in a simpler format — complex interactions don\'t work well with the permission system.', + label: ClideSettings.i18n.string(context, 'prompt.permission.allow', namespace: 'builtin.claude', placeholder: '1. Allow'), + variant: ClideButtonVariant.primary, + onPressed: () => _permAllow(), + ), + if (canRemember) + ClideButton( + label: ClideSettings.i18n.string( + context, + 'prompt.permission.allowRemember', + namespace: 'builtin.claude', + placeholder: "2. Allow & don't ask again", + ), + onPressed: () => _permAllow(remember: true), + ), + ClideButton( + label: ClideSettings.i18n.interpolated( + context, + 'prompt.permission.deny', + namespace: 'builtin.claude', + placeholder: '${canRemember ? '3' : '2'}. Deny', + replacers: [I18nReplacer(from: '{n}', replace: canRemember ? '3' : '2')], + ), + onPressed: _permDeny, + ), + ClideButton( + label: ClideSettings.i18n.interpolated( + context, + 'prompt.permission.denySimplify', + namespace: 'builtin.claude', + placeholder: '${canRemember ? '4' : '3'}. Deny & simplify', + replacers: [I18nReplacer(from: '{n}', replace: canRemember ? '4' : '3')], + ), + tooltip: ClideSettings.i18n.string( + context, + 'prompt.permission.denySimplify.tooltip', + namespace: 'builtin.claude', + placeholder: 'Deny and ask Claude to retry this action in a simpler format — complex interactions don\'t work well with the permission system.', + ), onPressed: _permDenySimplify, ), ], ), const SizedBox(height: 10), - _NoteField(controller: _note, placeholder: 'add a note (optional) — sent to Claude'), + _NoteField( + controller: _note, + placeholder: ClideSettings.i18n.string( + context, + 'prompt.permission.note.placeholder', + namespace: 'builtin.claude', + placeholder: 'add a note (optional) — sent to Claude', + ), + ), ], ); } /// Render the tool input in the shape that best fits the tool. Delegates to /// the shared top-level helpers (also used by ConversationView — T-168). - Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(tokens, p.toolName, p.input, mono); + Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(context, tokens, p.toolName, p.input, mono); // -- AskUserQuestion: single = bare, multi = stepper + review -------------- @@ -291,13 +338,17 @@ class _ToolPromptCardState extends State { if (_q.length <= 1) { return ( tokens.statusInfo, - 'question', + ClideSettings.i18n.string(context, 'prompt.question.label', namespace: 'builtin.claude', placeholder: 'question'), [ if (_q.isNotEmpty) _questionBody(tokens, 0), const SizedBox(height: 6), Row( children: [ - ClideButton(label: 'Submit', variant: ClideButtonVariant.primary, onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null), + ClideButton( + label: ClideSettings.i18n.string(context, 'prompt.submit', namespace: 'builtin.claude', placeholder: 'Submit'), + variant: ClideButtonVariant.primary, + onPressed: (_q.isNotEmpty && _answer(0).isNotEmpty) ? _submit : null, + ), const Spacer(), _chatInstead(tokens), ], @@ -308,17 +359,28 @@ class _ToolPromptCardState extends State { if (_step >= _q.length) { return ( tokens.statusInfo, - 'review', + ClideSettings.i18n.string(context, 'prompt.review.label', namespace: 'builtin.claude', placeholder: 'review'), [ - ClideText('Review your answers', color: tokens.globalForeground, fontSize: clideFontBody), + ClideText( + ClideSettings.i18n.string(context, 'prompt.review.title', namespace: 'builtin.claude', placeholder: 'Review your answers'), + color: tokens.globalForeground, + fontSize: clideFontBody, + ), const SizedBox(height: 8), for (var i = 0; i < _q.length; i++) _reviewRow(tokens, i), const SizedBox(height: 6), Row( children: [ - ClideButton(label: '‹ Back', onPressed: () => setState(() => _step = _q.length - 1)), + ClideButton( + label: ClideSettings.i18n.string(context, 'prompt.back', namespace: 'builtin.claude', placeholder: '‹ Back'), + onPressed: () => setState(() => _step = _q.length - 1), + ), const SizedBox(width: 8), - ClideButton(label: 'Submit answers', variant: ClideButtonVariant.primary, onPressed: _allAnswered ? _submit : null), + ClideButton( + label: ClideSettings.i18n.string(context, 'prompt.submitAnswers', namespace: 'builtin.claude', placeholder: 'Submit answers'), + variant: ClideButtonVariant.primary, + onPressed: _allAnswered ? _submit : null, + ), ], ), ], @@ -328,7 +390,7 @@ class _ToolPromptCardState extends State { final last = _step == _q.length - 1; return ( tokens.statusInfo, - 'question', + ClideSettings.i18n.string(context, 'prompt.question.label', namespace: 'builtin.claude', placeholder: 'question'), [ _navBar(tokens), const SizedBox(height: 10), @@ -336,8 +398,20 @@ class _ToolPromptCardState extends State { const SizedBox(height: 6), Row( children: [ - if (_step > 0) ...[ClideButton(label: '‹ Back', onPressed: () => setState(() => _step--)), const SizedBox(width: 8)], - ClideButton(label: last ? 'Review ›' : 'Next ›', variant: ClideButtonVariant.primary, onPressed: answered ? () => setState(() => _step++) : null), + if (_step > 0) ...[ + ClideButton( + label: ClideSettings.i18n.string(context, 'prompt.back', namespace: 'builtin.claude', placeholder: '‹ Back'), + onPressed: () => setState(() => _step--), + ), + const SizedBox(width: 8), + ], + ClideButton( + label: last + ? ClideSettings.i18n.string(context, 'prompt.reviewNav', namespace: 'builtin.claude', placeholder: 'Review ›') + : ClideSettings.i18n.string(context, 'prompt.next', namespace: 'builtin.claude', placeholder: 'Next ›'), + variant: ClideButtonVariant.primary, + onPressed: answered ? () => setState(() => _step++) : null, + ), const Spacer(), _chatInstead(tokens), ], @@ -375,7 +449,14 @@ class _ToolPromptCardState extends State { ); } } - chips.add(ClideText('Review', fontSize: clideFontMeta, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted)); + chips.add( + ClideText( + ClideSettings.i18n.string(context, 'prompt.nav.review', namespace: 'builtin.claude', placeholder: 'Review'), + fontSize: clideFontMeta, + fontFamily: ClideSettings.fonts.monoOf(context), + color: tokens.globalTextMuted, + ), + ); chips.add(ClideText('›', color: tokens.globalTextMuted, fontSize: 15)); return Wrap(spacing: 10, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: chips); } @@ -417,12 +498,28 @@ class _ToolPromptCardState extends State { children: [ for (var oi = 0; oi < q.options.length; oi++) _optButton(qi, q.options[oi].label, q.options[oi].label, q.multiSelect, q.options[oi].description, oi + 1), - _optButton(qi, _kOther, 'Other…', q.multiSelect, '', q.options.length + 1), + _optButton( + qi, + _kOther, + ClideSettings.i18n.string(context, 'prompt.option.other', namespace: 'builtin.claude', placeholder: 'Other…'), + q.multiSelect, + '', + q.options.length + 1, + ), ], ), - if (hasOther) ...[const SizedBox(height: 8), _NoteField(controller: _other[qi], placeholder: 'type your answer…')], + if (hasOther) ...[ + const SizedBox(height: 8), + _NoteField( + controller: _other[qi], + placeholder: ClideSettings.i18n.string(context, 'prompt.other.placeholder', namespace: 'builtin.claude', placeholder: 'type your answer…'), + ), + ], const SizedBox(height: 8), - _NoteField(controller: _qnote[qi], placeholder: '+ note (optional)'), + _NoteField( + controller: _qnote[qi], + placeholder: ClideSettings.i18n.string(context, 'prompt.note.placeholder', namespace: 'builtin.claude', placeholder: '+ note (optional)'), + ), ], ); } @@ -452,7 +549,7 @@ class _ToolPromptCardState extends State { Widget _chatInstead(SurfaceTokens tokens) { return ClideButton( - label: 'chat instead', + label: ClideSettings.i18n.string(context, 'prompt.chatInstead', namespace: 'builtin.claude', placeholder: 'chat instead'), variant: ClideButtonVariant.subtle, onPressed: () => widget.onResolve(widget.prompt.promptId, const DenyTool('User chose to chat instead.')), ); @@ -491,15 +588,15 @@ class _ToolPromptCardState extends State { /// /// Shared between [ToolPromptCard] (permission prompt body) and the /// [ConversationView] tool-use card bodies (T-168). -Widget toolInputBody(SurfaceTokens tokens, String toolName, Map input, String mono) { +Widget toolInputBody(BuildContext context, SurfaceTokens tokens, String toolName, Map input, String mono) { switch (toolName) { case 'Bash': - return toolBashBody(tokens, input); + return toolBashBody(context, tokens, input); case 'Write': return toolWriteBody(tokens, input, mono); case 'Edit': case 'MultiEdit': - return toolEditBody(tokens, input, mono); + return toolEditBody(context, tokens, input, mono); case 'Read': case 'Grep': case 'LS': @@ -511,9 +608,12 @@ Widget toolInputBody(SurfaceTokens tokens, String toolName, Map /// Bash tool body: the command as a shell code block, with optional background /// / timeout annotations. -Widget toolBashBody(SurfaceTokens tokens, Map input) { +Widget toolBashBody(BuildContext context, SurfaceTokens tokens, Map input) { final cmd = (input['command'] as String? ?? '').trimRight(); - final notes = [if (input['run_in_background'] == true) 'background', if (input['timeout'] is num) 'timeout ${input['timeout']}ms']; + final notes = [ + if (input['run_in_background'] == true) ClideSettings.i18n.string(context, 'tool.bash.background', namespace: 'builtin.claude', placeholder: 'background'), + if (input['timeout'] is num) 'timeout ${input['timeout']}ms', + ]; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, @@ -543,7 +643,7 @@ Widget toolWriteBody(SurfaceTokens tokens, Map input, String mo } /// Edit / MultiEdit tool body: before/after diff view. -Widget toolEditBody(SurfaceTokens tokens, Map input, String mono) { +Widget toolEditBody(BuildContext context, SurfaceTokens tokens, Map input, String mono) { final path = input['file_path'] as String? ?? ''; final oldStr = input['old_string'] as String? ?? ''; final newStr = input['new_string'] as String? ?? ''; @@ -553,11 +653,21 @@ Widget toolEditBody(SurfaceTokens tokens, Map input, String mon mainAxisSize: MainAxisSize.min, children: [ if (path.isNotEmpty) toolPathLine(tokens, path, mono), - ClideText('— before', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono), + ClideText( + ClideSettings.i18n.string(context, 'tool.edit.before', namespace: 'builtin.claude', placeholder: '— before'), + fontSize: clideFontMeta, + color: tokens.globalTextMuted, + fontFamily: mono, + ), const SizedBox(height: 4), ClideCodeBlock(source: oldStr, language: lang), const SizedBox(height: 8), - ClideText('+ after', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono), + ClideText( + ClideSettings.i18n.string(context, 'tool.edit.after', namespace: 'builtin.claude', placeholder: '+ after'), + fontSize: clideFontMeta, + color: tokens.globalTextMuted, + fontFamily: mono, + ), const SizedBox(height: 4), ClideCodeBlock(source: newStr, language: lang), ], diff --git a/lib/builtin/claude/src/running_indicator.dart b/lib/builtin/claude/src/running_indicator.dart index 69a14cbc..c905e882 100644 --- a/lib/builtin/claude/src/running_indicator.dart +++ b/lib/builtin/claude/src/running_indicator.dart @@ -87,7 +87,7 @@ class _RunningIndicatorState extends State with SingleTickerPr final reduced = MediaQuery.maybeOf(context)?.disableAnimations ?? false; // The animated text is decorative; AT gets one stable label. return Semantics( - label: 'Claude is running', + 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) diff --git a/lib/builtin/claude/src/session_picker.dart b/lib/builtin/claude/src/session_picker.dart index bb0572fc..525ef903 100644 --- a/lib/builtin/claude/src/session_picker.dart +++ b/lib/builtin/claude/src/session_picker.dart @@ -77,12 +77,20 @@ class _SessionPickerDialogState extends State { children: [ Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 8), - child: ClideText('Resume a Claude session', fontSize: clideFontBody, color: theme.globalForeground), + child: ClideText( + ClideSettings.i18n.string(context, 'sessionPicker.title', namespace: 'builtin.claude', placeholder: 'Resume a Claude session'), + fontSize: clideFontBody, + color: theme.globalForeground, + ), ), if (widget.sessions.isEmpty) Padding( padding: const EdgeInsets.fromLTRB(14, 4, 14, 16), - child: ClideText('No sessions found for this workspace.', muted: true, fontSize: clideFontSmall), + child: ClideText( + ClideSettings.i18n.string(context, 'sessionPicker.empty', namespace: 'builtin.claude', placeholder: 'No sessions found for this workspace.'), + muted: true, + fontSize: clideFontSmall, + ), ) else Flexible( diff --git a/lib/builtin/claude/src/team_chat_sidebar.dart b/lib/builtin/claude/src/team_chat_sidebar.dart index aa2addd5..3c292a67 100644 --- a/lib/builtin/claude/src/team_chat_sidebar.dart +++ b/lib/builtin/claude/src/team_chat_sidebar.dart @@ -132,15 +132,19 @@ class _TeamChatSidebarState extends State { Row( children: [ Expanded( - child: ClideText('MESSAGES', fontSize: clideFontSmall, color: tokens.globalTextMuted), + child: ClideText( + ClideSettings.i18n.string(context, 'teamChat.section.messages', namespace: 'builtin.claude', placeholder: 'MESSAGES'), + fontSize: clideFontSmall, + color: tokens.globalTextMuted, + ), ), Semantics( button: true, - label: 'Open full chat pane', + label: ClideSettings.i18n.string(context, 'teamChat.popOut.semantics', namespace: 'builtin.claude', placeholder: 'Open full chat pane'), excludeSemantics: true, onTap: widget.onPopOut, child: ClideTappable( - tooltip: 'Open full chat', + tooltip: ClideSettings.i18n.string(context, 'teamChat.popOut.tooltip', namespace: 'builtin.claude', placeholder: 'Open full chat'), onTap: widget.onPopOut, builder: (ctx, hovered, _) => Padding( padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1), @@ -155,7 +159,11 @@ class _TeamChatSidebarState extends State { if (messages.isEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), - child: ClideText('No messages yet.', muted: true, fontSize: clideFontSmall), + child: ClideText( + ClideSettings.i18n.string(context, 'teamChat.empty', namespace: 'builtin.claude', placeholder: 'No messages yet.'), + muted: true, + fontSize: clideFontSmall, + ), ) else for (final msg in messages.length > 5 ? messages.sublist(messages.length - 5) : messages) @@ -168,7 +176,13 @@ class _TeamChatSidebarState extends State { formatLabel: (n) => '@$n', child: Focus( onKeyEvent: _handleKeyEvent, - child: _ChatInputField(controller: _controller, focusNode: _focusNode, tokens: tokens, onSubmit: _submit, placeholder: '@name or @team …'), + child: _ChatInputField( + controller: _controller, + focusNode: _focusNode, + tokens: tokens, + onSubmit: _submit, + placeholder: ClideSettings.i18n.string(context, 'teamChat.composer.placeholder', namespace: 'builtin.claude', placeholder: '@name or @team …'), + ), ), ), ], @@ -303,12 +317,22 @@ class _TeamChatPaneState extends State { decoration: BoxDecoration( border: Border(bottom: BorderSide(color: tokens.panelBorder)), ), - child: ClideText('Team Chat', fontSize: clideFontSmall, color: tokens.globalTextMuted), + child: ClideText( + ClideSettings.i18n.string(context, 'teamChat.pane.title', namespace: 'builtin.claude', placeholder: 'Team Chat'), + fontSize: clideFontSmall, + color: tokens.globalTextMuted, + ), ), // Timeline. Expanded( child: messages.isEmpty - ? Center(child: ClideText('No messages yet.', muted: true, fontSize: clideFontSmall)) + ? Center( + child: ClideText( + ClideSettings.i18n.string(context, 'teamChat.empty', namespace: 'builtin.claude', placeholder: 'No messages yet.'), + muted: true, + fontSize: clideFontSmall, + ), + ) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -335,7 +359,12 @@ class _TeamChatPaneState extends State { children: [ Semantics( checked: _interrupt, - label: 'Interrupt target session', + label: ClideSettings.i18n.string( + context, + 'teamChat.interrupt.semantics', + namespace: 'builtin.claude', + placeholder: 'Interrupt target session', + ), excludeSemantics: true, onTap: () => setState(() => _interrupt = !_interrupt), child: Container( @@ -350,7 +379,11 @@ class _TeamChatPaneState extends State { child: _interrupt ? Center(child: ClideIcon(PhosphorIcons.byName('check'), size: 9, color: tokens.globalFocus)) : null, ), ), - ClideText('Interrupt', fontSize: clideFontSmall, color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted), + ClideText( + ClideSettings.i18n.string(context, 'teamChat.interrupt.label', namespace: 'builtin.claude', placeholder: 'Interrupt'), + fontSize: clideFontSmall, + color: _interrupt ? tokens.globalForeground : tokens.globalTextMuted, + ), ], ), ), @@ -363,7 +396,18 @@ class _TeamChatPaneState extends State { formatLabel: (n) => '@$n', child: Focus( onKeyEvent: _handleKeyEvent, - child: _ChatInputField(controller: _controller, focusNode: _focusNode, tokens: tokens, onSubmit: _submit, placeholder: '@name or @team …'), + child: _ChatInputField( + controller: _controller, + focusNode: _focusNode, + tokens: tokens, + onSubmit: _submit, + placeholder: ClideSettings.i18n.string( + context, + 'teamChat.composer.placeholder', + namespace: 'builtin.claude', + placeholder: '@name or @team …', + ), + ), ), ), ], diff --git a/lib/kernel/src/i18n/catalog/builtin.claude_en_us.json b/lib/kernel/src/i18n/catalog/builtin.claude_en_us.json index 8fe365b3..063be1cd 100644 --- a/lib/kernel/src/i18n/catalog/builtin.claude_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.claude_en_us.json @@ -3,5 +3,167 @@ "status.attaching": { "translation": "attaching…" }, "status.no-tmux": { "translation": "no-tmux · fresh every launch" }, "status.exited": { "translation": "session exited" }, - "status.primary-exited": { "translation": "session exited — restart clide to retry" } + "status.primary-exited": { "translation": "session exited — restart clide to retry" }, + "banner.title": { "translation": "Claude" }, + "banner.warmingUp": { "translation": "Warming up — your conversation will appear here." }, + "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.starting": { "translation": "starting…" }, + "pane.modeBadge.semantics": { "translation": "permission mode: {mode}" }, + "running.semantics": { "translation": "Claude is running" }, + "conversation.empty": { "translation": "Waiting for Claude…" }, + "conversation.label.you": { "translation": "you" }, + "conversation.label.claude": { "translation": "claude" }, + "conversation.label.clide": { "translation": "clide" }, + "conversation.label.agent": { "translation": "agent" }, + "conversation.label.agentPrompt": { "translation": "agent prompt" }, + "conversation.label.context": { "translation": "context" }, + "conversation.label.thinking": { "translation": "thinking" }, + "conversation.label.agentThinking": { "translation": "agent thinking" }, + "conversation.label.image": { "translation": "image" }, + "conversation.label.agentRun": { "translation": "agent run" }, + "conversation.label.workflow": { "translation": "workflow" }, + "conversation.label.error": { "translation": "error" }, + "conversation.label.result": { "translation": "result" }, + "conversation.label.denied": { "translation": "denied" }, + "conversation.segment.prompt": { "translation": "prompt" }, + "conversation.segment.result": { "translation": "result" }, + "conversation.segment.liveTail": { "translation": "live tail" }, + "conversation.segment.usage": { "translation": "usage" }, + "conversation.segment.script": { "translation": "script" }, + "conversation.workflow.launching": { "translation": "Launching…" }, + "conversation.imagePlaceholder": { "translation": "could not load {path}" }, + "conversation.bashTail.empty": { "translation": "no independent source to follow" }, + "conversation.bashTail.label": { "translation": "live tail" }, + "conversation.cluster.activity": { "translation": "Activity" }, + "conversation.cluster.edits": { "translation": "Edits" }, + "prompt.permission.allow": { "translation": "1. Allow" }, + "prompt.permission.allowRemember": { "translation": "2. Allow & don't ask again" }, + "prompt.permission.deny": { "translation": "{n}. Deny" }, + "prompt.permission.denySimplify": { "translation": "{n}. Deny & simplify" }, + "prompt.permission.denySimplify.tooltip": { "translation": "Deny and ask Claude to retry this action in a simpler format — complex interactions don't work well with the permission system." }, + "prompt.permission.note.placeholder": { "translation": "add a note (optional) — sent to Claude" }, + "prompt.permission.label": { "translation": "permission · {name}" }, + "prompt.question.label": { "translation": "question" }, + "prompt.review.label": { "translation": "review" }, + "prompt.review.title": { "translation": "Review your answers" }, + "prompt.submit": { "translation": "Submit" }, + "prompt.submitAnswers": { "translation": "Submit answers" }, + "prompt.back": { "translation": "‹ Back" }, + "prompt.next": { "translation": "Next ›" }, + "prompt.reviewNav": { "translation": "Review ›" }, + "prompt.nav.review": { "translation": "Review" }, + "prompt.option.other": { "translation": "Other…" }, + "prompt.other.placeholder": { "translation": "type your answer…" }, + "prompt.note.placeholder": { "translation": "+ note (optional)" }, + "prompt.chatInstead": { "translation": "chat instead" }, + "tool.edit.before": { "translation": "— before" }, + "tool.edit.after": { "translation": "+ after" }, + "tool.bash.background": { "translation": "background" }, + "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." }, + "permissionBadge.semantics": { "translation": "Permission mode: {label}" }, + "card.expand": { "translation": "Expand" }, + "card.collapse": { "translation": "Collapse" }, + "card.succeeded": { "translation": "succeeded" }, + "card.failed": { "translation": "failed" }, + "card.copy": { "translation": "copy" }, + "taskDock.summary": { "translation": "{count} {tasks} · {done} done" }, + "taskDock.task.singular": { "translation": "task" }, + "taskDock.task.plural": { "translation": "tasks" }, + "taskDock.collapse": { "translation": "Collapse tasks" }, + "taskDock.expand": { "translation": "Expand tasks" }, + "taskDock.semantics": { "translation": "Claude task list, {summary}, {state}" }, + "taskDock.state.expanded": { "translation": "expanded" }, + "taskDock.state.collapsed": { "translation": "collapsed" }, + "taskDock.status.done": { "translation": "done" }, + "taskDock.status.inProgress": { "translation": "in progress" }, + "taskDock.status.pending": { "translation": "pending" }, + "taskDock.row.semantics": { "translation": "{text}, {status}" }, + "sessionPicker.title": { "translation": "Resume a Claude session" }, + "sessionPicker.empty": { "translation": "No sessions found for this workspace." }, + "modelPicker.cancel": { "translation": "cancel" }, + "image.semantics": { "translation": "Image {name}" }, + "activity.section.session": { "translation": "SESSION" }, + "activity.control.clear": { "translation": "clear" }, + "activity.control.compact": { "translation": "compact" }, + "activity.control.fork": { "translation": "fork" }, + "activity.control.resume": { "translation": "resume" }, + "activity.control.refreshUsage": { "translation": "refresh usage" }, + "activity.control.semantics": { "translation": "{label} session" }, + "activity.control.tooltip": { "translation": "{label} · {command}" }, + "activity.empty": { "translation": "No activity recorded yet." }, + "activity.section.workflows": { "translation": "WORKFLOWS" }, + "activity.workflow.fallback": { "translation": "workflow" }, + "activity.workflow.done": { "translation": "done" }, + "activity.workflow.starting": { "translation": "starting" }, + "activity.section.usage": { "translation": "USAGE" }, + "activity.row.session": { "translation": "session" }, + "activity.row.weekAll": { "translation": "week (all)" }, + "activity.row.weekSonnet": { "translation": "week (sonnet)" }, + "activity.section.today": { "translation": "TODAY" }, + "activity.row.messages": { "translation": "messages" }, + "activity.row.sessions": { "translation": "sessions" }, + "activity.row.toolCalls": { "translation": "tool calls" }, + "activity.section.lifetime": { "translation": "LIFETIME" }, + "activity.section.runtime": { "translation": "RUNTIME · primary" }, + "activity.row.model": { "translation": "model" }, + "activity.row.effort": { "translation": "effort" }, + "activity.row.context": { "translation": "context" }, + "activity.row.mode": { "translation": "mode" }, + "activity.row.skills": { "translation": "skills" }, + "config.empty": { "translation": "Claude environment not loaded." }, + "config.section.settings": { "translation": "SETTINGS" }, + "config.row.model": { "translation": "model" }, + "config.row.effort": { "translation": "effort" }, + "config.row.permissionMode": { "translation": "permission mode" }, + "config.row.outputStyle": { "translation": "output style" }, + "config.row.source": { "translation": "source" }, + "config.row.source.value": { "translation": "~/.claude + .claude" }, + "config.footer": { "translation": "expand a list to see all · click a skill/agent/command → opens its .md" }, + "config.section.skills": { "translation": "SKILLS" }, + "config.section.agents": { "translation": "AGENTS" }, + "config.section.commands": { "translation": "COMMANDS" }, + "config.section.hooks": { "translation": "HOOKS" }, + "config.section.permissions": { "translation": "PERMISSIONS" }, + "config.section.mcpServers": { "translation": "MCP SERVERS" }, + "config.perm.allow": { "translation": "allow" }, + "config.perm.ask": { "translation": "ask" }, + "config.perm.deny": { "translation": "deny" }, + "config.control.semantics": { "translation": "{label}: {value}. Click to change." }, + "config.control.tooltip": { "translation": "change {label}" }, + "config.control.option.semantics": { "translation": "{label}: {name}" }, + "roster.bypass.confirmBody": { "translation": "Enable bypassPermissions? All tool calls will be auto-allowed." }, + "roster.bypass.confirm.semantics": { "translation": "Confirm bypass" }, + "roster.bypass.confirm.tooltip": { "translation": "Confirm" }, + "roster.bypass.ok": { "translation": "OK" }, + "roster.bypass.cancel.semantics": { "translation": "Cancel bypass" }, + "roster.bypass.cancel.tooltip": { "translation": "Cancel" }, + "roster.bypass.cancel": { "translation": "Cancel" }, + "roster.hidePane": { "translation": "Hide pane" }, + "roster.showPane": { "translation": "Show pane" }, + "roster.unmute": { "translation": "Unmute messages" }, + "roster.mute": { "translation": "Mute messages" }, + "roster.inject": { "translation": "Inject message" }, + "roster.fork": { "translation": "Fork session" }, + "roster.close": { "translation": "Close session" }, + "roster.inject.cancel": { "translation": "Cancel" }, + "taskRow.reassign": { "translation": "Reassign task" }, + "team.empty": { "translation": "No team active." }, + "team.section.tasks": { "translation": "TASKS" }, + "tabStrip.activity": { "translation": "Activity" }, + "tabStrip.team": { "translation": "Team" }, + "tabStrip.team.count": { "translation": "Team · {count}" }, + "tabStrip.config": { "translation": "Config" }, + "teamChat.section.messages": { "translation": "MESSAGES" }, + "teamChat.popOut.semantics": { "translation": "Open full chat pane" }, + "teamChat.popOut.tooltip": { "translation": "Open full chat" }, + "teamChat.empty": { "translation": "No messages yet." }, + "teamChat.composer.placeholder": { "translation": "@name or @team …" }, + "teamChat.pane.title": { "translation": "Team Chat" }, + "teamChat.interrupt.semantics": { "translation": "Interrupt target session" }, + "teamChat.interrupt.label": { "translation": "Interrupt" } }