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:
parent
b6bb8741a9
commit
5cd2745db5
6 changed files with 306 additions and 97 deletions
|
|
@ -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),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue