From 066197c5e155b23b649958ecb5aa75768001425d Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 28 Jun 2026 14:58:12 +0200 Subject: [PATCH] fix(i18n): stop warning on missing tool.name.* keys for proper-name tools (T-493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool display names are open-ended (Bash, Grep, Glob, ScheduleWakeup, MCP tools, …) — they intentionally have no catalog key and fall back to the raw name, so a miss isn't a gap to fix. The conversation pane was logging an i18n "missing key" warning for each, cluttering the output dock. Adds a `warnIfMissing` flag to I18nService.string (default true, so fixed UI strings still warn on a real gap) threaded through the ClideSettings facade; _toolNameLabel passes false. Display is unchanged — the placeholder already rendered the raw name. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 2 ++ .pql/changelog/tickets/2026-06.sql | 3 ++ CHANGELOG.md | 4 +++ lib/builtin/claude/src/conversation_view.dart | 7 +++-- lib/kernel/src/i18n/i18n.dart | 13 ++++++-- lib/widgets/src/clide_settings.dart | 4 +-- test/kernel/src/i18n/i18n_test.dart | 30 +++++++++++++++++++ 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 6c1f0084..f1f71ad1 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -7663,3 +7663,5 @@ DECISION (user, 2026-06-28): MANUAL-ONLY. The update check is a ''Check for upda P0 release-channel prerequisite is now T-491 (CI builds + signed package artifacts on Releases). P1 (this About button) is independent of it — Releases already exist to check against; P2/P3 (download+verify+apply) depend on T-491.', NULL, '2026-06-28 10:08:32', '2026-06-28 10:08:32.993', '2026-06-28 10:08:32.993', NULL, '3e7016c2feb2e8a0c10360240a73cb3b', 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 ('06FGVDBD0F14FV5CDJQB063YSR', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 10:10:28', '2026-06-28 10:10:28.234', '2026-06-28 10:10:28.234', NULL, '149c9f07d16db564a134917d5f42d8f7', 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 ('06FGVDBD0F14FV5CDJQB063YSR', 'status', 'in_progress', 'done', NULL, '2026-06-28 10:17:18', '2026-06-28 10:17:18.750', '2026-06-28 10:17:18.750', NULL, '56fc691a9c2eb8dd703db036acee907a', 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 ('06FGVNCZS0JZP5Y7596GBV4RFC', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 12:52:04', '2026-06-28 12:52:04.335', '2026-06-28 12:52:04.335', NULL, 'd0ba6a1fd4e721c47dcd766896eaf4f7', 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 ('06FGVNCZS0JZP5Y7596GBV4RFC', 'status', 'in_progress', 'done', NULL, '2026-06-28 12:58:12', '2026-06-28 12:58:12.193', '2026-06-28 12:58:12.193', NULL, '3086f007c53ceb3c9683b752649c19ad', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index e4e754de..84a58446 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -9676,3 +9676,6 @@ P0 release-channel prerequisite is now T-491 (CI builds + signed package artifac 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 ('06FGVDBD0F14FV5CDJQB063YSR', 'task', '06FB0TNQM7CEKQCMZAV751402G', 'T-47 P1: manual ''Check for updates'' button in the About box', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-28 10:10:21.572', '2026-06-28 10:10:21.572', NULL, '80921aa917352204a904eb22e0799368', 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 ('06FGVDBD0F14FV5CDJQB063YSR', 'task', '06FB0TNQM7CEKQCMZAV751402G', 'T-47 P1: manual ''Check for updates'' button in the About box', NULL, 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-28 10:10:21.572', '2026-06-28 10:10:28.233', NULL, '0d70cf614e033dc45a3dc314641443fd', 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 ('06FGVDBD0F14FV5CDJQB063YSR', 'task', '06FB0TNQM7CEKQCMZAV751402G', 'T-47 P1: manual ''Check for updates'' button in the About box', NULL, 'done', 'medium', NULL, NULL, NULL, '2026-06-28 10:10:21.572', '2026-06-28 10:17:18.750', NULL, 'e0006696152740e3fffc98fd67bf7edb', 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 ('06FGVNCZS0JZP5Y7596GBV4RFC', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'i18n warns on missing tool.name.* keys for proper-name tools (Bash, ScheduleWakeup, MCP)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-28 10:45:31.720', '2026-06-28 10:45:31.720', NULL, '771379bafaa0fc6b2bb7c887428c1c45', 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 ('06FGVNCZS0JZP5Y7596GBV4RFC', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'i18n warns on missing tool.name.* keys for proper-name tools (Bash, ScheduleWakeup, MCP)', NULL, 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-28 10:45:31.720', '2026-06-28 12:52:04.335', NULL, '2682b803f14e42f09665aed575dd55a9', 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 ('06FGVNCZS0JZP5Y7596GBV4RFC', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'i18n warns on missing tool.name.* keys for proper-name tools (Bash, ScheduleWakeup, MCP)', NULL, 'done', 'medium', NULL, NULL, NULL, '2026-06-28 10:45:31.720', '2026-06-28 12:58:12.193', NULL, 'a57d3c5c309321a204cb81441198d7a6', 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/CHANGELOG.md b/CHANGELOG.md index 7b78be72..bfa3c791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- **No more i18n "missing key" log spam for tool names.** Proper-name tools + (Bash, ScheduleWakeup, MCP tools, …) intentionally have no catalog entry and + fall back to the raw name; the conversation pane no longer logs a warning for + each. (T-493) - **`clide` CLI honors `CLIDE_SOCK`.** The shell client now connects to the socket named by `CLIDE_SOCK` when set — an explicit target (e.g. a spawned agent pinning its parent instance) that beats workspace auto-discovery — and diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index e49e6b5c..1c7bbed4 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -1162,10 +1162,11 @@ class _ActivityCard extends StatelessWidget { } /// Localized display label for a tool name (T-462). File/web/task operations -/// have natural translations; command/proper-name tools (Bash, Grep, Glob, LS) -/// have no catalog key and fall back to the raw name via the placeholder. +/// have natural translations; command/proper-name tools (Bash, Grep, Glob, +/// ScheduleWakeup, MCP tools, …) have no catalog key by design and fall back to +/// the raw name — so `warnIfMissing: false` keeps a miss from logging (T-493). String _toolNameLabel(BuildContext context, String name) => - ClideSettings.i18n.string(context, 'tool.name.$name', namespace: 'builtin.claude', placeholder: name); + ClideSettings.i18n.string(context, 'tool.name.$name', namespace: 'builtin.claude', placeholder: name, warnIfMissing: false); /// Localized "N steps" counter for a collapser header (T-462). Singular and /// plural are distinct catalog keys; the English forms double as the fallback. diff --git a/lib/kernel/src/i18n/i18n.dart b/lib/kernel/src/i18n/i18n.dart index d189f3a2..794cb1d2 100644 --- a/lib/kernel/src/i18n/i18n.dart +++ b/lib/kernel/src/i18n/i18n.dart @@ -92,10 +92,15 @@ class I18n extends ChangeNotifier { /// Look up a key, walking the locale fallback chain. Returns the /// placeholder if nothing hits; returns the key itself when placeholder /// is null (developer fallback — keys are more useful than blanks). - String string(String key, {required String namespace, String? placeholder}) { + /// [warnIfMissing] false suppresses the missing-key warning for OPEN-ENDED + /// lookups where a miss is the normal case, not a bug — e.g. tool display + /// names (`tool.name.Bash`, `tool.name.ScheduleWakeup`, MCP tools), which are + /// proper-name identifiers that intentionally fall back to the raw name. Keep + /// the warning on for fixed UI strings, where a miss is a real translation gap. + String string(String key, {required String namespace, String? placeholder, bool warnIfMissing = true}) { final byLocale = _cache[namespace]; if (byLocale == null) { - _warnOnce('$namespace::MISSING_NAMESPACE::$key', 'i18n: namespace not registered: $namespace (key: $key)'); + if (warnIfMissing) _warnOnce('$namespace::MISSING_NAMESPACE::$key', 'i18n: namespace not registered: $namespace (key: $key)'); return placeholder ?? key; } @@ -108,7 +113,9 @@ class I18n extends ChangeNotifier { if (hit != null) return hit; } - _warnOnce('$namespace::${_current.languageCode}::$key', 'i18n: missing key "$key" in namespace "$namespace" (locale ${_current.toString()})'); + if (warnIfMissing) { + _warnOnce('$namespace::${_current.languageCode}::$key', 'i18n: missing key "$key" in namespace "$namespace" (locale ${_current.toString()})'); + } return placeholder ?? key; } diff --git a/lib/widgets/src/clide_settings.dart b/lib/widgets/src/clide_settings.dart index 09679b7a..eac9cf74 100644 --- a/lib/widgets/src/clide_settings.dart +++ b/lib/widgets/src/clide_settings.dart @@ -54,9 +54,9 @@ class _I18n { /// uniform widget-facing lookup (T-462). [placeholder] is the inline English /// fallback. With no kernel in scope (isolated primitive tests) it returns /// the placeholder, so a widget never needs one to render. - String string(BuildContext context, String key, {required String namespace, String? placeholder}) { + String string(BuildContext context, String key, {required String namespace, String? placeholder, bool warnIfMissing = true}) { final i = ClideKernel.maybeOf(context)?.i18n; - return i == null ? (placeholder ?? key) : i.string(key, namespace: namespace, placeholder: placeholder); + return i == null ? (placeholder ?? key) : i.string(key, namespace: namespace, placeholder: placeholder, warnIfMissing: warnIfMissing); } /// [string] with `replaceAll` interpolation per replacer (templated labels). diff --git a/test/kernel/src/i18n/i18n_test.dart b/test/kernel/src/i18n/i18n_test.dart index 8d75a24e..9f19bab7 100644 --- a/test/kernel/src/i18n/i18n_test.dart +++ b/test/kernel/src/i18n/i18n_test.dart @@ -217,4 +217,34 @@ void main() { expect(FallbackChain.filenameSuffix(const Locale('en')), 'en'); }); }); + + // T-493: open-ended lookups (tool display names like `tool.name.Bash`, + // `tool.name.ScheduleWakeup`, MCP tools) intentionally miss and fall back to + // the raw name, so they must not spam the warn log. + group('warnIfMissing (T-493)', () { + I18n withCapture(List warns) => I18n( + loader: InMemoryCatalogLoader(const {}), + log: Logger(minLevel: LogLevel.warn, sinks: [warns.add]), + defaultLocale: const Locale('en', 'US'), + ); + + test('a real gap warns by default, but warnIfMissing:false stays silent', () async { + final warns = []; + final i = withCapture(warns); + await i.ensureNamespaceLoaded('builtin.x'); + + expect(i.string('missing', namespace: 'builtin.x', placeholder: 'fb'), 'fb'); + expect(warns, isNotEmpty, reason: 'a fixed UI string gap should still warn'); + + warns.clear(); + expect(i.string('tool.name.Bash', namespace: 'builtin.x', placeholder: 'Bash', warnIfMissing: false), 'Bash'); + expect(warns, isEmpty, reason: 'open-ended tool-name lookups stay quiet'); + }); + + test('warnIfMissing:false also silences an unregistered namespace', () { + final warns = []; + expect(withCapture(warns).string('k', namespace: 'unregistered', placeholder: 'p', warnIfMissing: false), 'p'); + expect(warns, isEmpty); + }); + }); }