refactor(i18n): route planning-pane labels through the catalog (T-467)
Migrate the user-facing strings in builtin.tickets, builtin.pql, builtin.decisions, and builtin.problems (list/detail views, section headers, filters, empty/loading/error states, type chips) to ClideSettings.i18n; extend each extension's catalog. The detail/links tabs that had a bare title now wire titleKey + i18nNamespace so the tab title resolves too. Context threaded into the static label helpers. No en_US behaviour change (D-21). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,8 @@ class TicketsExtension extends ClideExtension {
|
||||
id: 'tickets.detail',
|
||||
slot: Slots.contextPanel,
|
||||
title: 'Ticket',
|
||||
titleKey: 'tab.detail.title',
|
||||
i18nNamespace: id,
|
||||
icon: PhosphorIcons.byName('ticket'),
|
||||
build: (_) => const TicketDetailView(),
|
||||
),
|
||||
|
||||
@@ -62,9 +62,21 @@ class _TicketDetailViewState extends State<TicketDetailView> {
|
||||
return ListenableBuilder(
|
||||
listenable: c,
|
||||
builder: (ctx, _) {
|
||||
if (c.loading) return const Center(child: ClideText('Loading…', muted: true));
|
||||
if (c.loading) {
|
||||
return Center(
|
||||
child: ClideText(ClideSettings.i18n.string(ctx, 'detail.loading', namespace: 'builtin.tickets', placeholder: 'Loading…'), muted: true),
|
||||
);
|
||||
}
|
||||
final d = c.detail;
|
||||
if (d == null) return const Padding(padding: EdgeInsets.all(12), child: ClideText('Select a ticket to view details.', muted: true));
|
||||
if (d == null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(ctx, 'detail.empty', namespace: 'builtin.tickets', placeholder: 'Select a ticket to view details.'),
|
||||
muted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final tokens = ClideSettings.theme.of(ctx).surface;
|
||||
final isDark = ClideSettings.theme.of(ctx).dark;
|
||||
@@ -99,13 +111,19 @@ class _TicketDetailViewState extends State<TicketDetailView> {
|
||||
],
|
||||
if (d.parents.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionLabel(label: 'PARENT TREE', tokens: tokens),
|
||||
_SectionLabel(
|
||||
label: ClideSettings.i18n.string(ctx, 'detail.section.parents', namespace: 'builtin.tickets', placeholder: 'PARENT TREE'),
|
||||
tokens: tokens,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
for (var i = 0; i < d.parents.length; i++) _CompactCard(data: d.parents[i], tokens: tokens, typeColors: typeColors, indent: i),
|
||||
],
|
||||
if (d.decisions.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionLabel(label: 'REFERENCED DECISIONS', tokens: tokens),
|
||||
_SectionLabel(
|
||||
label: ClideSettings.i18n.string(ctx, 'detail.section.decisions', namespace: 'builtin.tickets', placeholder: 'REFERENCED DECISIONS'),
|
||||
tokens: tokens,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
for (final dec in d.decisions) _DecisionRefCard(data: dec, tokens: tokens),
|
||||
],
|
||||
@@ -158,7 +176,18 @@ class _TicketHeader extends StatelessWidget {
|
||||
ClideText(detail.title, fontSize: 15, fontWeight: FontWeight.w500),
|
||||
if (detail.assignedTo != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
ClideText('assigned: ${detail.assignedTo}', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
ClideText(
|
||||
ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'detail.assigned',
|
||||
namespace: 'builtin.tickets',
|
||||
placeholder: 'assigned: {name}',
|
||||
replacers: [I18nReplacer(from: '{name}', replace: detail.assignedTo!)],
|
||||
),
|
||||
muted: true,
|
||||
fontSize: clideFontSmall,
|
||||
fontFamily: ClideSettings.fonts.monoOf(context),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -207,7 +236,7 @@ class _StatusControls extends StatelessWidget {
|
||||
border: Border.all(color: active ? tokens.statusInfo : tokens.panelBorder),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: ClideText(_shortLabel(s), fontSize: clideFontBadge, color: color, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
child: ClideText(_shortLabel(ctx, s), fontSize: clideFontBadge, color: color, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -218,12 +247,12 @@ class _StatusControls extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static String _shortLabel(String s) => switch (s) {
|
||||
'backlog' => 'BACKLOG',
|
||||
'ready' => 'READY',
|
||||
'in_progress' => 'WIP',
|
||||
'review' => 'REVIEW',
|
||||
'done' => 'DONE',
|
||||
static String _shortLabel(BuildContext context, String s) => switch (s) {
|
||||
'backlog' => ClideSettings.i18n.string(context, 'status.backlog', namespace: 'builtin.tickets', placeholder: 'BACKLOG'),
|
||||
'ready' => ClideSettings.i18n.string(context, 'status.ready', namespace: 'builtin.tickets', placeholder: 'READY'),
|
||||
'in_progress' => ClideSettings.i18n.string(context, 'status.in_progress', namespace: 'builtin.tickets', placeholder: 'WIP'),
|
||||
'review' => ClideSettings.i18n.string(context, 'status.review', namespace: 'builtin.tickets', placeholder: 'REVIEW'),
|
||||
'done' => ClideSettings.i18n.string(context, 'status.done', namespace: 'builtin.tickets', placeholder: 'DONE'),
|
||||
_ => s.toUpperCase(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
if (!mounted) return;
|
||||
if (!resp.ok) {
|
||||
setState(() {
|
||||
_error = resp.error?.message ?? 'failed to load tickets';
|
||||
_error = resp.error?.message ?? ClideSettings.i18n.string(context, 'error.load', namespace: 'builtin.tickets', placeholder: 'failed to load tickets');
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
@@ -169,9 +169,21 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
if (_loading) return const Center(child: ClideText('Loading tickets...', muted: true));
|
||||
if (_loading) {
|
||||
return Center(
|
||||
child: ClideText(ClideSettings.i18n.string(context, 'loading', namespace: 'builtin.tickets', placeholder: 'Loading tickets...'), muted: true),
|
||||
);
|
||||
}
|
||||
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
||||
if (_tickets.isEmpty) return const Padding(padding: EdgeInsets.all(12), child: ClideText('No tickets.\nRun `pql ticket new` to create one.', muted: true));
|
||||
if (_tickets.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.tickets', placeholder: 'No tickets.\nRun `pql ticket new` to create one.'),
|
||||
muted: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final lf = _filter.toLowerCase();
|
||||
final hasTextFilter = lf.isNotEmpty;
|
||||
@@ -191,6 +203,8 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
('done', 'DONE'),
|
||||
('cancelled', 'CANCELLED'),
|
||||
];
|
||||
String sectionLabel(String status, String fallback) =>
|
||||
ClideSettings.i18n.string(context, 'section.$status', namespace: 'builtin.tickets', placeholder: fallback);
|
||||
|
||||
final byStatus = <String, List<_TicketEntry>>{};
|
||||
for (final t in filtered) {
|
||||
@@ -205,13 +219,17 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideFilterBox(address: 'tickets.panel', hint: 'Filter tickets…', onChanged: (v) => setState(() => _filter = v)),
|
||||
child: ClideFilterBox(
|
||||
address: 'tickets.panel',
|
||||
hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.tickets', placeholder: 'Filter tickets…'),
|
||||
onChanged: (v) => setState(() => _filter = v),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ClideTappable(
|
||||
onTap: _refreshing ? null : _refresh,
|
||||
tooltip: 'Refresh tickets',
|
||||
tooltip: ClideSettings.i18n.string(context, 'refresh.tooltip', namespace: 'builtin.tickets', placeholder: 'Refresh tickets'),
|
||||
builder: (ctx, hovered, _) =>
|
||||
ClideIcon(PhosphorIcons.byName('arrow-clockwise'), size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
@@ -227,7 +245,25 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
children: [
|
||||
for (final (type, label) in _typeOrder)
|
||||
_TypeChip(
|
||||
label: label,
|
||||
label: ClideSettings.i18n.string(context, 'type.$type', namespace: 'builtin.tickets', placeholder: label),
|
||||
semanticLabel: ClideSettings.i18n.interpolated(
|
||||
context,
|
||||
'chip.label',
|
||||
namespace: 'builtin.tickets',
|
||||
placeholder: '{type} type filter',
|
||||
replacers: [
|
||||
I18nReplacer(
|
||||
from: '{type}',
|
||||
replace: ClideSettings.i18n.string(context, 'type.$type', namespace: 'builtin.tickets', placeholder: label),
|
||||
),
|
||||
],
|
||||
),
|
||||
tooltip: ClideSettings.i18n.string(
|
||||
context,
|
||||
'chip.tooltip',
|
||||
namespace: 'builtin.tickets',
|
||||
placeholder: 'Click to toggle · double-click to isolate',
|
||||
),
|
||||
color: typeColors.forType(type),
|
||||
active: _enabledTypes.contains(type),
|
||||
onToggle: () => _toggleType(type),
|
||||
@@ -246,7 +282,7 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
for (final (status, label) in sections)
|
||||
if (byStatus[status] case final items? when items.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: label,
|
||||
label: sectionLabel(status, label),
|
||||
count: items.length,
|
||||
expanded: filtering || _isSectionExpanded(status),
|
||||
onToggle: () => _toggle(status),
|
||||
@@ -275,9 +311,20 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
/// type; double-click isolates it (chart-legend solo). One [GestureDetector]
|
||||
/// owns both so Flutter disambiguates single vs double.
|
||||
class _TypeChip extends StatelessWidget {
|
||||
const _TypeChip({required this.label, required this.color, required this.active, required this.onToggle, required this.onSolo, required this.tokens});
|
||||
const _TypeChip({
|
||||
required this.label,
|
||||
required this.semanticLabel,
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
required this.active,
|
||||
required this.onToggle,
|
||||
required this.onSolo,
|
||||
required this.tokens,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String semanticLabel;
|
||||
final String tooltip;
|
||||
final Color color;
|
||||
final bool active;
|
||||
final VoidCallback onToggle;
|
||||
@@ -290,9 +337,9 @@ class _TypeChip extends StatelessWidget {
|
||||
return Semantics(
|
||||
button: true,
|
||||
toggled: active,
|
||||
label: '$label type filter',
|
||||
label: semanticLabel,
|
||||
child: ClideTooltip(
|
||||
message: 'Click to toggle · double-click to isolate',
|
||||
message: tooltip,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
@@ -360,7 +407,7 @@ class _TicketCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final typeColor = typeColors.forType(entry.type);
|
||||
final statusLabel = _statusLabel(entry.status);
|
||||
final statusLabel = _statusLabel(context, entry.status);
|
||||
|
||||
return Padding(
|
||||
key: focusKey,
|
||||
@@ -436,10 +483,10 @@ class _TicketCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static String? _statusLabel(String? status) => switch (status) {
|
||||
'in_progress' => 'WIP',
|
||||
'review' => 'REVIEW',
|
||||
'cancelled' => 'CANCELLED',
|
||||
static String? _statusLabel(BuildContext context, String? status) => switch (status) {
|
||||
'in_progress' => ClideSettings.i18n.string(context, 'badge.wip', namespace: 'builtin.tickets', placeholder: 'WIP'),
|
||||
'review' => ClideSettings.i18n.string(context, 'badge.review', namespace: 'builtin.tickets', placeholder: 'REVIEW'),
|
||||
'cancelled' => ClideSettings.i18n.string(context, 'badge.cancelled', namespace: 'builtin.tickets', placeholder: 'CANCELLED'),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -455,7 +502,7 @@ class _PickUpAction extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClideTappable(
|
||||
tooltip: 'Pick up — hand this ticket to the Claude pane',
|
||||
tooltip: ClideSettings.i18n.string(context, 'pickUp.tooltip', namespace: 'builtin.tickets', placeholder: 'Pick up — hand this ticket to the Claude pane'),
|
||||
onTap: () {
|
||||
final kernel = ClideKernel.of(context);
|
||||
unawaited(() async {
|
||||
|
||||
Reference in New Issue
Block a user