diff --git a/lib/src/flow_analyzer.dart b/lib/src/flow_analyzer.dart index c7932e3..05dcd6e 100644 --- a/lib/src/flow_analyzer.dart +++ b/lib/src/flow_analyzer.dart @@ -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 _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 ` 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), ), ]; } diff --git a/lib/src/flow_editor_page.dart b/lib/src/flow_editor_page.dart index 6ac08b8..45feef5 100644 --- a/lib/src/flow_editor_page.dart +++ b/lib/src/flow_editor_page.dart @@ -137,6 +137,7 @@ class _FlowEditorPageState extends State _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 _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 _controller.codeController.setCapabilityProviders( available: () => newCaps, store: () => widget.storeCapabilities, + strings: AnalyzerStrings.from(_l), ); await _controller.codeController.reanalyze(); } @@ -217,6 +220,7 @@ class _FlowEditorPageState extends State _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 _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 " 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 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 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 = []; - 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 fixes; final Color tone; + final FlowEditorStrings strings; final Map busy; final Future 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 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, diff --git a/lib/src/flow_yaml_controller.dart b/lib/src/flow_yaml_controller.dart index 734ac2d..94f81bd 100644 --- a/lib/src/flow_yaml_controller.dart +++ b/lib/src/flow_yaml_controller.dart @@ -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 Function()? availableCapabilities, List Function()? storeCapabilities, + AnalyzerStrings analyzerStrings = AnalyzerStrings.english, }) : super( text: '', language: yaml, @@ -37,20 +39,23 @@ class FlowYamlCodeController extends CodeController { availableCapabilities: availableCapabilities ?? (() => const []), 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 Function() available, List Function()? store, + AnalyzerStrings? strings, }) { analyzer = FlowAnalyzer( availableCapabilities: available, storeCapabilities: store, + strings: strings ?? AnalyzerStrings.english, ); } diff --git a/lib/src/l10n.dart b/lib/src/l10n.dart index 6a0a3ce..201fc23 100644 --- a/lib/src/l10n.dart +++ b/lib/src/l10n.dart @@ -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 ` or check the spelling.', + 'Unbekannte Capability "$cap". ' + 'Nicht im Store — lokal mit ' + '`fai install --link ` 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 ` 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"'; } diff --git a/lib/src/widgets/run_tab.dart b/lib/src/widgets/run_tab.dart index 9ff37f3..7e496ee 100644 --- a/lib/src/widgets/run_tab.dart +++ b/lib/src/widgets/run_tab.dart @@ -183,7 +183,10 @@ class _RunTabState extends State { 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 { } 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 { 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 { 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 { 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 { 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, ), diff --git a/pubspec.yaml b/pubspec.yaml index 9697081..e77161c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fai_studio_flow_editor description: Swappable inline YAML editor for F∆I Studio flows. -version: 0.19.0 +version: 0.20.0 publish_to: 'none' repository: https://git.flemming.ai/fai/studio-flow-editor