refactor(i18n): route welcome labels through the catalog (T-465)

Migrate the ~25 hardcoded user-facing strings in builtin.welcome (section
headers, tips, action + status lines, both dialogs) to
ClideSettings.i18n.string(...), keeping the English as the placeholder; extend
builtin.welcome_en_us.json to cover them. The _tips const widened to carry
(key, English, glyph) so the labels resolve at render. No en_US behaviour
change — strings now resolve through the catalog so they can be localized
(D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:52:05 +02:00
co-authored by Claude Opus 4.8
parent 3e83373258
commit e1d57e7964
4 changed files with 166 additions and 32 deletions
@@ -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;
+5
View File
@@ -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);
+132 -31
View File
@@ -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(),
),
],
),
],
),
@@ -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." }
}