diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index c05c493b..4794795a 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5934,3 +5934,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 ('06FDA0TPYSWEM10RP0Q76XAP58', 'status', 'ready', 'in_progress', NULL, '2026-06-18 07:46:07', '2026-06-18 07:46:07.612', '2026-06-18 07:46:07.612', NULL, 'e8e84ffd0634306f2dc612d1d03152b4', 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 ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'status', 'backlog', 'in_progress', NULL, '2026-06-18 07:46:07', '2026-06-18 07:46:07.636', '2026-06-18 07:46:07.636', NULL, 'e9e9c6719b8a49a39309bf8268eeee55', 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 ('06FDA0VPB99NGS4J6Z5B2RJM2M', 'status', 'in_progress', 'done', NULL, '2026-06-18 07:56:10', '2026-06-18 07:56:10.133', '2026-06-18 07:56:10.133', NULL, '666602e799384337b8631789be144e53', 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 ('06FDA0X5WFME83GS0DKYG3WVJ8', 'status', 'backlog', 'done', NULL, '2026-06-18 13:41:29', '2026-06-18 13:41:29.322', '2026-06-18 13:41:29.322', NULL, 'debbcd81ff12cb112671ba428f1a826b', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 6172774c..9e645377 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7296,3 +7296,8 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat 2) builtin.diff contributes a TabContribution with titleKey ''tab.title'' + i18nNamespace ''builtin.diff'' (lib/builtin/diff/src/extension.dart) but ships NO catalog file. There is no lib/kernel/src/i18n/catalog/builtin.diff_en_us.json. Create it with tab.title (= ''Diff'', matching the placeholder). Acceptance: both theme-picker keys resolve from the catalog (not the placeholder); a builtin.diff catalog exists with tab.title; no ''missing key'' i18n warnings on boot for these namespaces.', 'done', 'high', NULL, NULL, 'D-21', '2026-06-17 09:57:14.458', '2026-06-18 07:56:10.133', NULL, '2d182b7c0f40848dc5a59771b73e4da1', 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 ('06FDA0X5WFME83GS0DKYG3WVJ8', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate builtin.welcome UI labels to the catalog (~15 strings)', 'builtin.welcome ships a full 10-key catalog (title, subtitle, open-project, open-project.hint, etc.) that is entirely orphaned — the view hardcodes ~15 strings instead. Wire the existing catalog up and add any missing keys. + +Hotspots from the audit: lib/builtin/welcome/src/welcome_view.dart — e.g. :108 ''TIPS'', :173 ''START'', :258 ''RECENT'', plus the title/subtitle/open-project block the catalog already has translations for. + +Acceptance: no hardcoded user-facing string literals remain in lib/builtin/welcome/; the pre-existing builtin.welcome catalog keys are actually consumed (no orphans); section headers added as keys.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:26.627', '2026-06-18 13:41:29.322', NULL, '9ce7dc6dd92151c29617d26088c3c85c', 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/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index 32065c80..ae84bacf 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -77,13 +77,15 @@ class _TipsCard extends StatelessWidget { // Every tip mirrors a binding that actually exists in the default // preset / contributed commands (T-383) — ctrl-based on the shipped // default keymap, hence ⌃ glyphs. If a binding moves, move the tip. - static const _tips = <(String, String)>[ - ('Quick open', '⌃P'), - ('Command palette', '⌃⇧P'), - ('Toggle sidebar', '⌃⇧1'), - ('Toggle context', '⌃⇧3'), - ('Find in files', '⌃⇧F'), - ('Focus mode', '⌃.'), + // (catalog key, English label, shortcut glyph). The key/English pair resolves + // through the i18n catalog at render (D-21); the glyph is not translated. + static const _tips = <(String, String, String)>[ + ('tips.quickOpen', 'Quick open', '⌃P'), + ('tips.commandPalette', 'Command palette', '⌃⇧P'), + ('tips.toggleSidebar', 'Toggle sidebar', '⌃⇧1'), + ('tips.toggleContext', 'Toggle context', '⌃⇧3'), + ('tips.findInFiles', 'Find in files', '⌃⇧F'), + ('tips.focusMode', 'Focus mode', '⌃.'), ]; @override @@ -105,18 +107,23 @@ class _TipsCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('TIPS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), + ClideText( + ClideSettings.i18n.string(context, 'section.tips', namespace: 'builtin.welcome', placeholder: 'TIPS'), + fontSize: clideFontSmall, + color: tokens.sidebarSectionHeader, + fontFamily: ClideSettings.fonts.monoOf(context), + ), const SizedBox(height: 14), - _tipRow(firstRow, ClideSettings.fonts.monoOf(context)), + _tipRow(context, firstRow, ClideSettings.fonts.monoOf(context)), const SizedBox(height: 8), - _tipRow(secondRow, ClideSettings.fonts.monoOf(context)), + _tipRow(context, secondRow, ClideSettings.fonts.monoOf(context)), ], ), ), ); } - Widget _tipRow(List<(String, String)> tips, String mono) { + Widget _tipRow(BuildContext context, List<(String, String, String)> tips, String mono) { return Row( children: [ for (var i = 0; i < tips.length; i++) ...[ @@ -124,9 +131,13 @@ class _TipsCard extends StatelessWidget { child: Row( children: [ Expanded( - child: ClideText(tips[i].$1, fontSize: clideFontMeta, color: tokens.globalTextMuted), + child: ClideText( + ClideSettings.i18n.string(context, tips[i].$1, namespace: 'builtin.welcome', placeholder: tips[i].$2), + fontSize: clideFontMeta, + color: tokens.globalTextMuted, + ), ), - ClideText(tips[i].$2, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: mono), + ClideText(tips[i].$3, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: mono), ], ), ), @@ -170,12 +181,23 @@ class _StartColumn extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('START', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), + ClideText( + ClideSettings.i18n.string(context, 'section.start', namespace: 'builtin.welcome', placeholder: 'START'), + fontSize: clideFontSmall, + color: tokens.sidebarSectionHeader, + fontFamily: ClideSettings.fonts.monoOf(context), + ), const SizedBox(height: 20), // Only flows that exist get a tile — the old Clone-from-git and // Start-a-Claude-session rows were inert and advertised shortcuts // that were never registered (T-383). Re-add each WITH its flow. - _ActionRow(icon: PhosphorIcons.byName('folder'), label: 'Open folder…', shortcut: '⌃O', tokens: tokens, onTap: () => _openFolder(context)), + _ActionRow( + icon: PhosphorIcons.byName('folder'), + label: ClideSettings.i18n.string(context, 'action.openFolder', namespace: 'builtin.welcome', placeholder: 'Open folder…'), + shortcut: '⌃O', + tokens: tokens, + onTap: () => _openFolder(context), + ), ], ); } @@ -255,10 +277,19 @@ class _RecentColumn extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('RECENT', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)), + ClideText( + ClideSettings.i18n.string(ctx, 'section.recent', namespace: 'builtin.welcome', placeholder: 'RECENT'), + fontSize: clideFontSmall, + color: tokens.sidebarSectionHeader, + fontFamily: ClideSettings.fonts.monoOf(context), + ), const SizedBox(height: 20), if (recents.isEmpty) - const ClideText('No recent projects.', muted: true, fontSize: clideFontCaption) + ClideText( + ClideSettings.i18n.string(ctx, 'recent.empty', namespace: 'builtin.welcome', placeholder: 'No recent projects.'), + muted: true, + fontSize: clideFontCaption, + ) else for (final r in recents) _RecentRow( @@ -358,8 +389,15 @@ class _StickyToggle extends StatelessWidget { return Semantics( button: true, checked: sticky, - label: 'always open this project on launch', - tooltip: sticky ? 'Always open this project on launch (uncheck to restore picker)' : 'Always open this project on launch', + label: ClideSettings.i18n.string(context, 'sticky.label', namespace: 'builtin.welcome', placeholder: 'always open this project on launch'), + tooltip: sticky + ? ClideSettings.i18n.string( + context, + 'sticky.tooltip.active', + namespace: 'builtin.welcome', + placeholder: 'Always open this project on launch (uncheck to restore picker)', + ) + : ClideSettings.i18n.string(context, 'sticky.tooltip', namespace: 'builtin.welcome', placeholder: 'Always open this project on launch'), child: ClideTappable( onTap: onTap, builder: (context, hovered, _) => Container( @@ -402,12 +440,32 @@ class _StatusLine extends StatelessWidget { ClideText('$clideName $clideVersion', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)), ClideText(' · ', muted: true, fontSize: clideFontSmall), if (!tc.resolved) - ClideText('checking…', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)) + ClideText( + ClideSettings.i18n.string(ctx, 'status.checking', namespace: 'builtin.welcome', placeholder: 'checking…'), + muted: true, + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + ) else if (tc.allOk) - ClideText('application ok', fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.statusSuccess) + ClideText( + ClideSettings.i18n.string(ctx, 'status.ok', namespace: 'builtin.welcome', placeholder: 'application ok'), + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + color: tokens.statusSuccess, + ) else ClideText( - tc.missing.map((t) => '$t not found').join(' · '), + tc.missing + .map( + (t) => ClideSettings.i18n.interpolated( + ctx, + 'status.notFound', + namespace: 'builtin.welcome', + placeholder: '{tool} not found', + replacers: [I18nReplacer(from: '{tool}', replace: t)], + ), + ) + .join(' · '), fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.statusWarning, @@ -435,7 +493,12 @@ class _ThemeLink extends StatelessWidget { builder: (ctx, hovered, _) => Row( mainAxisSize: MainAxisSize.min, children: [ - ClideText('theme: ', muted: true, fontSize: clideFontSmall, fontFamily: ClideSettings.fonts.monoOf(context)), + ClideText( + ClideSettings.i18n.string(ctx, 'status.theme', namespace: 'builtin.welcome', placeholder: 'theme: '), + muted: true, + fontSize: clideFontSmall, + fontFamily: ClideSettings.fonts.monoOf(context), + ), ClideText( themeName, fontSize: clideFontSmall, @@ -486,7 +549,11 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { try { await widget.onOpen(path); } catch (_) { - if (mounted) setState(() => _error = 'Not a git repository'); + if (mounted) { + setState( + () => _error = ClideSettings.i18n.string(context, 'dialog.openProject.error', namespace: 'builtin.welcome', placeholder: 'Not a git repository'), + ); + } } if (mounted) setState(() => _loading = false); } @@ -506,9 +573,17 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ClideText('Open project', fontSize: clideFontDialogTitle, fontWeight: FontWeight.w600), + ClideText( + ClideSettings.i18n.string(context, 'dialog.openProject.title', namespace: 'builtin.welcome', placeholder: 'Open project'), + fontSize: clideFontDialogTitle, + fontWeight: FontWeight.w600, + ), const SizedBox(height: 4), - const ClideText('Enter the path to a git repository.', muted: true, fontSize: clideFontMeta), + ClideText( + ClideSettings.i18n.string(context, 'dialog.openProject.body', namespace: 'builtin.welcome', placeholder: 'Enter the path to a git repository.'), + muted: true, + fontSize: clideFontMeta, + ), const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), @@ -536,9 +611,17 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - ClideButton(label: 'Cancel', onPressed: widget.onCancel), + ClideButton( + label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.welcome', placeholder: 'Cancel'), + onPressed: widget.onCancel, + ), const SizedBox(width: 8), - ClideButton(label: _loading ? 'Opening…' : 'Open', onPressed: _loading ? null : _submit), + ClideButton( + label: _loading + ? ClideSettings.i18n.string(context, 'button.opening', namespace: 'builtin.welcome', placeholder: 'Opening…') + : ClideSettings.i18n.string(context, 'button.open', namespace: 'builtin.welcome', placeholder: 'Open'), + onPressed: _loading ? null : _submit, + ), ], ), ], @@ -567,15 +650,33 @@ class _NotARepoDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ClideText('No git repo found', fontSize: clideFontDialogTitle, fontWeight: FontWeight.w600), + ClideText( + ClideSettings.i18n.string(context, 'dialog.notRepo.title', namespace: 'builtin.welcome', placeholder: 'No git repo found'), + fontSize: clideFontDialogTitle, + fontWeight: FontWeight.w600, + ), const SizedBox(height: 8), ClideText(path, muted: true, fontSize: clideFontMeta), const SizedBox(height: 8), - const ClideText('A clide project root requires a git repository.', muted: true, fontSize: clideFontMeta), + ClideText( + ClideSettings.i18n.string( + context, + 'dialog.notRepo.body', + namespace: 'builtin.welcome', + placeholder: 'A clide project root requires a git repository.', + ), + muted: true, + fontSize: clideFontMeta, + ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.end, - children: [ClideButton(label: 'OK', onPressed: () => onDismiss())], + children: [ + ClideButton( + label: ClideSettings.i18n.string(context, 'button.ok', namespace: 'builtin.welcome', placeholder: 'OK'), + onPressed: () => onDismiss(), + ), + ], ), ], ), diff --git a/lib/kernel/src/i18n/catalog/builtin.welcome_en_us.json b/lib/kernel/src/i18n/catalog/builtin.welcome_en_us.json index 9400453c..dedf4cd3 100644 --- a/lib/kernel/src/i18n/catalog/builtin.welcome_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.welcome_en_us.json @@ -3,5 +3,32 @@ "subtitle": { "translation": "IDE for Claude Code CLI" }, "open-project": { "translation": "Open project" }, "open-project.hint": { "translation": "Pick a git repository to open as the workspace" }, - "tab.title": { "translation": "Welcome" } + "tab.title": { "translation": "Welcome" }, + "section.tips": { "translation": "TIPS" }, + "section.start": { "translation": "START" }, + "section.recent": { "translation": "RECENT" }, + "tips.quickOpen": { "translation": "Quick open" }, + "tips.commandPalette": { "translation": "Command palette" }, + "tips.toggleSidebar": { "translation": "Toggle sidebar" }, + "tips.toggleContext": { "translation": "Toggle context" }, + "tips.findInFiles": { "translation": "Find in files" }, + "tips.focusMode": { "translation": "Focus mode" }, + "action.openFolder": { "translation": "Open folder…" }, + "recent.empty": { "translation": "No recent projects." }, + "sticky.label": { "translation": "always open this project on launch" }, + "sticky.tooltip": { "translation": "Always open this project on launch" }, + "sticky.tooltip.active": { "translation": "Always open this project on launch (uncheck to restore picker)" }, + "status.checking": { "translation": "checking…" }, + "status.ok": { "translation": "application ok" }, + "status.notFound": { "translation": "{tool} not found" }, + "status.theme": { "translation": "theme: " }, + "dialog.openProject.title": { "translation": "Open project" }, + "dialog.openProject.body": { "translation": "Enter the path to a git repository." }, + "dialog.openProject.error": { "translation": "Not a git repository" }, + "button.cancel": { "translation": "Cancel" }, + "button.open": { "translation": "Open" }, + "button.opening": { "translation": "Opening…" }, + "button.ok": { "translation": "OK" }, + "dialog.notRepo.title": { "translation": "No git repo found" }, + "dialog.notRepo.body": { "translation": "A clide project root requires a git repository." } }