refactor(i18n): route framework + shared chrome through a 'core' catalog (T-469)

Framework strings outside any extension — widget primitives (collapser, toast,
lightbox, multitab, ex-line, spine, pane chrome), the shared reader chrome, the
markdown 'Open in editor' tooltip, and the drag-resize handle a11y labels — now
resolve under a new 'core' namespace (preloaded at boot). Settles the T-469
namespace question: framework chrome gets one 'core' catalog.

Makes ClideSettings.i18n.string/.interpolated null-safe (ClideKernel.maybeOf):
primitives render kernel-less in isolated tests, returning the placeholder —
matching the D-101 fallback contract for fonts. The markdown tooltip threads
via the ClideMarkdownHooks carrier like mono/ui; drag_resize reads the kernel
i18n directly to avoid a kernel→widgets layering inversion. No en_US change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 23:26:02 +02:00
co-authored by Claude Opus 4.8
parent 17bc907415
commit 33a8a74240
16 changed files with 212 additions and 49 deletions
+5
View File
@@ -345,6 +345,11 @@ class ClideKernel extends InheritedWidget {
return w.services;
}
/// Like [of] but returns null instead of throwing when there is no kernel in
/// scope — lets widget-facing facades (e.g. [ClideSettings.i18n]) degrade
/// gracefully so a primitive renders in an isolated test without a kernel.
static KernelServices? maybeOf(BuildContext context) => context.dependOnInheritedWidgetOfExactType<ClideKernel>()?.services;
@override
bool updateShouldNotify(ClideKernel oldWidget) => services != oldWidget.services;
}
@@ -0,0 +1,27 @@
{
"collapser.expand": { "translation": "Expand" },
"collapser.collapse": { "translation": "Collapse" },
"collapser.expanded": { "translation": "expanded" },
"collapser.collapsed": { "translation": "collapsed" },
"toast.dismiss": { "translation": "Dismiss notification" },
"lightbox.close": { "translation": "close" },
"lightbox.hint": { "translation": "scroll to zoom · double-click to reset · Esc to close" },
"tab.new": { "translation": "New tab" },
"exline.notCommand": { "translation": "Not an editor command" },
"spine.expandSuffix": { "translation": "click to expand" },
"pane.close": { "translation": "Close pane" },
"pane.header": { "translation": "pane header: {title}" },
"reader.back": { "translation": "Back" },
"reader.forward": { "translation": "Forward" },
"reader.jumpToPin": { "translation": "Jump to pin" },
"reader.edit": { "translation": "Edit in editor" },
"reader.pin": { "translation": "Pin" },
"reader.unpin": { "translation": "Unpin" },
"link.openInEditor": { "translation": "Open in editor" },
"resize.axis.width": { "translation": "width" },
"resize.axis.height": { "translation": "height" },
"resize.sidebar": { "translation": "Sidebar {axis}" },
"resize.contextPanel": { "translation": "Context panel {axis}" },
"resize.slot": { "translation": "{slot} {axis}" },
"resize.pixels": { "translation": "{n} pixels" }
}
+55 -9
View File
@@ -1,3 +1,5 @@
import 'package:clide/kernel/src/facade.dart' show ClideKernel;
import 'package:clide/kernel/src/i18n/i18n.dart' show I18nReplacer;
import 'package:clide/kernel/src/panels/arrangement.dart';
import 'package:clide/kernel/src/panels/slot_id.dart';
import 'package:clide/kernel/src/theme/controller.dart';
@@ -43,10 +45,10 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
return Semantics(
container: true,
slider: true,
label: _semanticLabel(),
value: size == null ? null : '${size.round()} pixels',
increasedValue: size == null ? null : '${(size + DragResizeHandle.stepFine).round()} pixels',
decreasedValue: size == null ? null : '${(size - DragResizeHandle.stepFine).round()} pixels',
label: _semanticLabel(context),
value: size == null ? null : _pixels(context, size.round()),
increasedValue: size == null ? null : _pixels(context, (size + DragResizeHandle.stepFine).round()),
decreasedValue: size == null ? null : _pixels(context, (size - DragResizeHandle.stepFine).round()),
onIncrease: () => _bump(DragResizeHandle.stepFine),
onDecrease: () => _bump(-DragResizeHandle.stepFine),
child: FocusableActionDetector(
@@ -87,11 +89,55 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
);
}
String _semanticLabel() {
final axis = widget.axis == Axis.horizontal ? 'width' : 'height';
if (widget.slot == Slots.sidebar) return 'Sidebar $axis';
if (widget.slot == Slots.contextPanel) return 'Context panel $axis';
return '${widget.slot.value} $axis';
/// The slider's accessible name (D-21, `core` namespace). With no kernel in
/// scope (isolated tests) the i18n lookups fall back to the English
/// placeholders, mirroring the widget-facing facade's degrade behaviour.
String _semanticLabel(BuildContext context) {
final i18n = ClideKernel.maybeOf(context)?.i18n;
final axis = widget.axis == Axis.horizontal
? (i18n?.string('resize.axis.width', namespace: 'core', placeholder: 'width') ?? 'width')
: (i18n?.string('resize.axis.height', namespace: 'core', placeholder: 'height') ?? 'height');
if (widget.slot == Slots.sidebar) {
return i18n?.interpolated(
'resize.sidebar',
namespace: 'core',
placeholder: 'Sidebar {axis}',
replacers: [I18nReplacer(from: '{axis}', replace: axis)],
) ??
'Sidebar $axis';
}
if (widget.slot == Slots.contextPanel) {
return i18n?.interpolated(
'resize.contextPanel',
namespace: 'core',
placeholder: 'Context panel {axis}',
replacers: [I18nReplacer(from: '{axis}', replace: axis)],
) ??
'Context panel $axis';
}
return i18n?.interpolated(
'resize.slot',
namespace: 'core',
placeholder: '{slot} {axis}',
replacers: [
I18nReplacer(from: '{slot}', replace: widget.slot.value),
I18nReplacer(from: '{axis}', replace: axis),
],
) ??
'${widget.slot.value} $axis';
}
/// `'{n} pixels'` via the `core` catalog (falls back to English with no
/// kernel in scope).
String _pixels(BuildContext context, int n) {
final i18n = ClideKernel.maybeOf(context)?.i18n;
return i18n?.interpolated(
'resize.pixels',
namespace: 'core',
placeholder: '{n} pixels',
replacers: [I18nReplacer(from: '{n}', replace: '$n')],
) ??
'$n pixels';
}
Map<ShortcutActivator, Intent> _shortcuts() {