fix(i18n): stop warning on missing tool.name.* keys for proper-name tools (T-493)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 14:58:12 +02:00
co-authored by Claude Opus 4.8
parent e5976ad6b1
commit 066197c5e1
7 changed files with 55 additions and 8 deletions
@@ -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;
+3
View File
@@ -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);
+4
View File
@@ -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
@@ -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.
+10 -3
View File
@@ -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;
}
+2 -2
View File
@@ -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).
+30
View File
@@ -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<Object> 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 = <Object>[];
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 = <Object>[];
expect(withCapture(warns).string('k', namespace: 'unregistered', placeholder: 'p', warnIfMissing: false), 'p');
expect(warns, isEmpty);
});
});
}