refactor(i18n): route claude pane labels through the catalog (T-464)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 21:19:19 +02:00
co-authored by Claude Opus 4.8
parent 93aa9bc332
commit 16fe71cb3f
23 changed files with 870 additions and 193 deletions
@@ -49,21 +49,37 @@ class ActivityTabView extends StatelessWidget {
final latest = stats.latest;
final u = usage;
final sections = <MetaSection>[
..._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<MetaSection> _workflowSection(SurfaceTokens tokens) {
List<MetaSection> _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<MetaSection> _runtimeSection(SurfaceTokens tokens) {
List<MetaSection> _runtimeSection(BuildContext context, SurfaceTokens tokens) {
final st = primaryStatus;
final skills = config?.skills.length;
final rows = <MetaRow>[
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)];
}
}
@@ -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 <Widget>[];
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<Widget> _permissionRows(SurfaceTokens tokens, ClaudePermissions perms) {
List<Widget> _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<SettingControlRow> {
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)),
@@ -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) {
@@ -154,27 +154,40 @@ class _AgentRosterRowState extends State<AgentRosterRow> {
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<AgentRosterRow> {
// 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<AgentRosterRow> {
// 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<AgentRosterRow> {
// 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<AgentRosterRow> {
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),
),
@@ -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'),
};
}
@@ -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),
),
@@ -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 = <Widget>[
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),
],