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
+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." }
}