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
+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() {