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

@ -20,6 +20,7 @@
import 'package:flutter_code_editor/flutter_code_editor.dart';
import 'package:yaml/yaml.dart';
import 'l10n.dart';
import 'quick_fix.dart';
import 'wire_colors.dart';
@ -59,9 +60,15 @@ class FlowAnalyzer extends AbstractAnalyzer {
/// [kOutputsNodeId] sentinels.
final Map<String, IssueType> _stepSeverity = {};
/// Localised string builders the analyzer pipes its messages
/// through. Default is English so the analyzer behaves the
/// same in tests + host-less callers.
final AnalyzerStrings strings;
FlowAnalyzer({
required this.availableCapabilities,
this.storeCapabilities,
this.strings = AnalyzerStrings.english,
});
/// Read-only view of the fixes computed during the last
@ -115,14 +122,18 @@ class FlowAnalyzer extends AbstractAnalyzer {
issues.add(
Issue(
line: e.span?.start.line ?? 0,
message: 'YAML: ${e.message}',
message: strings.yamlError(e.message),
type: IssueType.error,
),
);
return AnalysisResult(issues: issues);
} catch (e) {
issues.add(
Issue(line: 0, message: 'YAML: $e', type: IssueType.error),
Issue(
line: 0,
message: strings.yamlError(e.toString()),
type: IssueType.error,
),
);
return AnalysisResult(issues: issues);
}
@ -159,16 +170,12 @@ class FlowAnalyzer extends AbstractAnalyzer {
// Tailor the message: "unknown" reads identical in
// both cases, but the recovery line nudges toward
// the right action.
// the right action. Localised via [strings].
final message = inStore
? 'Unknown capability "$useValue". '
'Install via the Fix button or check the spelling.'
? strings.unknownCapInStore(useValue)
: didYouMean != null
? 'Unknown capability "$useValue". '
'Did you mean "$didYouMean"?'
: 'Unknown capability "$useValue". '
'Not in the store — install locally with '
'`fai install --link <path>` or check the spelling.';
? strings.unknownCapTypo(useValue, didYouMean)
: strings.unknownCapNotInStore(useValue);
final issue = Issue(
line: issueLine,
message: message,
@ -192,7 +199,7 @@ class FlowAnalyzer extends AbstractAnalyzer {
ReplaceLineValueFix(
line: issueLine,
replacement: fullReplacement,
label: 'Use "$fullReplacement"',
label: strings.fixUseInstead(fullReplacement),
),
);
}
@ -200,20 +207,14 @@ class FlowAnalyzer extends AbstractAnalyzer {
fixes.add(
InstallCapabilityFix(
capability: useValue,
label: 'Install $useValue',
label: strings.fixInstallCap(useValue),
),
);
} else if (didYouMean == null) {
// Not in store + no near-miss spelling give the
// operator a path to register the module they
// actually have (local clone, internal URL, ).
// Hidden behind the "Did you mean" suggestion when
// present so the suggested fix stays the primary
// action.
fixes.add(
AddModuleSourceFix(
capability: useValue,
label: 'Add source for $useValue',
label: strings.fixAddSource(useValue),
),
);
}
@ -226,13 +227,13 @@ class FlowAnalyzer extends AbstractAnalyzer {
_checkFieldTypes(
doc['inputs'],
'input',
strings.inputKind(),
issues,
nodeId: kInputsNodeId,
);
_checkFieldTypes(
doc['outputs'],
'output',
strings.outputKind(),
issues,
nodeId: kOutputsNodeId,
);
@ -278,23 +279,22 @@ class FlowAnalyzer extends AbstractAnalyzer {
if (!kKnownTypes.contains(value)) {
final issue = Issue(
line: entry.value.span.start.line,
message:
'Unknown $kind type "$value". '
'Use one of: ${kKnownTypes.join(", ")}.',
message: strings.unknownType(
kind,
value,
kKnownTypes.join(", "),
),
type: IssueType.warning,
);
out.add(issue);
_bumpSeverity(nodeId, IssueType.warning);
// If the typo is within edit-distance two of a known
// type token, offer a one-click replace as the primary
// fix. The strip and tooltip render this as a button.
final closest = _closestKnownType(value);
if (closest != null) {
_fixesByIssue[issue] = [
ReplaceLineValueFix(
line: entry.value.span.start.line,
replacement: closest,
label: 'Change to "$closest"',
label: strings.fixChangeTo(closest),
),
];
}

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,

View file

@ -23,6 +23,7 @@ import 'package:flutter_code_editor/flutter_code_editor.dart';
import 'package:highlight/languages/yaml.dart';
import 'flow_analyzer.dart';
import 'l10n.dart';
import 'quick_fix.dart';
import 'wire_colors.dart';
@ -30,6 +31,7 @@ class FlowYamlCodeController extends CodeController {
FlowYamlCodeController({
List<String> Function()? availableCapabilities,
List<String> Function()? storeCapabilities,
AnalyzerStrings analyzerStrings = AnalyzerStrings.english,
}) : super(
text: '',
language: yaml,
@ -37,20 +39,23 @@ class FlowYamlCodeController extends CodeController {
availableCapabilities:
availableCapabilities ?? (() => const <String>[]),
storeCapabilities: storeCapabilities,
strings: analyzerStrings,
),
);
/// Set both capability providers in one go. Re-creating the
/// analyzer is the simplest way to flip both closures without
/// risking a stale storeCapabilities reference inside an old
/// FlowAnalyzer instance.
/// Set capability providers AND the analyzer's locale-aware
/// message strings. Re-creating the analyzer is the simplest
/// way to flip these atomically without risking a stale
/// closure or stale strings inside an old FlowAnalyzer.
void setCapabilityProviders({
required List<String> Function() available,
List<String> Function()? store,
AnalyzerStrings? strings,
}) {
analyzer = FlowAnalyzer(
availableCapabilities: available,
storeCapabilities: store,
strings: strings ?? AnalyzerStrings.english,
);
}

View file

@ -145,4 +145,161 @@ class FlowEditorStrings {
String get endpointExpression => _t('Expression', 'Ausdruck');
String get endpointAdd => _t('Add', 'Hinzufügen');
String get endpointRemove => _t('Remove', 'Entfernen');
// Run-tab step row + error block.
String get runAwaitingApproval =>
_t('awaiting approval', 'wartet auf Freigabe');
String get runCopy => _t('Copy', 'Kopieren');
String get runCopied => _t('Copied', 'Kopiert');
String runErrorsBlockRun(int n) => _t(
'$n error${n == 1 ? '' : 's'} prevent the run',
'$n ${n == 1 ? 'Fehler verhindert' : 'Fehler verhindern'} den Lauf',
);
String get runErrorsBlockHint => _t(
'check the diagnostic strip',
'siehe Diagnose-Leiste unten',
);
String runErrorsBlockTooltip(int n) => _t(
'$n error${n == 1 ? '' : 's'} prevent the run — fix them first',
'$n ${n == 1 ? 'Fehler verhindert' : 'Fehler verhindern'} den Lauf — '
'bitte zuerst beheben',
);
// Bottom diagnostic strip + hover card.
String get diagnosticNoIssues => _t('No issues', 'Keine Probleme');
String diagnosticErrors(int n) =>
_t('$n error${n == 1 ? '' : 's'}', '$n Fehler');
String diagnosticWarnings(int n) =>
_t('$n warning${n == 1 ? '' : 's'}', '$n Warnungen');
String get diagnosticCopyAll => _t('Copy all', 'Alle kopieren');
String diagnosticLinePrefix(int line) => 'L$line';
// Analyzer-emitted messages used by FlowAnalyzer via the
// [AnalyzerStrings] adapter below so the same strings can be
// localized centrally.
String analyzerUnknownCapInStore(String cap) => _t(
'Unknown capability "$cap". '
'Install via the Fix button or check the spelling.',
'Unbekannte Capability "$cap". '
'Per Fix-Button installieren oder Tippfehler prüfen.',
);
String analyzerUnknownCapTypo(String cap, String suggestion) => _t(
'Unknown capability "$cap". Did you mean "$suggestion"?',
'Unbekannte Capability "$cap". Meintest du "$suggestion"?',
);
String analyzerUnknownCapNotInStore(String cap) => _t(
'Unknown capability "$cap". '
'Not in the store — install locally with '
'`fai install --link <path>` or check the spelling.',
'Unbekannte Capability "$cap". '
'Nicht im Store — lokal mit '
'`fai install --link <pfad>` installieren oder Tippfehler prüfen.',
);
String analyzerInputKind() => _t('input', 'Eingabe');
String analyzerOutputKind() => _t('output', 'Ausgabe');
String analyzerUnknownType(String kind, String value, String validList) =>
_t(
'Unknown $kind type "$value". Use one of: $validList.',
'Unbekannter $kind-Typ "$value". Erlaubt sind: $validList.',
);
String analyzerYamlError(String detail) => _t('YAML: $detail', 'YAML: $detail');
// Quick-fix button labels.
String fixInstallCap(String cap) => _t('Install $cap', '$cap installieren');
String fixAddSource(String cap) =>
_t('Add source for $cap', 'Quelle für $cap hinzufügen…');
String fixUseInstead(String name) =>
_t('Use "$name"', '"$name" verwenden');
String fixChangeTo(String name) =>
_t('Change to "$name"', 'Auf "$name" ändern');
}
/// Subset of [FlowEditorStrings] the analyzer consumes. Kept as
/// a separate adapter so a test or alternate host can plug in
/// a custom string-builder without owning the whole Editor
/// string table.
class AnalyzerStrings {
final String Function(String cap) unknownCapInStore;
final String Function(String cap, String suggestion) unknownCapTypo;
final String Function(String cap) unknownCapNotInStore;
final String Function(String kind, String value, String validList)
unknownType;
final String Function() inputKind;
final String Function() outputKind;
final String Function(String detail) yamlError;
final String Function(String cap) fixInstallCap;
final String Function(String cap) fixAddSource;
final String Function(String name) fixUseInstead;
final String Function(String name) fixChangeTo;
const AnalyzerStrings({
required this.unknownCapInStore,
required this.unknownCapTypo,
required this.unknownCapNotInStore,
required this.unknownType,
required this.inputKind,
required this.outputKind,
required this.yamlError,
required this.fixInstallCap,
required this.fixAddSource,
required this.fixUseInstead,
required this.fixChangeTo,
});
/// Build an [AnalyzerStrings] from the editor's
/// [FlowEditorStrings] every host that uses the editor
/// gets analyzer messages in the same language as the rest
/// of the UI for free.
factory AnalyzerStrings.from(FlowEditorStrings s) => AnalyzerStrings(
unknownCapInStore: s.analyzerUnknownCapInStore,
unknownCapTypo: s.analyzerUnknownCapTypo,
unknownCapNotInStore: s.analyzerUnknownCapNotInStore,
unknownType: s.analyzerUnknownType,
inputKind: s.analyzerInputKind,
outputKind: s.analyzerOutputKind,
yamlError: s.analyzerYamlError,
fixInstallCap: s.fixInstallCap,
fixAddSource: s.fixAddSource,
fixUseInstead: s.fixUseInstead,
fixChangeTo: s.fixChangeTo,
);
/// Built-in English defaults used by tests + by the
/// editor when no host has wired strings yet. Identical to
/// FlowEditorStrings(en) output so behaviour matches across
/// the two code paths.
static const AnalyzerStrings english = AnalyzerStrings._english();
const AnalyzerStrings._english()
: unknownCapInStore = _enUnknownCapInStore,
unknownCapTypo = _enUnknownCapTypo,
unknownCapNotInStore = _enUnknownCapNotInStore,
unknownType = _enUnknownType,
inputKind = _enInputKind,
outputKind = _enOutputKind,
yamlError = _enYamlError,
fixInstallCap = _enFixInstall,
fixAddSource = _enFixAddSource,
fixUseInstead = _enFixUse,
fixChangeTo = _enFixChange;
static String _enUnknownCapInStore(String cap) =>
'Unknown capability "$cap". '
'Install via the Fix button or check the spelling.';
static String _enUnknownCapTypo(String cap, String suggestion) =>
'Unknown capability "$cap". Did you mean "$suggestion"?';
static String _enUnknownCapNotInStore(String cap) =>
'Unknown capability "$cap". '
'Not in the store — install locally with '
'`fai install --link <path>` or check the spelling.';
static String _enUnknownType(String kind, String value, String validList) =>
'Unknown $kind type "$value". Use one of: $validList.';
static String _enInputKind() => 'input';
static String _enOutputKind() => 'output';
static String _enYamlError(String detail) => 'YAML: $detail';
static String _enFixInstall(String cap) => 'Install $cap';
static String _enFixAddSource(String cap) => 'Add source for $cap';
static String _enFixUse(String name) => 'Use "$name"';
static String _enFixChange(String name) => 'Change to "$name"';
}

View file

@ -183,7 +183,10 @@ class _RunTabState extends State<RunTab> {
if (_error != null) ...[
const SizedBox(height: FaiSpace.lg),
_section(theme, strings.runTitleFailed),
_CopyableErrorBox(text: _error.toString()),
_CopyableErrorBox(
text: _error.toString(),
strings: widget.strings,
),
],
],
),
@ -300,6 +303,7 @@ class _RunTabState extends State<RunTab> {
}
Widget _stepRow(ThemeData theme, String id, _StepState state) {
final strings = widget.strings;
final IconData glyph;
final Color color;
String suffix = '';
@ -317,7 +321,7 @@ class _RunTabState extends State<RunTab> {
case _StepKind.awaiting:
glyph = Icons.pause_circle_outline;
color = theme.colorScheme.tertiary;
suffix = ' awaiting approval';
suffix = ' ${strings.runAwaitingApproval}';
}
final isError = state.kind == _StepKind.error &&
state.error != null &&
@ -347,7 +351,10 @@ class _RunTabState extends State<RunTab> {
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.only(left: 24),
child: _CopyableErrorBox(text: state.error!),
child: _CopyableErrorBox(
text: state.error!,
strings: widget.strings,
),
),
],
],
@ -365,9 +372,7 @@ class _RunTabState extends State<RunTab> {
return Row(
children: [
Tooltip(
message: blocked
? '$errors error${errors == 1 ? '' : 's'} prevent the run'
: '',
message: blocked ? strings.runErrorsBlockTooltip(errors) : '',
child: FilledButton.icon(
onPressed: _running || widget.driver == null || blocked
? null
@ -389,8 +394,8 @@ class _RunTabState extends State<RunTab> {
const SizedBox(width: FaiSpace.sm),
Flexible(
child: Text(
'$errors error${errors == 1 ? '' : 's'} prevent the run · '
'check the diagnostic strip',
'${strings.runErrorsBlockRun(errors)} · '
'${strings.runErrorsBlockHint}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
@ -619,7 +624,8 @@ class _StepState {
/// so flow_editor doesn't depend on Studio internals.
class _CopyableErrorBox extends StatefulWidget {
final String text;
const _CopyableErrorBox({required this.text});
final FlowEditorStrings strings;
const _CopyableErrorBox({required this.text, required this.strings});
@override
State<_CopyableErrorBox> createState() => _CopyableErrorBoxState();
@ -661,7 +667,8 @@ class _CopyableErrorBoxState extends State<_CopyableErrorBox> {
Align(
alignment: Alignment.centerRight,
child: Tooltip(
message: _justCopied ? 'Copied' : 'Copy',
message:
_justCopied ? widget.strings.runCopied : widget.strings.runCopy,
child: IconButton(
icon: Icon(
_justCopied ? Icons.check : Icons.content_copy,
@ -682,6 +689,12 @@ class _CopyableErrorBoxState extends State<_CopyableErrorBox> {
widget.text,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontFamilyFallback: const [
'Menlo',
'Consolas',
'Courier New',
'monospace',
],
fontSize: 12,
color: theme.colorScheme.onErrorContainer,
),