From 1c79c9c4dc0b265dfbfbcf5e6c30c4883bb2c13e Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 17 Jun 2026 21:37:51 +0200 Subject: [PATCH] refactor(i18n): route consumer reads through ClideSettings.i18n (T-474) Migrate widget i18n reads from ClideKernel.of(context).i18n to the unified ClideSettings.i18n.of(context) facade (D-101), so settings/theme/fonts/i18n share one widget-facing entry point. The facade delegates to the same I18n service, so behaviour is unchanged. Infra plumbing (KernelServices / ExtensionManager fields, the slot-host Listenable.merge) keeps its direct kernel reference. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 1 + lib/builtin/extensions_ui/src/extensions_notice.dart | 2 +- lib/builtin/settings_ui/src/settings_category_view.dart | 4 ++-- lib/builtin/settings_ui/src/settings_modal.dart | 8 ++++---- lib/builtin/theme_picker/src/appearance_control.dart | 2 +- lib/builtin/theme_picker/src/settings_view.dart | 3 +-- lib/src/shell/slot_host.dart | 2 +- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 643e2214..515a4c4b 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5927,3 +5927,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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'description', NULL, 'Per D-101. Migrate ClideTheme.of(context) consumers (hundreds of sites) to ClideSettings.theme.of(context). The facade already delegates to ClideTheme, so this is a mechanical, no-behavior-change sweep — do it in focused batches with analyze between, ideally via a scripted find/replace + import fixups.', NULL, '2026-06-17 19:03:21', '2026-06-17 19:03:21.095', '2026-06-17 19:03:21.095', NULL, '45e0fb561bb187552a423d098aeb1cb5', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'description', NULL, 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', NULL, '2026-06-17 19:03:22', '2026-06-17 19:03:22.235', '2026-06-17 19:03:22.235', NULL, '5d617ff30e22d3b4c57ca6325920d91a', 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 ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'status', 'backlog', 'done', NULL, '2026-06-17 19:35:22', '2026-06-17 19:35:22.690', '2026-06-17 19:35:22.690', NULL, '580448456994d6acecc88cfbd40a595f', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'status', 'backlog', 'done', NULL, '2026-06-17 19:37:39', '2026-06-17 19:37:39.813', '2026-06-17 19:37:39.813', NULL, 'd59d0df2b693a0616265dd5e39559dc0', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index b95a9b6d..28d8b25f 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7246,3 +7246,4 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat 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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate theme consumers (ClideTheme.of → ClideSettings.theme.of) per D-101', 'Per D-101. Migrate ClideTheme.of(context) consumers (hundreds of sites) to ClideSettings.theme.of(context). The facade already delegates to ClideTheme, so this is a mechanical, no-behavior-change sweep — do it in focused batches with analyze between, ideally via a scripted find/replace + import fixups.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:20.828', '2026-06-17 19:03:21.095', NULL, 'b9144ecc303e9223fe212b17ee79c9d8', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate i18n consumers (ClideKernel.i18n → ClideSettings.i18n.of) per D-101', 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:21.490', '2026-06-17 19:03:22.235', NULL, '16d6229039400bd826a77a08eca9573b', 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 ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate 11 context-less mono-font sites onto ClideSettings.fonts (thread family through helpers)', 'Per D-101. The 11 sites that need a BuildContext threaded from the nearest context-bearing caller (resolve ClideSettings.fonts.monoOf(context) and pass the family in): prompt_card.dart toolEditBody/toolReadLikeBody/toolPathLine (top-level fns); conversation_view.dart _toolResult; welcome_view.dart _tipRow; search_panel_view.dart _base getter; clide_markdown.dart _inlineElementSpan/_recordLinkSpan/_fileLinkSpan (static span chain). Until done, these render the default JetBrains Mono regardless of the mono-font setting.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 18:48:11.649', '2026-06-17 19:35:22.690', NULL, '7dd6dae4197fc95d0bc6becfad4eecbb', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate i18n consumers (ClideKernel.i18n → ClideSettings.i18n.of) per D-101', 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:21.490', '2026-06-17 19:37:39.813', NULL, 'fc64918991f0c9133419ee51227e1939', 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/extensions_ui/src/extensions_notice.dart b/lib/builtin/extensions_ui/src/extensions_notice.dart index 9dfae84d..951d05e9 100644 --- a/lib/builtin/extensions_ui/src/extensions_notice.dart +++ b/lib/builtin/extensions_ui/src/extensions_notice.dart @@ -18,7 +18,7 @@ class ExtensionsNotice extends StatelessWidget { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/builtin/settings_ui/src/settings_category_view.dart b/lib/builtin/settings_ui/src/settings_category_view.dart index 589ae03e..c1026070 100644 --- a/lib/builtin/settings_ui/src/settings_category_view.dart +++ b/lib/builtin/settings_ui/src/settings_category_view.dart @@ -69,7 +69,7 @@ class SettingsSearchResults extends StatelessWidget { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); final store = ClideKernel.of(context).settings; final registry = ClideKernel.of(context).settingsRegistry; return ListenableBuilder( @@ -501,7 +501,7 @@ class _ScopeTagState extends State<_ScopeTag> { super.dispose(); } - String _ns(String key, String fallback) => ClideKernel.of(context).i18n.string(key, namespace: _settingsNs, placeholder: fallback); + String _ns(String key, String fallback) => ClideSettings.i18n.of(context).string(key, namespace: _settingsNs, placeholder: fallback); ({String glyph, Color color, String tip, String label}) _appearance(SettingsScope? layer) { final tokens = ClideTheme.of(context).surface; diff --git a/lib/builtin/settings_ui/src/settings_modal.dart b/lib/builtin/settings_ui/src/settings_modal.dart index 9f50c315..3261e2fe 100644 --- a/lib/builtin/settings_ui/src/settings_modal.dart +++ b/lib/builtin/settings_ui/src/settings_modal.dart @@ -49,7 +49,7 @@ class _SettingsModalState extends State { @override Widget build(BuildContext context) { - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); final tokens = ClideTheme.of(context).surface; final title = i.string('modal.title', namespace: SettingsModal.ns, placeholder: 'Settings'); @@ -140,7 +140,7 @@ class _CloseButton extends StatelessWidget { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); final label = i.string('modal.close', namespace: SettingsModal.ns, placeholder: 'Close'); final hint = i.string('modal.close.hint', namespace: SettingsModal.ns, placeholder: 'Close settings'); return Semantics( @@ -178,7 +178,7 @@ class _CategoryRail extends StatelessWidget { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); final registry = ClideKernel.of(context).settingsRegistry; final searching = query.trim().isNotEmpty; return ListenableBuilder( @@ -312,7 +312,7 @@ class _EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); return Center( child: Padding( padding: const EdgeInsets.all(24), diff --git a/lib/builtin/theme_picker/src/appearance_control.dart b/lib/builtin/theme_picker/src/appearance_control.dart index a3b1b8f5..867a5457 100644 --- a/lib/builtin/theme_picker/src/appearance_control.dart +++ b/lib/builtin/theme_picker/src/appearance_control.dart @@ -19,7 +19,7 @@ class AppearanceThemeControl extends StatelessWidget { @override Widget build(BuildContext context) { final controller = ClideKernel.of(context).theme; - final i = ClideKernel.of(context).i18n; + final i = ClideSettings.i18n.of(context); return ListenableBuilder( listenable: controller, builder: (context, _) { diff --git a/lib/builtin/theme_picker/src/settings_view.dart b/lib/builtin/theme_picker/src/settings_view.dart index 0cfc738d..190d0f84 100644 --- a/lib/builtin/theme_picker/src/settings_view.dart +++ b/lib/builtin/theme_picker/src/settings_view.dart @@ -45,9 +45,8 @@ class _SettingsViewState extends State { @override Widget build(BuildContext context) { - final kernel = ClideKernel.of(context); final tokens = ClideTheme.of(context).surface; - final i = kernel.i18n; + final i = ClideSettings.i18n.of(context); final themes = baseThemes(widget.controller.available); final currentBase = baseThemeName(widget.controller.currentName); final title = i.string('modal.title', namespace: SettingsView.ns, placeholder: 'Settings'); diff --git a/lib/src/shell/slot_host.dart b/lib/src/shell/slot_host.dart index 80d90e38..c07e0f37 100644 --- a/lib/src/shell/slot_host.dart +++ b/lib/src/shell/slot_host.dart @@ -368,5 +368,5 @@ String resolveTabTitle(BuildContext context, TabContribution t) { final key = t.titleKey; final ns = t.i18nNamespace; if (key == null || ns == null) return t.title; - return ClideKernel.of(context).i18n.string(key, namespace: ns, placeholder: t.title); + return ClideSettings.i18n.of(context).string(key, namespace: ns, placeholder: t.title); }