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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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." }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user