From c5500dc2356e7ecb438d4962d585421d9828de3a Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 18 Jun 2026 22:16:20 +0200 Subject: [PATCH] refactor(i18n): route remaining extension labels through the catalog (T-468) Migrate the user-facing strings in menubar, output, search, deeplink, editor, files, terminal, markdown, and vim to ClideSettings.i18n.string/.interpolated; extend the existing catalogs and create the missing ones (deeplink, output, search, markdown, menubar, vim). vim mode labels now resolve through builtin.vim (default still `-- NORMAL --`, now localizable). A dock-status test seeds builtin.output so the widget's own i18n lookups resolve instead of warning into the logRing it counts. No en_US behaviour change (D-21). Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 7 + lib/builtin/deeplink/src/extension.dart | 34 ++++- lib/builtin/editor/src/editor_view.dart | 18 ++- lib/builtin/files/src/file_tree_view.dart | 43 +++++- lib/builtin/markdown/src/markdown_viewer.dart | 18 ++- lib/builtin/menubar/src/about_dialog.dart | 45 +++++- lib/builtin/menubar/src/file_actions.dart | 57 ++++++-- lib/builtin/menubar/src/menu_bar.dart | 16 ++- lib/builtin/output/src/dock_status_item.dart | 5 +- lib/builtin/output/src/output_view.dart | 55 +++++-- lib/builtin/search/src/search_panel_view.dart | 136 +++++++++++++----- lib/builtin/terminal/src/terminal_pane.dart | 36 ++++- lib/builtin/vim/src/vim_mode_indicator.dart | 9 +- lib/builtin/vim/src/vim_mode_service.dart | 5 +- .../i18n/catalog/builtin.deeplink_en_us.json | 7 + .../i18n/catalog/builtin.editor_en_us.json | 4 +- .../src/i18n/catalog/builtin.files_en_us.json | 7 +- .../i18n/catalog/builtin.markdown_en_us.json | 5 + .../i18n/catalog/builtin.menubar_en_us.json | 22 +++ .../i18n/catalog/builtin.output_en_us.json | 14 ++ .../i18n/catalog/builtin.search_en_us.json | 23 +++ .../i18n/catalog/builtin.terminal_en_us.json | 4 +- .../src/i18n/catalog/builtin.vim_en_us.json | 5 + .../builtin/output/dock_status_item_test.dart | 16 ++- 25 files changed, 495 insertions(+), 97 deletions(-) create mode 100644 lib/kernel/src/i18n/catalog/builtin.deeplink_en_us.json create mode 100644 lib/kernel/src/i18n/catalog/builtin.markdown_en_us.json create mode 100644 lib/kernel/src/i18n/catalog/builtin.menubar_en_us.json create mode 100644 lib/kernel/src/i18n/catalog/builtin.output_en_us.json create mode 100644 lib/kernel/src/i18n/catalog/builtin.search_en_us.json create mode 100644 lib/kernel/src/i18n/catalog/builtin.vim_en_us.json diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 00414d55..6aba9620 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5942,3 +5942,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 ('06FDA0WCPG458GF5FXTXM9VY98', 'status', 'in_progress', 'done', NULL, '2026-06-18 19:14:41', '2026-06-18 19:14:41.116', '2026-06-18 19:14:41.116', NULL, '2bf6c6fbef7a2e950fc1f31541f8dc22', 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 ('06FDA0XMA9BQV4RHJ55BFN4JR4', 'status', 'in_progress', 'done', NULL, '2026-06-18 19:19:25', '2026-06-18 19:19:25.269', '2026-06-18 19:19:25.269', NULL, 'f1660567ecfe0c7220c23244206bc4d2', 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 ('06FDA0YCY2WGSG8W46RHQ5Z9MW', 'status', 'in_progress', 'done', NULL, '2026-06-18 20:13:50', '2026-06-18 20:13:50.352', '2026-06-18 20:13:50.352', NULL, 'a885fb7b56c053669a8ef5f0e6032feb', 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 ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'status', 'in_progress', 'done', NULL, '2026-06-18 20:16:03', '2026-06-18 20:16:03.747', '2026-06-18 20:16:03.747', NULL, '5e67beaa758d779f1b9212aecb82e050', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index e69581c9..2001ccc9 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7348,3 +7348,10 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat For each: route hardcoded ClideText/Text/tooltip literals through ctx.i18n.string(..., namespace: ''builtin.'', placeholder: ) and add keys. Acceptance: no hardcoded user-facing literals remain in lib/builtin/{tickets,pql,decisions,problems}/; keys present in each respective catalog.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:36.624', '2026-06-18 20:13:50.352', NULL, '919efbfb38b655d5157f73572aa20fad', 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 ('06FDA0YYCJ0NK8C4HMDYKJHRDG', 'task', '06FDA0TPYSWEM10RP0Q76XAP58', 'i18n: migrate remaining extension labels — menubar, output, search, deeplink, editor, files, terminal, markdown, vim (~24 strings)', 'Tail of the audit: the remaining extensions with hardcoded labels. menubar ~6 (no catalog), output ~4 (no catalog), search ~3 (no catalog), deeplink ~3 (no catalog), editor ~1, files ~1, terminal ~1, markdown ~1 (no catalog), vim ~1 (no catalog). + +Several have no catalog file yet — create lib/kernel/src/i18n/catalog/builtin._en_us.json for menubar, output, search, deeplink, markdown, vim. editor/files/terminal already have catalogs (with orphaned keys) — wire their hardcoded literals up and consume the orphans where they match. + +Can be split into smaller tasks if a single PR is too large; grouped here to keep the long tail in one tracked place. + +Acceptance: no hardcoded user-facing literals remain in any of these extensions; each has a catalog file with the keys it uses; no orphaned keys left in editor/files/terminal.', 'done', 'medium', NULL, NULL, 'D-21', '2026-06-17 09:57:41.092', '2026-06-18 20:16:03.746', NULL, '8212472a6e32ec3000b829a7abee3cb9', 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/deeplink/src/extension.dart b/lib/builtin/deeplink/src/extension.dart index 4ce79d76..8f7dde49 100644 --- a/lib/builtin/deeplink/src/extension.dart +++ b/lib/builtin/deeplink/src/extension.dart @@ -78,20 +78,44 @@ class _DeepLinkConfirmDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('Open an external link?', fontSize: clideFontBody, color: t.globalForeground), + ClideText( + ClideSettings.i18n.string(context, 'dialog.title', namespace: 'builtin.deeplink', placeholder: 'Open an external link?'), + fontSize: clideFontBody, + color: t.globalForeground, + ), const SizedBox(height: 6), - ClideText('A clide:// link from outside the app is asking to:', muted: true, fontSize: clideFontSmall), + ClideText( + ClideSettings.i18n.string(context, 'dialog.body', namespace: 'builtin.deeplink', placeholder: 'A clide:// link from outside the app is asking to:'), + muted: true, + fontSize: clideFontSmall, + ), const SizedBox(height: 8), ClideText(action.describe, fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontSmall, color: t.globalForeground), const SizedBox(height: 8), - ClideText('Only allow this if you trust where the link came from.', fontSize: clideFontMeta, color: t.statusWarning), + ClideText( + ClideSettings.i18n.string( + context, + 'dialog.warning', + namespace: 'builtin.deeplink', + placeholder: 'Only allow this if you trust where the link came from.', + ), + fontSize: clideFontMeta, + color: t.statusWarning, + ), const SizedBox(height: 14), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - ClideButton(label: 'Cancel', onPressed: () => onResolve(false)), + ClideButton( + label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.deeplink', placeholder: 'Cancel'), + onPressed: () => onResolve(false), + ), const SizedBox(width: 8), - ClideButton(label: 'Open', variant: ClideButtonVariant.primary, onPressed: () => onResolve(true)), + ClideButton( + label: ClideSettings.i18n.string(context, 'button.open', namespace: 'builtin.deeplink', placeholder: 'Open'), + variant: ClideButtonVariant.primary, + onPressed: () => onResolve(true), + ), ], ), ], diff --git a/lib/builtin/editor/src/editor_view.dart b/lib/builtin/editor/src/editor_view.dart index 1bd22f75..6ec30ae3 100644 --- a/lib/builtin/editor/src/editor_view.dart +++ b/lib/builtin/editor/src/editor_view.dart @@ -320,9 +320,19 @@ class _EditorViewState extends State { builder: (context, _) { if (c.buffers.isEmpty) { return ClidePaneChrome( - title: 'editor', - subtitle: 'no buffer · use `clide open ` or pick a file in the tree', - child: const Center(child: ClideText('Open a file to begin editing.', muted: true)), + title: ClideSettings.i18n.string(context, 'chrome.title', namespace: 'builtin.editor', placeholder: 'editor'), + subtitle: ClideSettings.i18n.string( + context, + 'subtitle.no-buffer', + namespace: 'builtin.editor', + placeholder: 'no buffer · use `clide open ` or pick a file in the tree', + ), + child: Center( + child: ClideText( + ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.editor', placeholder: 'Open a file to begin editing.'), + muted: true, + ), + ), ); } return MultitabPane( @@ -392,7 +402,7 @@ class _TextBody extends StatelessWidget { showCursor: true, ); return Semantics( - label: 'editor text area', + label: ClideSettings.i18n.string(context, 'a11y.text-area', namespace: 'builtin.editor', placeholder: 'editor text area'), textField: true, multiline: true, child: ColoredBox( diff --git a/lib/builtin/files/src/file_tree_view.dart b/lib/builtin/files/src/file_tree_view.dart index aee7bc3f..9a0398ea 100644 --- a/lib/builtin/files/src/file_tree_view.dart +++ b/lib/builtin/files/src/file_tree_view.dart @@ -105,7 +105,10 @@ class _FileTreeViewState extends State { } final root = c.rootPath; if (root == null) { - return const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)); + return Padding( + padding: const EdgeInsets.all(12), + child: ClideText(ClideSettings.i18n.string(context, 'loading', namespace: 'builtin.files', placeholder: 'Loading…'), muted: true), + ); } final rootName = root.split(Platform.pathSeparator).last; final selected = c.selectedPath; @@ -127,10 +130,20 @@ class _FileTreeViewState extends State { ); return Column( children: [ - ClideFilterBox(address: 'files.tree', hint: 'Filter files…', onChanged: (v) => setState(() => _filter = v)), + ClideFilterBox( + address: 'files.tree', + hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.files', placeholder: 'Filter files…'), + onChanged: (v) => setState(() => _filter = v), + ), Expanded( child: Semantics( - label: 'file tree — $rootName', + label: ClideSettings.i18n.interpolated( + context, + 'a11y.tree', + namespace: 'builtin.files', + placeholder: 'file tree — {name}', + replacers: [I18nReplacer(from: '{name}', replace: rootName)], + ), container: true, explicitChildNodes: true, // Vim nav (j/k/h/l/gg/G/o) drives a selection cursor while this @@ -206,7 +219,21 @@ class _DirRow extends StatelessWidget { final selected = path == selectedPath; return Semantics( button: true, - label: '${expanded ? 'Collapse' : 'Expand'} $name', + label: expanded + ? ClideSettings.i18n.interpolated( + context, + 'a11y.collapse', + namespace: 'builtin.files', + placeholder: 'Collapse {name}', + replacers: [I18nReplacer(from: '{name}', replace: name)], + ) + : ClideSettings.i18n.interpolated( + context, + 'a11y.expand', + namespace: 'builtin.files', + placeholder: 'Expand {name}', + replacers: [I18nReplacer(from: '{name}', replace: name)], + ), onTap: () => controller.toggle(path), child: _Row( key: selected ? selectedKey : null, @@ -235,7 +262,13 @@ class _FileRow extends StatelessWidget { final selected = path == selectedPath; return Semantics( button: true, - label: 'Open $name', + label: ClideSettings.i18n.interpolated( + context, + 'a11y.open', + namespace: 'builtin.files', + placeholder: 'Open {name}', + replacers: [I18nReplacer(from: '{name}', replace: name)], + ), onTap: () => _openFile(context, path), child: _Row(key: selected ? selectedKey : null, depth: depth, onTap: () => _openFile(context, path), label: name, selected: selected), ); diff --git a/lib/builtin/markdown/src/markdown_viewer.dart b/lib/builtin/markdown/src/markdown_viewer.dart index eb571842..0cac1f1b 100644 --- a/lib/builtin/markdown/src/markdown_viewer.dart +++ b/lib/builtin/markdown/src/markdown_viewer.dart @@ -95,11 +95,23 @@ class _MarkdownViewerState extends State { return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)); } if (_content == null) { - return const Padding(padding: EdgeInsets.all(12), child: ClideText('Select a .md file to preview it here.', muted: true)); + return Padding( + padding: const EdgeInsets.all(12), + child: ClideText( + ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.markdown', placeholder: 'Select a .md file to preview it here.'), + muted: true, + ), + ); } return ClidePaneChrome( - title: _path ?? 'viewer', - subtitle: '${_content!.split('\n').length} lines', + title: _path ?? ClideSettings.i18n.string(context, 'chrome.title', namespace: 'builtin.markdown', placeholder: 'viewer'), + subtitle: ClideSettings.i18n.interpolated( + context, + 'subtitle.lines', + namespace: 'builtin.markdown', + placeholder: '{count} lines', + replacers: [I18nReplacer(from: '{count}', replace: '${_content!.split('\n').length}')], + ), leading: ReaderPinButton(pinned: _nav?.hasPinned ?? false, onTap: _path != null ? _onPin : null), trailing: [ ReaderActionBar( diff --git a/lib/builtin/menubar/src/about_dialog.dart b/lib/builtin/menubar/src/about_dialog.dart index c720de35..4710ece9 100644 --- a/lib/builtin/menubar/src/about_dialog.dart +++ b/lib/builtin/menubar/src/about_dialog.dart @@ -32,18 +32,43 @@ class AboutDialog extends StatelessWidget { const SizedBox(height: 2), const ClideText(clideTagline, muted: true, fontSize: 13), const SizedBox(height: 16), - _Kv(label: 'Version', value: clideVersion, tokens: tokens), - _Kv(label: 'Commit', value: clideCommit, tokens: tokens), - _Kv(label: 'Built', value: _formatDate(clideDate), tokens: tokens), - _Kv(label: 'Repository', value: clideRepository, tokens: tokens), + _Kv( + label: ClideSettings.i18n.string(context, 'about.version', namespace: 'builtin.menubar', placeholder: 'Version'), + value: clideVersion, + tokens: tokens, + ), + _Kv( + label: ClideSettings.i18n.string(context, 'about.commit', namespace: 'builtin.menubar', placeholder: 'Commit'), + value: clideCommit, + tokens: tokens, + ), + _Kv( + label: ClideSettings.i18n.string(context, 'about.built', namespace: 'builtin.menubar', placeholder: 'Built'), + value: _formatDate(clideDate), + tokens: tokens, + ), + _Kv( + label: ClideSettings.i18n.string(context, 'about.repository', namespace: 'builtin.menubar', placeholder: 'Repository'), + value: clideRepository, + tokens: tokens, + ), const SizedBox(height: 16), - ClideText('Bundled dependencies', fontSize: clideFontCaption, color: tokens.globalTextMuted), + ClideText( + ClideSettings.i18n.string(context, 'licenses.heading', namespace: 'builtin.menubar', placeholder: 'Bundled dependencies'), + fontSize: clideFontCaption, + color: tokens.globalTextMuted, + ), const SizedBox(height: 6), _Licenses(tokens: tokens), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.end, - children: [ClideButton(label: 'Close', onPressed: onDismiss)], + children: [ + ClideButton( + label: ClideSettings.i18n.string(context, 'button.close', namespace: 'builtin.menubar', placeholder: 'Close'), + onPressed: onDismiss, + ), + ], ), ], ), @@ -86,7 +111,13 @@ class _Licenses extends StatelessWidget { future: loadLicenses(), builder: (context, snap) { if (!snap.hasData) { - return ClideText(snap.hasError ? 'Licenses unavailable.' : 'Loading…', fontSize: 12, color: tokens.globalTextMuted); + return ClideText( + snap.hasError + ? ClideSettings.i18n.string(context, 'licenses.unavailable', namespace: 'builtin.menubar', placeholder: 'Licenses unavailable.') + : ClideSettings.i18n.string(context, 'licenses.loading', namespace: 'builtin.menubar', placeholder: 'Loading…'), + fontSize: 12, + color: tokens.globalTextMuted, + ); } final deps = snap.data!.dependencies; return Container( diff --git a/lib/builtin/menubar/src/file_actions.dart b/lib/builtin/menubar/src/file_actions.dart index 642504a7..04a3b028 100644 --- a/lib/builtin/menubar/src/file_actions.dart +++ b/lib/builtin/menubar/src/file_actions.dart @@ -123,9 +123,17 @@ class _OpenFolderDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ClideText('Open project', fontSize: 16, fontWeight: FontWeight.w600), + ClideText( + ClideSettings.i18n.string(context, 'dialog.openProject.title', namespace: 'builtin.menubar', placeholder: 'Open project'), + fontSize: 16, + fontWeight: FontWeight.w600, + ), const SizedBox(height: 4), - const ClideText('Enter the path to a git repository.', muted: true, fontSize: 13), + ClideText( + ClideSettings.i18n.string(context, 'dialog.openProject.body', namespace: 'builtin.menubar', placeholder: 'Enter the path to a git repository.'), + muted: true, + fontSize: 13, + ), const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), @@ -148,14 +156,29 @@ class _OpenFolderDialogState extends State { onSubmitted: (_) => unawaited(_submit()), ), ), - if (_error != null) ...[const SizedBox(height: 8), ClideText(_error!, color: tokens.statusError, fontSize: 12)], + if (_error != null) ...[ + const SizedBox(height: 8), + ClideText( + ClideSettings.i18n.string(context, 'dialog.openProject.error', namespace: 'builtin.menubar', placeholder: 'Not a git repository'), + color: tokens.statusError, + fontSize: 12, + ), + ], const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - ClideButton(label: 'Cancel', onPressed: widget.onCancel), + ClideButton( + label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.menubar', 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.menubar', placeholder: 'Opening…') + : ClideSettings.i18n.string(context, 'button.open', namespace: 'builtin.menubar', placeholder: 'Open'), + onPressed: _loading ? null : _submit, + ), ], ), ], @@ -186,15 +209,33 @@ class NotARepoDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ClideText('No git repo found', fontSize: 16, fontWeight: FontWeight.w600), + ClideText( + ClideSettings.i18n.string(context, 'dialog.notRepo.title', namespace: 'builtin.menubar', placeholder: 'No git repo found'), + fontSize: 16, + fontWeight: FontWeight.w600, + ), const SizedBox(height: 8), ClideText(path, muted: true, fontSize: 13), const SizedBox(height: 8), - const ClideText('A clide project root requires a git repository.', muted: true, fontSize: 13), + ClideText( + ClideSettings.i18n.string( + context, + 'dialog.notRepo.body', + namespace: 'builtin.menubar', + placeholder: 'A clide project root requires a git repository.', + ), + muted: true, + fontSize: 13, + ), 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.menubar', placeholder: 'OK'), + onPressed: () => onDismiss(), + ), + ], ), ], ), diff --git a/lib/builtin/menubar/src/menu_bar.dart b/lib/builtin/menubar/src/menu_bar.dart index 716aa609..11f65fe3 100644 --- a/lib/builtin/menubar/src/menu_bar.dart +++ b/lib/builtin/menubar/src/menu_bar.dart @@ -187,10 +187,24 @@ class _TopMenuButtonState extends State<_TopMenuButton> { alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: clideInsetStandard), color: open || hovered ? tokens.listItemHoverBackground : null, - child: ClideText(widget.menu.title, fontSize: 12, color: open || hovered ? tokens.globalForeground : tokens.chromeForeground), + child: ClideText(_menuTitle(ctx, widget.menu.title), fontSize: 12, color: open || hovered ? tokens.globalForeground : tokens.chromeForeground), ), ), ), ); } + + /// Localize a curated top-menu title. The curated tree authors English + /// titles ('File'/'View'/'Help'); resolve each to its catalog entry, falling + /// back to the authored English (which also drives the Alt mnemonic). + String _menuTitle(BuildContext context, String title) { + final key = switch (title) { + 'File' => 'menu.file', + 'View' => 'menu.view', + 'Help' => 'menu.help', + _ => null, + }; + if (key == null) return title; + return ClideSettings.i18n.string(context, key, namespace: 'builtin.menubar', placeholder: title); + } } diff --git a/lib/builtin/output/src/dock_status_item.dart b/lib/builtin/output/src/dock_status_item.dart index 2380c738..535e38bb 100644 --- a/lib/builtin/output/src/dock_status_item.dart +++ b/lib/builtin/output/src/dock_status_item.dart @@ -62,9 +62,10 @@ class _DockStatusItemState extends State { : warns > 0 ? ('⚠ $warns', tokens.statusWarning) : ('✓', tokens.statusSuccess); + final dockLabel = ClideSettings.i18n.string(ctx, 'dock.label', namespace: 'builtin.output', placeholder: 'Output'); return Semantics( button: true, - label: 'toggle output dock', + label: ClideSettings.i18n.string(ctx, 'a11y.toggleDock', namespace: 'builtin.output', placeholder: 'toggle output dock'), child: GestureDetector( onTap: () => _toggle(kernel), child: MouseRegion( @@ -74,7 +75,7 @@ class _DockStatusItemState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - ClideText('${open ? '▼' : '▲'} Output ', fontSize: clideFontCaption, color: tokens.globalForeground), + ClideText('${open ? '▼' : '▲'} $dockLabel ', fontSize: clideFontCaption, color: tokens.globalForeground), ClideText(badge, fontSize: clideFontCaption, color: color), ], ), diff --git a/lib/builtin/output/src/output_view.dart b/lib/builtin/output/src/output_view.dart index 1ea565f9..c094ef61 100644 --- a/lib/builtin/output/src/output_view.dart +++ b/lib/builtin/output/src/output_view.dart @@ -78,18 +78,23 @@ class _OutputViewState extends State { final tokens = ClideSettings.theme.of(context).surface; final rows = _c.filtered; return Semantics( - label: 'output log', + label: ClideSettings.i18n.string(context, 'a11y.log', namespace: 'builtin.output', placeholder: 'output log'), container: true, explicitChildNodes: true, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _header(tokens), + _header(context, tokens), Expanded( child: rows.isEmpty ? Padding( padding: const EdgeInsets.all(12), - child: ClideText(widget.ring.isEmpty ? 'No output yet.' : 'No output matches the filter.', muted: true), + child: ClideText( + widget.ring.isEmpty + ? ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.output', placeholder: 'No output yet.') + : ClideSettings.i18n.string(context, 'empty.filtered', namespace: 'builtin.output', placeholder: 'No output matches the filter.'), + muted: true, + ), ) : Stack( children: [ @@ -113,7 +118,8 @@ class _OutputViewState extends State { ); } - Widget _header(SurfaceTokens tokens) { + Widget _header(BuildContext context, SurfaceTokens tokens) { + final sourceValue = _c.source ?? ClideSettings.i18n.string(context, 'source.all', namespace: 'builtin.output', placeholder: 'all'); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( @@ -122,14 +128,39 @@ class _OutputViewState extends State { child: Row( children: [ Expanded( - child: ClideFilterBox(address: 'output.panel', hint: 'Filter…', onChanged: _c.setText), + child: ClideFilterBox( + address: 'output.panel', + hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.output', placeholder: 'Filter…'), + onChanged: _c.setText, + ), ), const SizedBox(width: 8), - _Chip(label: 'Level: ${_c.minLevel.name}', onTap: () => _c.setMinLevel(LogLevel.values[(_c.minLevel.index + 1) % LogLevel.values.length])), + _Chip( + label: ClideSettings.i18n.interpolated( + context, + 'chip.level', + namespace: 'builtin.output', + placeholder: 'Level: {level}', + replacers: [I18nReplacer(from: '{level}', replace: _c.minLevel.name)], + ), + onTap: () => _c.setMinLevel(LogLevel.values[(_c.minLevel.index + 1) % LogLevel.values.length]), + ), const SizedBox(width: 6), - _Chip(label: 'Source: ${_c.source ?? 'all'}', onTap: _cycleSource), + _Chip( + label: ClideSettings.i18n.interpolated( + context, + 'chip.source', + namespace: 'builtin.output', + placeholder: 'Source: {source}', + replacers: [I18nReplacer(from: '{source}', replace: sourceValue)], + ), + onTap: _cycleSource, + ), const SizedBox(width: 6), - _Chip(label: 'Clear', onTap: _c.clear), + _Chip( + label: ClideSettings.i18n.string(context, 'chip.clear', namespace: 'builtin.output', placeholder: 'Clear'), + onTap: _c.clear, + ), ], ), ); @@ -180,7 +211,7 @@ class _JumpPill extends StatelessWidget { final tokens = ClideSettings.theme.of(context).surface; return Semantics( button: true, - label: 'jump to latest', + label: ClideSettings.i18n.string(context, 'a11y.jumpToLatest', namespace: 'builtin.output', placeholder: 'jump to latest'), child: GestureDetector( onTap: onTap, child: MouseRegion( @@ -192,7 +223,11 @@ class _JumpPill extends StatelessWidget { border: Border.all(color: tokens.globalFocus), borderRadius: BorderRadius.circular(12), ), - child: ClideText('Jump to latest ↓', fontSize: clideFontCaption, color: tokens.globalFocus), + child: ClideText( + ClideSettings.i18n.string(context, 'jump.label', namespace: 'builtin.output', placeholder: 'Jump to latest ↓'), + fontSize: clideFontCaption, + color: tokens.globalFocus, + ), ), ), ), diff --git a/lib/builtin/search/src/search_panel_view.dart b/lib/builtin/search/src/search_panel_view.dart index 6693981d..8f9b35b0 100644 --- a/lib/builtin/search/src/search_panel_view.dart +++ b/lib/builtin/search/src/search_panel_view.dart @@ -51,14 +51,33 @@ class _SearchPanelViewState extends State { final dialog = ClideKernel.of(context).dialog; if (!await c.isWorkingTreeClean()) { await dialog.show( - (ctx, dismiss) => - _MessageDialog(title: 'Working tree not clean', body: 'Commit or stash your changes before replacing — git is the only undo.', dismiss: dismiss), + (ctx, dismiss) => _MessageDialog( + title: ClideSettings.i18n.string(ctx, 'dialog.dirty.title', namespace: 'builtin.search', placeholder: 'Working tree not clean'), + body: ClideSettings.i18n.string( + ctx, + 'dialog.dirty.body', + namespace: 'builtin.search', + placeholder: 'Commit or stash your changes before replacing — git is the only undo.', + ), + dismiss: dismiss, + ), ); return; } final confirmed = await dialog.show( - (ctx, dismiss) => - _ConfirmDialog(body: 'Replace ${c.matchCount} match(es) across ${c.fileCount} file(s)? This cannot be undone in clide.', dismiss: dismiss), + (ctx, dismiss) => _ConfirmDialog( + body: ClideSettings.i18n.interpolated( + ctx, + 'dialog.confirm.body', + namespace: 'builtin.search', + placeholder: 'Replace {matches} match(es) across {files} file(s)? This cannot be undone in clide.', + replacers: [ + I18nReplacer(from: '{matches}', replace: '${c.matchCount}'), + I18nReplacer(from: '{files}', replace: '${c.fileCount}'), + ], + ), + dismiss: dismiss, + ), ); if (confirmed != true) return; await c.applyReplace(); @@ -104,13 +123,18 @@ class _SearchPanelViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ClideFilterBox(address: 'search.findInFiles', hint: 'Search', onChanged: c.run, onSubmitted: c.run), + ClideFilterBox( + address: 'search.findInFiles', + hint: ClideSettings.i18n.string(context, 'find.hint', namespace: 'builtin.search', placeholder: 'Search'), + onChanged: c.run, + onSubmitted: c.run, + ), const SizedBox(height: 6), Row( children: [ _Toggle( label: '.*', - tooltip: 'Regular expression', + tooltip: ClideSettings.i18n.string(context, 'toggle.regex', namespace: 'builtin.search', placeholder: 'Regular expression'), active: c.regex, tokens: tokens, onTap: () { @@ -121,7 +145,7 @@ class _SearchPanelViewState extends State { const SizedBox(width: 6), _Toggle( label: 'Aa', - tooltip: 'Case insensitive', + tooltip: ClideSettings.i18n.string(context, 'toggle.caseInsensitive', namespace: 'builtin.search', placeholder: 'Case insensitive'), active: c.ignoreCase, tokens: tokens, onTap: () { @@ -139,7 +163,7 @@ class _SearchPanelViewState extends State { Expanded( child: ClideFilterBox( address: 'search.findInFiles.replace', - hint: 'Replace', + hint: ClideSettings.i18n.string(context, 'replace.hint', namespace: 'builtin.search', placeholder: 'Replace'), icon: null, debounce: Duration.zero, onChanged: c.setReplacement, @@ -152,7 +176,7 @@ class _SearchPanelViewState extends State { const SizedBox(height: 6), ClideFilterBox( address: 'search.findInFiles.include', - hint: 'files to include (e.g. *.dart)', + hint: ClideSettings.i18n.string(context, 'include.hint', namespace: 'builtin.search', placeholder: 'files to include (e.g. *.dart)'), icon: null, debounce: Duration.zero, onChanged: (v) => c.include = v, @@ -160,7 +184,7 @@ class _SearchPanelViewState extends State { const SizedBox(height: 4), ClideFilterBox( address: 'search.findInFiles.exclude', - hint: 'files to exclude', + hint: ClideSettings.i18n.string(context, 'exclude.hint', namespace: 'builtin.search', placeholder: 'files to exclude'), icon: null, debounce: Duration.zero, onChanged: (v) => c.exclude = v, @@ -196,7 +220,14 @@ class _ModeSwitcher extends StatelessWidget { final SurfaceTokens tokens; final ValueChanged onSelect; - static const _labels = {SearchTabMode.find: 'Find', SearchTabMode.vault: 'Vault', SearchTabMode.query: 'Query', SearchTabMode.markdown: 'Markdown'}; + // (catalog key, English label) per mode. The key/English pair resolves + // through the i18n catalog at render (D-21). + static const _labels = { + SearchTabMode.find: ('mode.find', 'Find'), + SearchTabMode.vault: ('mode.vault', 'Vault'), + SearchTabMode.query: ('mode.query', 'Query'), + SearchTabMode.markdown: ('mode.markdown', 'Markdown'), + }; @override Widget build(BuildContext context) { @@ -207,26 +238,28 @@ class _ModeSwitcher extends StatelessWidget { ), child: Row( children: [ - for (final m in SearchTabMode.values) ...[ - Semantics( - button: true, - selected: m == mode, - label: _labels[m]!, - child: ClideTappable( - onTap: () => onSelect(m), - builder: (ctx, hovered, _) => ClideText( - _labels[m]!, - fontSize: clideFontCaption, - color: m == mode ? tokens.globalForeground : (hovered ? tokens.sidebarForeground : tokens.globalTextMuted), - ), - ), - ), - const SizedBox(width: 10), - ], + for (final m in SearchTabMode.values) ...[_modeButton(context, m), const SizedBox(width: 10)], ], ), ); } + + Widget _modeButton(BuildContext context, SearchTabMode m) { + final label = ClideSettings.i18n.string(context, _labels[m]!.$1, namespace: 'builtin.search', placeholder: _labels[m]!.$2); + return Semantics( + button: true, + selected: m == mode, + label: label, + child: ClideTappable( + onTap: () => onSelect(m), + builder: (ctx, hovered, _) => ClideText( + label, + fontSize: clideFontCaption, + color: m == mode ? tokens.globalForeground : (hovered ? tokens.sidebarForeground : tokens.globalTextMuted), + ), + ), + ); + } } class _Toggle extends StatelessWidget { @@ -274,11 +307,20 @@ class _StatusText extends StatelessWidget { Widget build(BuildContext context) { final String text; if (c.running) { - text = 'Searching…'; + text = ClideSettings.i18n.string(context, 'status.searching', namespace: 'builtin.search', placeholder: 'Searching…'); } else if (c.matchCount == 0 && c.done) { - text = 'No results'; + text = ClideSettings.i18n.string(context, 'status.noResults', namespace: 'builtin.search', placeholder: 'No results'); } else if (c.matchCount > 0) { - text = '${c.matchCount} in ${c.fileCount}'; + text = ClideSettings.i18n.interpolated( + context, + 'status.counts', + namespace: 'builtin.search', + placeholder: '{matches} in {files}', + replacers: [ + I18nReplacer(from: '{matches}', replace: '${c.matchCount}'), + I18nReplacer(from: '{files}', replace: '${c.fileCount}'), + ], + ); } else { text = ''; } @@ -332,7 +374,16 @@ class _MatchRow extends StatelessWidget { Widget build(BuildContext context) { return Semantics( button: true, - label: 'Open ${match.path} line ${match.line}', + label: ClideSettings.i18n.interpolated( + context, + 'a11y.openMatch', + namespace: 'builtin.search', + placeholder: 'Open {path} line {line}', + replacers: [ + I18nReplacer(from: '{path}', replace: match.path), + I18nReplacer(from: '{line}', replace: '${match.line}'), + ], + ), onTap: onTap, child: ClideTappable( onTap: onTap, @@ -414,10 +465,11 @@ class _ReplaceAllButton extends StatelessWidget { @override Widget build(BuildContext context) { + final label = ClideSettings.i18n.string(context, 'button.replaceAll', namespace: 'builtin.search', placeholder: 'Replace all'); return Semantics( button: true, enabled: enabled, - label: 'Replace all', + label: label, child: ClideTappable( onTap: enabled ? onTap : () {}, builder: (context, hovered, _) => Container( @@ -427,7 +479,7 @@ class _ReplaceAllButton extends StatelessWidget { border: Border.all(color: tokens.buttonBorder), borderRadius: BorderRadius.circular(4), ), - child: ClideText('Replace all', fontSize: clideFontCaption, color: enabled ? tokens.sidebarForeground : tokens.globalTextMuted), + child: ClideText(label, fontSize: clideFontCaption, color: enabled ? tokens.sidebarForeground : tokens.globalTextMuted), ), ), ); @@ -453,7 +505,11 @@ class _MessageDialog extends StatelessWidget { const SizedBox(height: 12), Align( alignment: Alignment.centerRight, - child: _DialogButton(label: 'OK', tokens: tokens, onTap: () => dismiss()), + child: _DialogButton( + label: ClideSettings.i18n.string(context, 'button.ok', namespace: 'builtin.search', placeholder: 'OK'), + tokens: tokens, + onTap: () => dismiss(), + ), ), ], ); @@ -477,9 +533,17 @@ class _ConfirmDialog extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - _DialogButton(label: 'Cancel', tokens: tokens, onTap: () => dismiss(false)), + _DialogButton( + label: ClideSettings.i18n.string(context, 'button.cancel', namespace: 'builtin.search', placeholder: 'Cancel'), + tokens: tokens, + onTap: () => dismiss(false), + ), const SizedBox(width: 8), - _DialogButton(label: 'Confirm', tokens: tokens, onTap: () => dismiss(true)), + _DialogButton( + label: ClideSettings.i18n.string(context, 'button.confirm', namespace: 'builtin.search', placeholder: 'Confirm'), + tokens: tokens, + onTap: () => dismiss(true), + ), ], ), ], diff --git a/lib/builtin/terminal/src/terminal_pane.dart b/lib/builtin/terminal/src/terminal_pane.dart index c1b97ef8..8742c223 100644 --- a/lib/builtin/terminal/src/terminal_pane.dart +++ b/lib/builtin/terminal/src/terminal_pane.dart @@ -140,14 +140,38 @@ class _TerminalPaneState extends State { @override Widget build(BuildContext context) { - final subtitle = _error != null ? _error! : (_paneId == null ? 'spawning shell…' : 'pid $_pid · ${_paneId!}'); + final spawning = ClideSettings.i18n.string(context, 'subtitle.spawning', namespace: 'builtin.terminal', placeholder: 'spawning shell…'); + final errorText = _error == null ? null : _localizeError(context, _error!); + final subtitle = errorText ?? (_paneId == null ? spawning : 'pid $_pid · ${_paneId!}'); return ClidePaneChrome( - title: 'terminal', + title: ClideSettings.i18n.string(context, 'chrome.title', namespace: 'builtin.terminal', placeholder: 'terminal'), subtitle: subtitle, - child: _error != null ? _ErrorBody(message: _error!) : ClidePtyView(terminal: _terminal, label: 'terminal — $subtitle'), + child: errorText != null + ? _ErrorBody(message: errorText) + : ClidePtyView( + terminal: _terminal, + label: ClideSettings.i18n.interpolated( + context, + 'a11y.label', + namespace: 'builtin.terminal', + placeholder: 'terminal — {subtitle}', + replacers: [I18nReplacer(from: '{subtitle}', replace: subtitle)], + ), + ), ); } + + /// Localize the two known error literals stored in [_error] (set in async + /// handlers where `context` can't be used); any other value — e.g. a daemon + /// `response.error?.message` — is dynamic data and passes through as-is. + String _localizeError(BuildContext context, String raw) { + return switch (raw) { + 'Backend not connected.' => ClideSettings.i18n.string(context, 'error.daemon', namespace: 'builtin.terminal', placeholder: 'Backend not connected.'), + 'Shell exited.' => ClideSettings.i18n.string(context, 'subtitle.exited', namespace: 'builtin.terminal', placeholder: 'Shell exited.'), + _ => raw, + }; + } } class _ErrorBody extends StatelessWidget { @@ -161,7 +185,11 @@ class _ErrorBody extends StatelessWidget { alignment: Alignment.topLeft, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [const ClideText('Terminal unavailable'), const SizedBox(height: 4), ClideText(message, muted: true)], + children: [ + ClideText(ClideSettings.i18n.string(context, 'error.unavailable', namespace: 'builtin.terminal', placeholder: 'Terminal unavailable')), + const SizedBox(height: 4), + ClideText(message, muted: true), + ], ), ), ); diff --git a/lib/builtin/vim/src/vim_mode_indicator.dart b/lib/builtin/vim/src/vim_mode_indicator.dart index 62456af9..3b2f6cf6 100644 --- a/lib/builtin/vim/src/vim_mode_indicator.dart +++ b/lib/builtin/vim/src/vim_mode_indicator.dart @@ -17,14 +17,11 @@ class VimModeIndicator extends StatelessWidget { builder: (context, _) { if (!service.enabled) return const SizedBox.shrink(); final tokens = ClideSettings.theme.of(context).surface; + final mode = service.mode; + final label = ClideSettings.i18n.string(context, 'mode.${mode.name}', namespace: 'builtin.vim', placeholder: mode.label); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: ClideText( - '-- ${service.mode.label} --', - fontFamily: ClideSettings.fonts.monoOf(context), - fontSize: clideFontCaption, - color: tokens.statusBarForeground, - ), + child: ClideText('-- $label --', fontFamily: ClideSettings.fonts.monoOf(context), fontSize: clideFontCaption, color: tokens.statusBarForeground), ); }, ); diff --git a/lib/builtin/vim/src/vim_mode_service.dart b/lib/builtin/vim/src/vim_mode_service.dart index bd33c011..e83f1fb8 100644 --- a/lib/builtin/vim/src/vim_mode_service.dart +++ b/lib/builtin/vim/src/vim_mode_service.dart @@ -31,8 +31,9 @@ enum VimMode { /// The keymap scope flag set true exactly when this mode is active. final String scopeFlag; - /// Status-bar label, shown as `-- NORMAL --` (Vim's own vocabulary; - /// intentionally untranslated, like Vim itself). + /// English status-bar label, shown as `-- NORMAL --`. Used as the i18n + /// placeholder for `mode.` in the builtin.vim catalog (T-462); the + /// default keeps Vim's own vocabulary, but a locale may override it. final String label; } diff --git a/lib/kernel/src/i18n/catalog/builtin.deeplink_en_us.json b/lib/kernel/src/i18n/catalog/builtin.deeplink_en_us.json new file mode 100644 index 00000000..ee25f878 --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.deeplink_en_us.json @@ -0,0 +1,7 @@ +{ + "dialog.title": { "translation": "Open an external link?" }, + "dialog.body": { "translation": "A clide:// link from outside the app is asking to:" }, + "dialog.warning": { "translation": "Only allow this if you trust where the link came from." }, + "button.cancel": { "translation": "Cancel" }, + "button.open": { "translation": "Open" } +} diff --git a/lib/kernel/src/i18n/catalog/builtin.editor_en_us.json b/lib/kernel/src/i18n/catalog/builtin.editor_en_us.json index 58a96b0f..d6962930 100644 --- a/lib/kernel/src/i18n/catalog/builtin.editor_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.editor_en_us.json @@ -1,5 +1,7 @@ { "tab.title": { "translation": "Editor" }, + "chrome.title": { "translation": "editor" }, "empty": { "translation": "Open a file to begin editing." }, - "subtitle.no-buffer": { "translation": "no buffer · use `clide open ` or pick a file in the tree" } + "subtitle.no-buffer": { "translation": "no buffer · use `clide open ` or pick a file in the tree" }, + "a11y.text-area": { "translation": "editor text area" } } diff --git a/lib/kernel/src/i18n/catalog/builtin.files_en_us.json b/lib/kernel/src/i18n/catalog/builtin.files_en_us.json index 670ebe5f..edd999ea 100644 --- a/lib/kernel/src/i18n/catalog/builtin.files_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.files_en_us.json @@ -1,5 +1,10 @@ { "tab.title": { "translation": "Files" }, "loading": { "translation": "Loading…" }, - "empty": { "translation": "No visible files" } + "empty": { "translation": "No visible files" }, + "filter.hint": { "translation": "Filter files…" }, + "a11y.tree": { "translation": "file tree — {name}" }, + "a11y.collapse": { "translation": "Collapse {name}" }, + "a11y.expand": { "translation": "Expand {name}" }, + "a11y.open": { "translation": "Open {name}" } } diff --git a/lib/kernel/src/i18n/catalog/builtin.markdown_en_us.json b/lib/kernel/src/i18n/catalog/builtin.markdown_en_us.json new file mode 100644 index 00000000..7a02985b --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.markdown_en_us.json @@ -0,0 +1,5 @@ +{ + "empty": { "translation": "Select a .md file to preview it here." }, + "chrome.title": { "translation": "viewer" }, + "subtitle.lines": { "translation": "{count} lines" } +} diff --git a/lib/kernel/src/i18n/catalog/builtin.menubar_en_us.json b/lib/kernel/src/i18n/catalog/builtin.menubar_en_us.json new file mode 100644 index 00000000..ee6ae904 --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.menubar_en_us.json @@ -0,0 +1,22 @@ +{ + "menu.file": { "translation": "File" }, + "menu.view": { "translation": "View" }, + "menu.help": { "translation": "Help" }, + "about.version": { "translation": "Version" }, + "about.commit": { "translation": "Commit" }, + "about.built": { "translation": "Built" }, + "about.repository": { "translation": "Repository" }, + "licenses.heading": { "translation": "Bundled dependencies" }, + "licenses.unavailable": { "translation": "Licenses unavailable." }, + "licenses.loading": { "translation": "Loading…" }, + "button.close": { "translation": "Close" }, + "button.cancel": { "translation": "Cancel" }, + "button.open": { "translation": "Open" }, + "button.opening": { "translation": "Opening…" }, + "button.ok": { "translation": "OK" }, + "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" }, + "dialog.notRepo.title": { "translation": "No git repo found" }, + "dialog.notRepo.body": { "translation": "A clide project root requires a git repository." } +} diff --git a/lib/kernel/src/i18n/catalog/builtin.output_en_us.json b/lib/kernel/src/i18n/catalog/builtin.output_en_us.json new file mode 100644 index 00000000..c967109e --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.output_en_us.json @@ -0,0 +1,14 @@ +{ + "a11y.log": { "translation": "output log" }, + "empty": { "translation": "No output yet." }, + "empty.filtered": { "translation": "No output matches the filter." }, + "filter.hint": { "translation": "Filter…" }, + "chip.level": { "translation": "Level: {level}" }, + "chip.source": { "translation": "Source: {source}" }, + "chip.clear": { "translation": "Clear" }, + "source.all": { "translation": "all" }, + "a11y.jumpToLatest": { "translation": "jump to latest" }, + "jump.label": { "translation": "Jump to latest ↓" }, + "a11y.toggleDock": { "translation": "toggle output dock" }, + "dock.label": { "translation": "Output" } +} diff --git a/lib/kernel/src/i18n/catalog/builtin.search_en_us.json b/lib/kernel/src/i18n/catalog/builtin.search_en_us.json new file mode 100644 index 00000000..3ea22a4e --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.search_en_us.json @@ -0,0 +1,23 @@ +{ + "mode.find": { "translation": "Find" }, + "mode.vault": { "translation": "Vault" }, + "mode.query": { "translation": "Query" }, + "mode.markdown": { "translation": "Markdown" }, + "find.hint": { "translation": "Search" }, + "replace.hint": { "translation": "Replace" }, + "include.hint": { "translation": "files to include (e.g. *.dart)" }, + "exclude.hint": { "translation": "files to exclude" }, + "toggle.regex": { "translation": "Regular expression" }, + "toggle.caseInsensitive": { "translation": "Case insensitive" }, + "status.searching": { "translation": "Searching…" }, + "status.noResults": { "translation": "No results" }, + "status.counts": { "translation": "{matches} in {files}" }, + "a11y.openMatch": { "translation": "Open {path} line {line}" }, + "button.replaceAll": { "translation": "Replace all" }, + "button.ok": { "translation": "OK" }, + "button.cancel": { "translation": "Cancel" }, + "button.confirm": { "translation": "Confirm" }, + "dialog.dirty.title": { "translation": "Working tree not clean" }, + "dialog.dirty.body": { "translation": "Commit or stash your changes before replacing — git is the only undo." }, + "dialog.confirm.body": { "translation": "Replace {matches} match(es) across {files} file(s)? This cannot be undone in clide." } +} diff --git a/lib/kernel/src/i18n/catalog/builtin.terminal_en_us.json b/lib/kernel/src/i18n/catalog/builtin.terminal_en_us.json index 5eced900..9fd66990 100644 --- a/lib/kernel/src/i18n/catalog/builtin.terminal_en_us.json +++ b/lib/kernel/src/i18n/catalog/builtin.terminal_en_us.json @@ -1,7 +1,9 @@ { "tab.title": { "translation": "Terminal" }, + "chrome.title": { "translation": "terminal" }, "subtitle.spawning": { "translation": "spawning shell…" }, "subtitle.exited": { "translation": "Shell exited." }, "error.unavailable": { "translation": "Terminal unavailable" }, - "error.daemon": { "translation": "Backend not connected." } + "error.daemon": { "translation": "Backend not connected." }, + "a11y.label": { "translation": "terminal — {subtitle}" } } diff --git a/lib/kernel/src/i18n/catalog/builtin.vim_en_us.json b/lib/kernel/src/i18n/catalog/builtin.vim_en_us.json new file mode 100644 index 00000000..2196f7af --- /dev/null +++ b/lib/kernel/src/i18n/catalog/builtin.vim_en_us.json @@ -0,0 +1,5 @@ +{ + "mode.normal": { "translation": "NORMAL" }, + "mode.insert": { "translation": "INSERT" }, + "mode.visual": { "translation": "VISUAL" } +} diff --git a/test/builtin/output/dock_status_item_test.dart b/test/builtin/output/dock_status_item_test.dart index 41bd361a..b27e7597 100644 --- a/test/builtin/output/dock_status_item_test.dart +++ b/test/builtin/output/dock_status_item_test.dart @@ -3,6 +3,8 @@ library; import 'package:clide/builtin/default_layout/default_layout.dart'; import 'package:clide/builtin/output/output.dart'; +import 'dart:ui'; + import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -16,7 +18,19 @@ void main() { late KernelFixture f; setUp(() async { - f = await KernelFixture.create(); + // Seed builtin.output so the widget's own i18n lookups resolve — as they do + // at boot in the real app. Otherwise "namespace not registered" warnings + // land in the logRing this very widget counts, skewing the health badge. + f = await KernelFixture.create( + i18nCatalogs: { + 'builtin.output': { + const Locale('en', 'US'): const { + 'dock.label': {'translation': 'Output'}, + 'a11y.toggleDock': {'translation': 'toggle output dock'}, + }, + }, + }, + ); f.services.extensions.register(DefaultLayoutExtension()); await f.services.extensions.activate('builtin.default-layout'); });