diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index ff6102a8..00414d55 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5941,3 +5941,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 ('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; 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', 'in_progress', 'done', NULL, '2026-06-18 19:19:25', '2026-06-18 19:19:25.269', '2026-06-18 19:19:25.269', NULL, 'f1660567ecfe0c7220c23244206bc4d2', 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 ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'status', 'in_progress', 'done', NULL, '2026-06-18 20:13:50', '2026-06-18 20:13:50.352', '2026-06-18 20:13:50.352', NULL, 'a885fb7b56c053669a8ef5f0e6032feb', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 0e772a11..e69581c9 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7343,3 +7343,8 @@ Hotspots from the audit: - lib/builtin/diff/src/diff_view.dart:105 ''No staged changes.''/''No unstaged changes.'', :153 ''Unstaged'', :163 ''Staged'' Acceptance: no hardcoded user-facing literals in lib/builtin/git/ or lib/builtin/diff/; keys added to builtin.git and builtin.diff catalogs. Depends on T-463 for the builtin.diff catalog file.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:30.322', '2026-06-18 19:19:25.268', NULL, 'c14bcdd5a816e86b78af9b5ac795b8a7', 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 ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate planning-pane labels — tickets, pql, decisions, problems (~21 strings)', 'The four pql/planning-surface extensions each have a tab.title-only catalog and hardcode the rest: tickets ~6, pql ~6, decisions ~6, problems ~3. Sweep all four, extend each catalog. + +For each: route hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(..., namespace: ''builtin.'', placeholder: ) and add keys. + +Acceptance: no hardcoded user-facing literals remain in lib/builtin/{tickets,pql,decisions,problems}/; keys present in each respective catalog.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:36.624', '2026-06-18 20:13:50.352', NULL, '919efbfb38b655d5157f73572aa20fad', 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/decisions/src/decision_detail_view.dart b/lib/builtin/decisions/src/decision_detail_view.dart index 1cb30ba2..ec7eb613 100644 --- a/lib/builtin/decisions/src/decision_detail_view.dart +++ b/lib/builtin/decisions/src/decision_detail_view.dart @@ -88,9 +88,21 @@ class _DecisionDetailViewState extends State { @override Widget build(BuildContext context) { - if (_loading) return const Center(child: ClideText('Loading…', muted: true)); + if (_loading) { + return Center( + child: ClideText(ClideSettings.i18n.string(context, 'detail.loading', namespace: 'builtin.decisions', placeholder: 'Loading…'), muted: true), + ); + } final d = _decision; - if (d == null) return const Padding(padding: EdgeInsets.all(12), child: ClideText('Select a decision to view details.', muted: true)); + if (d == null) { + return Padding( + padding: const EdgeInsets.all(12), + child: ClideText( + ClideSettings.i18n.string(context, 'detail.empty', namespace: 'builtin.decisions', placeholder: 'Select a decision to view details.'), + muted: true, + ), + ); + } final tokens = ClideSettings.theme.of(context).surface; final isDark = ClideSettings.theme.of(context).dark; @@ -171,7 +183,12 @@ class _DecisionDetailViewState extends State { if (body != null && body.isNotEmpty) ...[const SizedBox(height: 12), ClideMarkdown(body, onRecordTap: (id) => _navigateToRecord(context, id))], if (refs.isNotEmpty) ...[ const SizedBox(height: 16), - ClideText('CROSS-REFERENCES', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), + ClideText( + ClideSettings.i18n.string(context, 'detail.section.refs', namespace: 'builtin.decisions', placeholder: 'CROSS-REFERENCES'), + fontSize: clideFontSmall, + color: tokens.sidebarSectionHeader, + fontFamily: ClideSettings.fonts.monoOf(context), + ), const SizedBox(height: 6), for (final ref in refs) _RefCard(ref: ref, tokens: tokens), ], diff --git a/lib/builtin/decisions/src/decisions_view.dart b/lib/builtin/decisions/src/decisions_view.dart index b16936c5..55c4b8a3 100644 --- a/lib/builtin/decisions/src/decisions_view.dart +++ b/lib/builtin/decisions/src/decisions_view.dart @@ -74,7 +74,8 @@ class _DecisionsViewState extends State { if (!mounted) return; if (!resp.ok) { setState(() { - _error = resp.error?.message ?? 'failed to load decisions'; + _error = + resp.error?.message ?? ClideSettings.i18n.string(context, 'error.load', namespace: 'builtin.decisions', placeholder: 'failed to load decisions'); _loading = false; }); return; @@ -129,10 +130,20 @@ class _DecisionsViewState extends State { @override Widget build(BuildContext context) { final tokens = ClideSettings.theme.of(context).surface; - if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true)); + if (_loading) { + return Center( + child: ClideText(ClideSettings.i18n.string(context, 'loading', namespace: 'builtin.decisions', placeholder: 'Loading decisions...'), muted: true), + ); + } if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)); if (_decisions.isEmpty) { - return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true)); + return Padding( + padding: const EdgeInsets.all(12), + child: ClideText( + ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.decisions', placeholder: 'No decisions found.\nRun `pql decisions sync` to index.'), + muted: true, + ), + ); } final lf = _filter.toLowerCase(); @@ -161,13 +172,17 @@ class _DecisionsViewState extends State { Row( children: [ Expanded( - child: ClideFilterBox(address: 'decisions.panel', hint: 'Filter decisions…', onChanged: (v) => setState(() => _filter = v)), + child: ClideFilterBox( + address: 'decisions.panel', + hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.decisions', placeholder: 'Filter decisions…'), + onChanged: (v) => setState(() => _filter = v), + ), ), Padding( padding: const EdgeInsets.only(right: 8), child: ClideTappable( onTap: _refreshing ? null : _refresh, - tooltip: 'Refresh decisions', + tooltip: ClideSettings.i18n.string(context, 'refresh.tooltip', namespace: 'builtin.decisions', placeholder: 'Refresh decisions'), builder: (ctx, hovered, _) => ClideIcon(PhosphorIcons.byName('arrow-clockwise'), size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), ), @@ -182,7 +197,7 @@ class _DecisionsViewState extends State { children: [ if (confirmed.isNotEmpty) ClideAccordion( - label: 'CONFIRMED', + label: ClideSettings.i18n.string(context, 'section.confirmed', namespace: 'builtin.decisions', placeholder: 'CONFIRMED'), count: confirmed.length, leading: Container( width: 8, @@ -204,7 +219,7 @@ class _DecisionsViewState extends State { ), if (questions.isNotEmpty) ClideAccordion( - label: 'QUESTIONS', + label: ClideSettings.i18n.string(context, 'section.questions', namespace: 'builtin.decisions', placeholder: 'QUESTIONS'), count: questions.length, leading: Container( width: 8, @@ -226,7 +241,7 @@ class _DecisionsViewState extends State { ), if (rejected.isNotEmpty) ClideAccordion( - label: 'REJECTED', + label: ClideSettings.i18n.string(context, 'section.rejected', namespace: 'builtin.decisions', placeholder: 'REJECTED'), count: rejected.length, leading: Container( width: 8, @@ -322,7 +337,12 @@ class _DecisionCard extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: tokens.statusSuccess.withAlpha(0x30), borderRadius: BorderRadius.circular(3)), - child: ClideText('resolved', fontSize: clideFontBadge, color: tokens.statusSuccess, fontFamily: ClideSettings.fonts.monoOf(context)), + child: ClideText( + ClideSettings.i18n.string(context, 'badge.resolved', namespace: 'builtin.decisions', placeholder: 'resolved'), + fontSize: clideFontBadge, + color: tokens.statusSuccess, + fontFamily: ClideSettings.fonts.monoOf(context), + ), ), ], ], diff --git a/lib/builtin/decisions/src/extension.dart b/lib/builtin/decisions/src/extension.dart index 805929cd..4be3407c 100644 --- a/lib/builtin/decisions/src/extension.dart +++ b/lib/builtin/decisions/src/extension.dart @@ -50,6 +50,8 @@ class DecisionsExtension extends ClideExtension { id: 'decisions.detail', slot: Slots.contextPanel, title: 'Decision', + titleKey: 'tab.detail.title', + i18nNamespace: id, icon: PhosphorIcons.byName('lightbulb'), build: (_) => const DecisionDetailView(), ), diff --git a/lib/builtin/pql/src/backlinks_view.dart b/lib/builtin/pql/src/backlinks_view.dart index 938c3dc7..c564423e 100644 --- a/lib/builtin/pql/src/backlinks_view.dart +++ b/lib/builtin/pql/src/backlinks_view.dart @@ -42,10 +42,22 @@ class _BacklinksViewState extends State { builder: (context, _) { final tokens = ClideSettings.theme.of(context).surface; if (c.activePath == null) { - return const Padding(padding: EdgeInsets.all(12), child: ClideText('Open a file to see its links.', muted: true)); + return Padding( + padding: const EdgeInsets.all(12), + child: ClideText( + ClideSettings.i18n.string(context, 'backlinks.empty', namespace: 'builtin.pql', placeholder: 'Open a file to see its links.'), + muted: true, + ), + ); } return Semantics( - label: 'backlinks for ${c.activePath}', + label: ClideSettings.i18n.interpolated( + context, + 'backlinks.semantics', + namespace: 'builtin.pql', + placeholder: 'backlinks for {path}', + replacers: [I18nReplacer(from: '{path}', replace: c.activePath!)], + ), container: true, explicitChildNodes: true, child: SingleChildScrollView( @@ -63,9 +75,21 @@ class _BacklinksViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption), ), - if (c.loading) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)), - _LinkGroup(label: 'Backlinks', links: c.backlinks, pathKey: 'source'), - _LinkGroup(label: 'Outlinks', links: c.outlinks, pathKey: 'target'), + if (c.loading) + Padding( + padding: const EdgeInsets.all(12), + child: ClideText(ClideSettings.i18n.string(context, 'backlinks.loading', namespace: 'builtin.pql', placeholder: 'Loading…'), muted: true), + ), + _LinkGroup( + label: ClideSettings.i18n.string(context, 'group.backlinks', namespace: 'builtin.pql', placeholder: 'Backlinks'), + links: c.backlinks, + pathKey: 'source', + ), + _LinkGroup( + label: ClideSettings.i18n.string(context, 'group.outlinks', namespace: 'builtin.pql', placeholder: 'Outlinks'), + links: c.outlinks, + pathKey: 'target', + ), ], ), ), @@ -90,12 +114,29 @@ class _LinkGroup extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2), - child: ClideText('$label (${links.length})', fontSize: clideFontCaption, muted: true), + child: ClideText( + ClideSettings.i18n.interpolated( + context, + 'group.label', + namespace: 'builtin.pql', + placeholder: '{label} ({count})', + replacers: [ + I18nReplacer(from: '{label}', replace: label), + I18nReplacer(from: '{count}', replace: '${links.length}'), + ], + ), + fontSize: clideFontCaption, + muted: true, + ), ), if (links.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2), - child: ClideText('None', fontSize: clideFontCaption, muted: true), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), + child: ClideText( + ClideSettings.i18n.string(context, 'group.none', namespace: 'builtin.pql', placeholder: 'None'), + fontSize: clideFontCaption, + muted: true, + ), ), for (final link in links) _LinkRow(link: link, pathKey: pathKey), ], diff --git a/lib/builtin/pql/src/extension.dart b/lib/builtin/pql/src/extension.dart index dd98e354..69aec349 100644 --- a/lib/builtin/pql/src/extension.dart +++ b/lib/builtin/pql/src/extension.dart @@ -21,6 +21,8 @@ class PqlExtension extends ClideExtension { id: 'pql.backlinks', slot: Slots.contextPanel, title: 'Links', + titleKey: 'tab.links.title', + i18nNamespace: id, icon: PhosphorIcons.byName('link'), priority: -80, build: (_) => const BacklinksView(), diff --git a/lib/builtin/pql/src/pql_search_body.dart b/lib/builtin/pql/src/pql_search_body.dart index 940097ae..3a0ef699 100644 --- a/lib/builtin/pql/src/pql_search_body.dart +++ b/lib/builtin/pql/src/pql_search_body.dart @@ -104,7 +104,7 @@ class _PqlSearchBodyState extends State { if (widget.mode == PqlPaneMode.markdown) ClideFilterBox( address: 'search.pql.markdown', - hint: 'Filter markdown…', + hint: ClideSettings.i18n.string(context, 'filter.markdown.hint', namespace: 'builtin.pql', placeholder: 'Filter markdown…'), onChanged: (v) => unawaited(c.loadMarkdownFiles(glob: v.isEmpty ? null : '**/*$v*.md')), ) else @@ -118,9 +118,19 @@ class _PqlSearchBodyState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption, maxLines: 3), ), - if (c.loading && c.results.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)), + if (c.loading && c.results.isEmpty) + Padding( + padding: const EdgeInsets.all(12), + child: ClideText(ClideSettings.i18n.string(context, 'loading', namespace: 'builtin.pql', placeholder: 'Loading…'), muted: true), + ), if (!c.loading && c.results.isEmpty && c.error == null && widget.mode == PqlPaneMode.markdown) - const Padding(padding: EdgeInsets.all(12), child: ClideText('No markdown files found.', muted: true)), + Padding( + padding: const EdgeInsets.all(12), + child: ClideText( + ClideSettings.i18n.string(context, 'empty.markdown', namespace: 'builtin.pql', placeholder: 'No markdown files found.'), + muted: true, + ), + ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 4), @@ -160,7 +170,9 @@ class _PqlSearchInput extends StatelessWidget { Widget build(BuildContext context) { return ClideFilterBox( address: address, - hint: dsl ? 'PQL query…' : 'Search vault…', + hint: dsl + ? ClideSettings.i18n.string(context, 'search.query.hint', namespace: 'builtin.pql', placeholder: 'PQL query…') + : ClideSettings.i18n.string(context, 'search.vault.hint', namespace: 'builtin.pql', placeholder: 'Search vault…'), debounce: dsl ? Duration.zero : const Duration(milliseconds: 300), onChanged: dsl ? (_) {} : (v) => unawaited(controller.search(v)), onSubmitted: dsl ? (v) => unawaited(controller.runQuery(v)) : (v) => unawaited(controller.search(v)), diff --git a/lib/builtin/problems/src/problems_view.dart b/lib/builtin/problems/src/problems_view.dart index d01ec8d3..2190f83c 100644 --- a/lib/builtin/problems/src/problems_view.dart +++ b/lib/builtin/problems/src/problems_view.dart @@ -44,7 +44,7 @@ class _ProblemsViewState extends State { builder: (context, _) { final tokens = ClideSettings.theme.of(context).surface; return Semantics( - label: 'problems panel', + label: ClideSettings.i18n.string(context, 'semantics.panel', namespace: 'builtin.problems', placeholder: 'problems panel'), container: true, explicitChildNodes: true, child: () { @@ -55,30 +55,59 @@ class _ProblemsViewState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ClideFilterBox(address: 'problems.panel', hint: 'Filter problems…', onChanged: (v) => setState(() => _filter = v)), + ClideFilterBox( + address: 'problems.panel', + hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.problems', placeholder: 'Filter problems…'), + onChanged: (v) => setState(() => _filter = v), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), child: Row( children: [ Expanded( - child: ClideText('Problems (${filtered.length})', fontSize: clideFontCaption, color: tokens.sidebarForeground), + child: ClideText( + ClideSettings.i18n.interpolated( + context, + 'count', + namespace: 'builtin.problems', + placeholder: 'Problems ({count})', + replacers: [I18nReplacer(from: '{count}', replace: '${filtered.length}')], + ), + fontSize: clideFontCaption, + color: tokens.sidebarForeground, + ), ), Semantics( button: true, - label: 'refresh problems', + label: ClideSettings.i18n.string(context, 'refresh.semantics', namespace: 'builtin.problems', placeholder: 'refresh problems'), child: GestureDetector( onTap: () => unawaited(c.refresh()), child: MouseRegion( cursor: SystemMouseCursors.click, - child: ClideText('Refresh', fontSize: clideFontCaption, color: tokens.sidebarForeground), + child: ClideText( + ClideSettings.i18n.string(context, 'refresh.label', namespace: 'builtin.problems', placeholder: 'Refresh'), + fontSize: clideFontCaption, + color: tokens.sidebarForeground, + ), ), ), ), ], ), ), - if (c.loading && c.problems.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Scanning…', muted: true)), - if (!c.loading && filtered.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('No problems found.', muted: true)), + if (c.loading && c.problems.isEmpty) + Padding( + padding: const EdgeInsets.all(12), + child: ClideText(ClideSettings.i18n.string(context, 'scanning', namespace: 'builtin.problems', placeholder: 'Scanning…'), muted: true), + ), + if (!c.loading && filtered.isEmpty) + Padding( + padding: const EdgeInsets.all(12), + child: ClideText( + ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.problems', placeholder: 'No problems found.'), + muted: true, + ), + ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 4), diff --git a/lib/builtin/tickets/src/extension.dart b/lib/builtin/tickets/src/extension.dart index 7f5a2793..5d308af9 100644 --- a/lib/builtin/tickets/src/extension.dart +++ b/lib/builtin/tickets/src/extension.dart @@ -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(), ), diff --git a/lib/builtin/tickets/src/ticket_detail_view.dart b/lib/builtin/tickets/src/ticket_detail_view.dart index a8443c3d..b496b027 100644 --- a/lib/builtin/tickets/src/ticket_detail_view.dart +++ b/lib/builtin/tickets/src/ticket_detail_view.dart @@ -62,9 +62,21 @@ class _TicketDetailViewState extends State { 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 { ], 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(), }; } diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index f1870c35..cf09cb1c 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -150,7 +150,7 @@ class _TicketsViewState extends State { 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 { @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 { ('done', 'DONE'), ('cancelled', 'CANCELLED'), ]; + String sectionLabel(String status, String fallback) => + ClideSettings.i18n.string(context, 'section.$status', namespace: 'builtin.tickets', placeholder: fallback); final byStatus = >{}; for (final t in filtered) { @@ -205,13 +219,17 @@ class _TicketsViewState extends State { 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 { 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 { 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 { /// 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 { diff --git a/lib/kernel/src/i18n/catalog/builtin.decisions_en_us.json b/lib/kernel/src/i18n/catalog/builtin.decisions_en_us.json index f86c67e9..72a568ee 100644 --- a/lib/kernel/src/i18n/catalog/builtin.decisions_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.decisions_en_us.json @@ -1,3 +1,16 @@ { - "tab.title": { "translation": "Decisions" } + "tab.title": { "translation": "Decisions" }, + "tab.detail.title": { "translation": "Decision" }, + "loading": { "translation": "Loading decisions..." }, + "error.load": { "translation": "failed to load decisions" }, + "empty": { "translation": "No decisions found.\nRun `pql decisions sync` to index." }, + "filter.hint": { "translation": "Filter decisions…" }, + "refresh.tooltip": { "translation": "Refresh decisions" }, + "section.confirmed": { "translation": "CONFIRMED" }, + "section.questions": { "translation": "QUESTIONS" }, + "section.rejected": { "translation": "REJECTED" }, + "badge.resolved": { "translation": "resolved" }, + "detail.loading": { "translation": "Loading…" }, + "detail.empty": { "translation": "Select a decision to view details." }, + "detail.section.refs": { "translation": "CROSS-REFERENCES" } } diff --git a/lib/kernel/src/i18n/catalog/builtin.pql_en_us.json b/lib/kernel/src/i18n/catalog/builtin.pql_en_us.json index 601439c9..54cf163c 100644 --- a/lib/kernel/src/i18n/catalog/builtin.pql_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.pql_en_us.json @@ -1,3 +1,16 @@ { - "tab.title": { "translation": "pql" } + "tab.title": { "translation": "pql" }, + "tab.links.title": { "translation": "Links" }, + "filter.markdown.hint": { "translation": "Filter markdown…" }, + "loading": { "translation": "Loading…" }, + "empty.markdown": { "translation": "No markdown files found." }, + "search.vault.hint": { "translation": "Search vault…" }, + "search.query.hint": { "translation": "PQL query…" }, + "backlinks.empty": { "translation": "Open a file to see its links." }, + "backlinks.loading": { "translation": "Loading…" }, + "backlinks.semantics": { "translation": "backlinks for {path}" }, + "group.backlinks": { "translation": "Backlinks" }, + "group.outlinks": { "translation": "Outlinks" }, + "group.label": { "translation": "{label} ({count})" }, + "group.none": { "translation": "None" } } diff --git a/lib/kernel/src/i18n/catalog/builtin.problems_en_us.json b/lib/kernel/src/i18n/catalog/builtin.problems_en_us.json index 63b8ca81..64284325 100644 --- a/lib/kernel/src/i18n/catalog/builtin.problems_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.problems_en_us.json @@ -1,3 +1,10 @@ { - "tab.title": { "translation": "Problems" } + "tab.title": { "translation": "Problems" }, + "semantics.panel": { "translation": "problems panel" }, + "filter.hint": { "translation": "Filter problems…" }, + "count": { "translation": "Problems ({count})" }, + "refresh.label": { "translation": "Refresh" }, + "refresh.semantics": { "translation": "refresh problems" }, + "scanning": { "translation": "Scanning…" }, + "empty": { "translation": "No problems found." } } diff --git a/lib/kernel/src/i18n/catalog/builtin.tickets_en_us.json b/lib/kernel/src/i18n/catalog/builtin.tickets_en_us.json index c90cf1ef..cfb36f6c 100644 --- a/lib/kernel/src/i18n/catalog/builtin.tickets_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.tickets_en_us.json @@ -1,3 +1,36 @@ { - "tab.title": { "translation": "Tickets" } + "tab.title": { "translation": "Tickets" }, + "tab.detail.title": { "translation": "Ticket" }, + "type.initiative": { "translation": "Initiative" }, + "type.epic": { "translation": "Epic" }, + "type.story": { "translation": "Story" }, + "type.task": { "translation": "Task" }, + "type.bug": { "translation": "Bug" }, + "loading": { "translation": "Loading tickets..." }, + "error.load": { "translation": "failed to load tickets" }, + "empty": { "translation": "No tickets.\nRun `pql ticket new` to create one." }, + "filter.hint": { "translation": "Filter tickets…" }, + "refresh.tooltip": { "translation": "Refresh tickets" }, + "section.in_progress": { "translation": "IN PROGRESS" }, + "section.review": { "translation": "REVIEW" }, + "section.ready": { "translation": "READY" }, + "section.backlog": { "translation": "BACKLOG" }, + "section.done": { "translation": "DONE" }, + "section.cancelled": { "translation": "CANCELLED" }, + "chip.label": { "translation": "{type} type filter" }, + "chip.tooltip": { "translation": "Click to toggle · double-click to isolate" }, + "badge.wip": { "translation": "WIP" }, + "badge.review": { "translation": "REVIEW" }, + "badge.cancelled": { "translation": "CANCELLED" }, + "pickUp.tooltip": { "translation": "Pick up — hand this ticket to the Claude pane" }, + "detail.loading": { "translation": "Loading…" }, + "detail.empty": { "translation": "Select a ticket to view details." }, + "detail.assigned": { "translation": "assigned: {name}" }, + "detail.section.parents": { "translation": "PARENT TREE" }, + "detail.section.decisions": { "translation": "REFERENCED DECISIONS" }, + "status.backlog": { "translation": "BACKLOG" }, + "status.ready": { "translation": "READY" }, + "status.in_progress": { "translation": "WIP" }, + "status.review": { "translation": "REVIEW" }, + "status.done": { "translation": "DONE" } }