feat(editor): localised analyzer/strip/run messages + monospace font fallback

Closes operator-reported issues around the text-tab font and
the English-only run + diagnostic surfaces.

  - **Real monospace everywhere**. The package never bundled
    JetBrains Mono as an asset (it relied on Studio's
    google_fonts pre-load), so a host that doesn't ship the
    font saw the YAML editor render in the system proportional
    default. New _monoTextStyle() pins fontFamilyFallback to a
    cross-platform monospace chain (Menlo / Consolas / Courier
    New / monospace) so the editor stays a grid in every host.
    Applied to the code field, gutter, diagnostic strip,
    issue rows, hover card, fix buttons, and the run tab's
    error box.

  - **Locale-aware analyzer messages**. New AnalyzerStrings
    adapter holds every string the analyzer emits, with an
    .english default + an .from(FlowEditorStrings) factory.
    FlowYamlCodeController.setCapabilityProviders takes the
    strings; FlowEditorPage wires them from the active locale.
    The analyzer's 'Unknown capability', 'Did you mean',
    'Not in the store — install locally with fai install
    --link', 'Unknown input/output type', YAML parse errors,
    and every quick-fix label (Install / Add source / Use /
    Change to) now flip to DE when the editor's locale is DE.

  - **Localised run + diagnostic chrome**. The bottom strip's
    'No issues', '2 errors · 1 warning', 'Copy all'; the
    issue row's L-prefix + Copy tooltip; the hover card's
    L-prefix + Copy tooltip; the run-block tooltip + inline
    message ('2 Fehler verhindern den Lauf · siehe
    Diagnose-Leiste unten'); the step-row 'awaiting approval'
    suffix; the CopyableErrorBox's Copy / Copied tooltip —
    all now flow through FlowEditorStrings.

Bumped to 0.20.0.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-09 01:36:39 +02:00
parent b6bb8741a9
commit 5cd2745db5
6 changed files with 306 additions and 97 deletions

View file

@ -137,6 +137,7 @@ class _FlowEditorPageState extends State<FlowEditorPage>
_controller.codeController.setCapabilityProviders(
available: () => widget.availableCapabilities,
store: () => widget.storeCapabilities,
strings: AnalyzerStrings.from(_l),
);
_controller.addListener(_onCtrlChanged);
_controller.codeController.hoverRequest.addListener(_onHoverChanged);
@ -175,6 +176,7 @@ class _FlowEditorPageState extends State<FlowEditorPage>
_hoverEntry = OverlayEntry(
builder: (ctx) => _IssueHoverCard(
request: req,
strings: _l,
onApplyFix: _applyQuickFix,
onEnter: _controller.codeController.cancelHoverDismiss,
onExit: _controller.codeController.scheduleHoverDismiss,
@ -205,6 +207,7 @@ class _FlowEditorPageState extends State<FlowEditorPage>
_controller.codeController.setCapabilityProviders(
available: () => newCaps,
store: () => widget.storeCapabilities,
strings: AnalyzerStrings.from(_l),
);
await _controller.codeController.reanalyze();
}
@ -217,6 +220,7 @@ class _FlowEditorPageState extends State<FlowEditorPage>
_controller.codeController.setCapabilityProviders(
available: () => newCaps,
store: () => widget.storeCapabilities,
strings: AnalyzerStrings.from(_l),
);
await _controller.codeController.reanalyze();
}
@ -578,6 +582,7 @@ outputs:
// the message + quick-fix buttons.
_DiagnosticStrip(
controller: _controller.codeController,
strings: _l,
onApplyFix: _applyQuickFix,
),
],
@ -719,9 +724,15 @@ outputs:
}
Widget _textTab(ThemeData theme) {
final mono = TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 13,
// The editor itself doesn't bundle JetBrains Mono as an
// asset (Studio preloads it via google_fonts). When the
// host isn't Studio, Flutter would fall back to a
// proportional default and the code would line up like
// ransom-note prose. _monoTextStyle's fontFamilyFallback
// pins the chain to the system monospace stack so a YAML
// grid stays a grid in every host.
final mono = _monoTextStyle(
size: 13,
height: 1.45,
color: theme.colorScheme.onSurface,
);
@ -757,11 +768,7 @@ outputs:
Map<String, TextStyle> _yamlStyle(ThemeData theme) {
final cs = theme.colorScheme;
final isDark = theme.brightness == Brightness.dark;
final monoBase = TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 13,
height: 1.45,
);
final monoBase = _monoTextStyle(size: 13, height: 1.45);
// Pick clearly-distinguished hues so keys, strings,
// numbers, anchors, and comments don't blur into each
// other. We derive accents from the active ColorScheme
@ -1002,8 +1009,7 @@ class _TabActionStrip extends StatelessWidget {
const SizedBox(width: FaiSpace.sm),
Tooltip(
message: errorCount > 0
? '$errorCount error${errorCount == 1 ? '' : 's'} '
'prevent the run — fix them first'
? strings.runErrorsBlockTooltip(errorCount)
: '',
child: FilledButton.icon(
onPressed: onRun,
@ -1277,11 +1283,44 @@ class _NewFlowDialogState extends State<_NewFlowDialog> {
/// the trackpad-only operator. When the analyzer attaches a
/// `QuickFix` to an issue, the row also renders an action
/// button (e.g. "Install <cap>" or "Change to \"bytes\"").
/// Build a TextStyle that prefers `JetBrains Mono` (Studio
/// preloads it via `google_fonts`) but falls back to the
/// system's generic `monospace` family when the bundled font
/// isn't registered — which is what happens when this package
/// is hosted by something other than Studio. Without the
/// fallback the field rendered with the default proportional
/// font and code lined up like ransom-note prose.
TextStyle _monoTextStyle({
required double size,
Color? color,
FontWeight? weight,
double? letterSpacing,
double? height,
}) {
return TextStyle(
fontFamily: 'JetBrains Mono',
fontFamilyFallback: const [
'JetBrainsMono Nerd Font',
'Menlo',
'Consolas',
'Courier New',
'monospace',
],
fontSize: size,
color: color,
fontWeight: weight,
letterSpacing: letterSpacing,
height: height,
);
}
class _DiagnosticStrip extends StatefulWidget {
final FlowYamlCodeController controller;
final FlowEditorStrings strings;
final Future<void> Function(QuickFix) onApplyFix;
const _DiagnosticStrip({
required this.controller,
required this.strings,
required this.onApplyFix,
});
@ -1324,7 +1363,9 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
String _formatAll(List<Issue> issues) {
final buf = StringBuffer();
for (final i in issues) {
buf.writeln('L${i.line + 1}: ${i.message}');
buf.writeln(
'${widget.strings.diagnosticLinePrefix(i.line + 1)}: ${i.message}',
);
}
return buf.toString().trimRight();
}
@ -1354,12 +1395,11 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
),
),
child: Text(
'No issues',
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 10,
letterSpacing: 0.4,
widget.strings.diagnosticNoIssues,
style: _monoTextStyle(
size: 10,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
letterSpacing: 0.4,
),
),
);
@ -1402,27 +1442,25 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
const SizedBox(width: 8),
Text(
_summary(errorCount, warnCount),
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
fontWeight: FontWeight.w600,
style: _monoTextStyle(
size: 11,
weight: FontWeight.w600,
color: tone,
),
),
const SizedBox(width: 8),
Expanded(
child: SelectableText(
'L${issues.first.line + 1}: ${issues.first.message}',
'${widget.strings.diagnosticLinePrefix(issues.first.line + 1)}: ${issues.first.message}',
maxLines: 1,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
style: _monoTextStyle(
size: 11,
color: theme.colorScheme.onSurface,
),
),
),
IconButton(
tooltip: 'Copy all',
tooltip: widget.strings.diagnosticCopyAll,
icon: const Icon(Icons.content_copy, size: 14),
visualDensity: VisualDensity.compact,
onPressed: () => _copy(_formatAll(issues)),
@ -1453,10 +1491,12 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
issue: issue,
fixes: widget.controller.fixesFor(issue),
tone: _toneForIssue(issue.type, theme),
strings: widget.strings,
busy: _busy,
onApplyFix: _runFix,
onCopy: () =>
_copy('L${issue.line + 1}: ${issue.message}'),
onCopy: () => _copy(
'${widget.strings.diagnosticLinePrefix(issue.line + 1)}: ${issue.message}',
),
),
],
),
@ -1471,9 +1511,9 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
String _summary(int errors, int warnings) {
final parts = <String>[];
if (errors > 0) parts.add('$errors error${errors == 1 ? '' : 's'}');
if (errors > 0) parts.add(widget.strings.diagnosticErrors(errors));
if (warnings > 0) {
parts.add('$warnings warning${warnings == 1 ? '' : 's'}');
parts.add(widget.strings.diagnosticWarnings(warnings));
}
return parts.join(' · ');
}
@ -1487,6 +1527,7 @@ class _IssueRow extends StatelessWidget {
final Issue issue;
final List<QuickFix> fixes;
final Color tone;
final FlowEditorStrings strings;
final Map<QuickFix, bool> busy;
final Future<void> Function(QuickFix) onApplyFix;
final VoidCallback onCopy;
@ -1495,6 +1536,7 @@ class _IssueRow extends StatelessWidget {
required this.issue,
required this.fixes,
required this.tone,
required this.strings,
required this.busy,
required this.onApplyFix,
required this.onCopy,
@ -1517,10 +1559,9 @@ class _IssueRow extends StatelessWidget {
SizedBox(
width: 42,
child: Text(
'L${issue.line + 1}',
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
strings.diagnosticLinePrefix(issue.line + 1),
style: _monoTextStyle(
size: 11,
color: theme.colorScheme.onSurfaceVariant,
),
),
@ -1528,16 +1569,15 @@ class _IssueRow extends StatelessWidget {
Expanded(
child: SelectableText(
issue.message,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
style: _monoTextStyle(
size: 11,
color: theme.colorScheme.onSurface,
),
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Copy',
tooltip: strings.runCopy,
icon: const Icon(Icons.content_copy, size: 13),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
@ -1557,10 +1597,7 @@ class _IssueRow extends StatelessWidget {
: const Icon(Icons.auto_fix_high, size: 13),
label: Text(
fix.label,
style: const TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
),
style: _monoTextStyle(size: 11),
),
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,
@ -1586,12 +1623,14 @@ class _IssueRow extends StatelessWidget {
/// tooltip so action buttons stay clickable.
class _IssueHoverCard extends StatelessWidget {
final IssueHoverRequest request;
final FlowEditorStrings strings;
final Future<void> Function(QuickFix) onApplyFix;
final VoidCallback onEnter;
final VoidCallback onExit;
const _IssueHoverCard({
required this.request,
required this.strings,
required this.onApplyFix,
required this.onEnter,
required this.onExit,
@ -1666,10 +1705,9 @@ class _IssueHoverCard extends StatelessWidget {
),
const SizedBox(width: 8),
Text(
'L${request.line + 1}',
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
strings.diagnosticLinePrefix(request.line + 1),
style: _monoTextStyle(
size: 11,
color: theme.colorScheme.onSurfaceVariant,
),
),
@ -1677,22 +1715,21 @@ class _IssueHoverCard extends StatelessWidget {
Expanded(
child: SelectableText(
request.message,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 12,
style: _monoTextStyle(
size: 12,
color: theme.colorScheme.onSurface,
),
),
),
IconButton(
tooltip: 'Copy',
tooltip: strings.runCopy,
icon: const Icon(Icons.content_copy, size: 14),
visualDensity: VisualDensity.compact,
onPressed: () {
Clipboard.setData(
ClipboardData(
text:
'L${request.line + 1}: ${request.message}',
'${strings.diagnosticLinePrefix(request.line + 1)}: ${request.message}',
),
);
},
@ -1711,10 +1748,7 @@ class _IssueHoverCard extends StatelessWidget {
icon: const Icon(Icons.auto_fix_high, size: 13),
label: Text(
fix.label,
style: const TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
),
style: _monoTextStyle(size: 11),
),
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,