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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.<ext>'', placeholder: <current literal>) 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.<ext>_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);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -320,9 +320,19 @@ class _EditorViewState extends State<EditorView> {
|
||||
builder: (context, _) {
|
||||
if (c.buffers.isEmpty) {
|
||||
return ClidePaneChrome(
|
||||
title: 'editor',
|
||||
subtitle: 'no buffer · use `clide open <path>` 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 <path>` 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<String>(
|
||||
@@ -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(
|
||||
|
||||
@@ -105,7 +105,10 @@ class _FileTreeViewState extends State<FileTreeView> {
|
||||
}
|
||||
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<FileTreeView> {
|
||||
);
|
||||
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),
|
||||
);
|
||||
|
||||
@@ -95,11 +95,23 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -123,9 +123,17 @@ class _OpenFolderDialogState extends State<OpenFolderDialog> {
|
||||
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<OpenFolderDialog> {
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,10 @@ class _DockStatusItemState extends State<DockStatusItem> {
|
||||
: 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<DockStatusItem> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -78,18 +78,23 @@ class _OutputViewState extends State<OutputView> {
|
||||
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<OutputView> {
|
||||
);
|
||||
}
|
||||
|
||||
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<OutputView> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -51,14 +51,33 @@ class _SearchPanelViewState extends State<SearchPanelView> {
|
||||
final dialog = ClideKernel.of(context).dialog;
|
||||
if (!await c.isWorkingTreeClean()) {
|
||||
await dialog.show<Object>(
|
||||
(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<bool>(
|
||||
(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<SearchPanelView> {
|
||||
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<SearchPanelView> {
|
||||
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<SearchPanelView> {
|
||||
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<SearchPanelView> {
|
||||
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<SearchPanelView> {
|
||||
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<SearchTabMode> 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -140,14 +140,38 @@ class _TerminalPaneState extends State<TerminalPane> {
|
||||
|
||||
@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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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.<name>` in the builtin.vim catalog (T-462); the
|
||||
/// default keeps Vim's own vocabulary, but a locale may override it.
|
||||
final String label;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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 <path>` or pick a file in the tree" }
|
||||
"subtitle.no-buffer": { "translation": "no buffer · use `clide open <path>` or pick a file in the tree" },
|
||||
"a11y.text-area": { "translation": "editor text area" }
|
||||
}
|
||||
|
||||
@@ -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}" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"empty": { "translation": "Select a .md file to preview it here." },
|
||||
"chrome.title": { "translation": "viewer" },
|
||||
"subtitle.lines": { "translation": "{count} lines" }
|
||||
}
|
||||
@@ -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." }
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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." }
|
||||
}
|
||||
@@ -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}" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mode.normal": { "translation": "NORMAL" },
|
||||
"mode.insert": { "translation": "INSERT" },
|
||||
"mode.visual": { "translation": "VISUAL" }
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user