diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ce67195..07c6444 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -61,6 +61,9 @@ "@storeNResults": { "placeholders": { "n": { "type": "int" } } }, + "storeFederationNudgeTitle": "Mehr Capabilities?", + "storeFederationNudgeBody": "MCP-Server oder n8n-Endpunkt unter Einstellungen → MCP-Clients / N8N-Endpunkte konfigurieren. Entdeckte Tools und Workflows tauchen hier als `mcp..`- und `n8n..`-Capabilities auf — jeder Anthropic-/Community-MCP-Server bringt mehrere auf einmal.", + "storeFederationNudgeButton": "Einstellungen öffnen", "auditTitle": "Protokoll", "auditNoEvents": "Noch keine Ereignisse", @@ -68,7 +71,32 @@ "modulesTitle": "Module", "modulesNoneTitle": "Noch keine Module", + "modulesNoneHint": "`fai install ` ausführen oder ~/.fai/modules/ prüfen.", + "modulesReloadTooltip": "Aktualisieren", "modulesRecentActivity": "LETZTE AKTIVITÄT", + "modulesRecentActivityHint": "letzte {n} install/uninstall-Ereignisse aus dem Audit-Log", + "@modulesRecentActivityHint": { "placeholders": { "n": { "type": "int" } } }, + "modulesActivityInstalled": "installiert", + "modulesActivityUninstalled": "deinstalliert", + "modulesCapabilitiesLabel": "Capabilities", + "modulesUninstallTitle": "Modul deinstallieren?", + "modulesUninstallBody": "Entfernt {name} v{version} von diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl. Ein `module.uninstalled`-Audit-Event wird festgehalten.", + "@modulesUninstallBody": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "modulesUninstalledToast": "{name} v{version} deinstalliert.", + "@modulesUninstalledToast": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "modulesUninstallFailed": "Deinstallation fehlgeschlagen: {error}", + "@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } }, + "modulesUninstalling": "Wird deinstalliert…", "approvalsTitle": "Freigaben", "approvalsTabPending": "Offen", @@ -77,6 +105,73 @@ "approvalsInboxHint": "Alles erledigt. Neue `system.approval@^0`-Schritte landen automatisch hier.", "doctorTitle": "Diagnose", + "doctorRecheckTooltip": "Erneut prüfen", + "doctorEventLogSection": "Audit-Log", + "doctorModulesApprovalsSection": "Module & Freigaben", + "doctorHostServicesSection": "Host-Dienste", + "doctorDaemonFilesSection": "Daemon-Dateien", + "doctorDaemonControlSection": "Daemon-Steuerung", + "doctorChainIntact": "Hash-Chain intakt", + "doctorChainIntactDetail": "{n} Ereignisse · prev_event_sha256 lückenlos verifiziert", + "@doctorChainIntactDetail": { "placeholders": { "n": { "type": "int" } } }, + "doctorChainTampered": "Manipulation erkannt", + "doctorChainTamperedDetail": "{verified} von {total} verifiziert vor Mismatch bei {id}", + "@doctorChainTamperedDetail": { + "placeholders": { + "verified": { "type": "int" }, + "total": { "type": "int" }, + "id": { "type": "String" } + } + }, + "doctorChainPillOk": "WORM-1", + "doctorChainPillTamper": "MANIPULIERT", + "doctorVerifyNow": "Jetzt prüfen", + "doctorRestart": "Neustart", + "doctorStart": "Starten", + "doctorStop": "Stoppen", + "doctorStatusAction": "Status", + "doctorRunningOn": "Läuft auf {name}: {endpoint}", + "@doctorRunningOn": { + "placeholders": { + "name": { "type": "String" }, + "endpoint": { "type": "String" } + } + }, + "doctorStoppedOn": "{name}-Daemon gestoppt", + "@doctorStoppedOn": { "placeholders": { "name": { "type": "String" } } }, + "doctorStatusUnknown": "Daemon-Status: …", + "doctorPillRunning": "läuft", + "doctorPillStopped": "gestoppt", + "doctorDaemonControlHint": "Studio ruft das Platform-Binary auf — entspricht `fai daemon …`, sodass CLI und UI im Gleichschritt bleiben.", + "doctorDaemonControlWorking": "Wird ausgeführt…", + "doctorPathLog": "Log", + "doctorPathConfig": "Konfig", + "doctorPathDb": "Audit-DB", + "doctorPathModules": "Module-Verzeichnis", + "doctorPathFlows": "Flow-Verzeichnis", + "doctorPathPid": "PID-Datei", + "doctorPathsEmpty": "Daemon hat keine Datei-Pfade gemeldet. Hub aktualisieren via `fai daemon restart`.", + "doctorOpenError": "Konnte nicht öffnen: {detail}", + "@doctorOpenError": { "placeholders": { "detail": { "type": "String" } } }, + "doctorUpdateAvailable": "Update verfügbar — {version}", + "@doctorUpdateAvailable": { "placeholders": { "version": { "type": "String" } } }, + "doctorUpdateBody": "Kanal {channel} bietet jetzt {version}. Per Button anwenden — Studio ruft `fai update apply --channel {channel}` auf.", + "@doctorUpdateBody": { + "placeholders": { + "channel": { "type": "String" }, + "version": { "type": "String" } + } + }, + "doctorUpdateUnreachable": "Release-Host nicht erreichbar", + "doctorUpdateUnreachableBody": "Release-Feed für {channel} konnte nicht erreicht werden.", + "@doctorUpdateUnreachableBody": { "placeholders": { "channel": { "type": "String" } } }, + "doctorReleaseNotes": "Notes: {url}", + "@doctorReleaseNotes": { "placeholders": { "url": { "type": "String" } } }, + "doctorPillNew": "neu", + "doctorPillOffline": "offline", + "doctorApplyUpdate": "Update anwenden", + "doctorApplying": "Wird angewendet…", + "doctorApplyDone": "Update angewendet. Studio neu starten, um die neue Daemon-Version zu sehen.", "mcpHeader": "MCP-CLIENTS", "mcpHint": "Externe MCP-Server bringen ihre Tools als `mcp..` Capabilities ein. Einmal konfigurieren, im Store sichtbar.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5f19313..94c08cc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -62,6 +62,9 @@ "@storeNResults": { "placeholders": { "n": { "type": "int" } } }, + "storeFederationNudgeTitle": "Want more capabilities?", + "storeFederationNudgeBody": "Configure an MCP server or n8n endpoint in Settings → MCP Clients / N8N Endpoints. Discovered tools and workflows surface here as `mcp..` and `n8n..` capabilities — every Anthropic / community MCP server adds a handful at once.", + "storeFederationNudgeButton": "Open Settings", "auditTitle": "Audit", "auditNoEvents": "No events yet", @@ -69,7 +72,32 @@ "modulesTitle": "Modules", "modulesNoneTitle": "No modules yet", + "modulesNoneHint": "Run `fai install ` or check ~/.fai/modules/.", + "modulesReloadTooltip": "Reload", "modulesRecentActivity": "RECENT ACTIVITY", + "modulesRecentActivityHint": "last {n} install/uninstall events from the audit log", + "@modulesRecentActivityHint": { "placeholders": { "n": { "type": "int" } } }, + "modulesActivityInstalled": "installed", + "modulesActivityUninstalled": "uninstalled", + "modulesCapabilitiesLabel": "Capabilities", + "modulesUninstallTitle": "Uninstall module?", + "modulesUninstallBody": "Removes {name} v{version} from this hub. Flows that reference it will fail at the next run. A `module.uninstalled` audit event is recorded.", + "@modulesUninstallBody": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "modulesUninstalledToast": "Uninstalled {name} v{version}.", + "@modulesUninstalledToast": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "modulesUninstallFailed": "Uninstall failed: {error}", + "@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } }, + "modulesUninstalling": "Uninstalling…", "approvalsTitle": "Approvals", "approvalsTabPending": "Pending", @@ -78,6 +106,73 @@ "approvalsInboxHint": "All caught up. New `system.approval@^0` steps land here automatically.", "doctorTitle": "Doctor", + "doctorRecheckTooltip": "Re-check", + "doctorEventLogSection": "Event log", + "doctorModulesApprovalsSection": "Modules & approvals", + "doctorHostServicesSection": "Host services", + "doctorDaemonFilesSection": "Daemon files", + "doctorDaemonControlSection": "Daemon control", + "doctorChainIntact": "Hash chain intact", + "doctorChainIntactDetail": "{n} events · prev_event_sha256 verified end-to-end", + "@doctorChainIntactDetail": { "placeholders": { "n": { "type": "int" } } }, + "doctorChainTampered": "Tampering detected", + "doctorChainTamperedDetail": "{verified} of {total} verified before mismatch at {id}", + "@doctorChainTamperedDetail": { + "placeholders": { + "verified": { "type": "int" }, + "total": { "type": "int" }, + "id": { "type": "String" } + } + }, + "doctorChainPillOk": "WORM-1", + "doctorChainPillTamper": "TAMPER", + "doctorVerifyNow": "Verify now", + "doctorRestart": "Restart", + "doctorStart": "Start", + "doctorStop": "Stop", + "doctorStatusAction": "Status", + "doctorRunningOn": "Running on {name}: {endpoint}", + "@doctorRunningOn": { + "placeholders": { + "name": { "type": "String" }, + "endpoint": { "type": "String" } + } + }, + "doctorStoppedOn": "{name} daemon stopped", + "@doctorStoppedOn": { "placeholders": { "name": { "type": "String" } } }, + "doctorStatusUnknown": "Daemon status: …", + "doctorPillRunning": "running", + "doctorPillStopped": "stopped", + "doctorDaemonControlHint": "Studio shells out to the platform binary — mirrors `fai daemon …` exactly so the CLI and UI stay in lockstep.", + "doctorDaemonControlWorking": "Working…", + "doctorPathLog": "Log", + "doctorPathConfig": "Config", + "doctorPathDb": "Audit DB", + "doctorPathModules": "Modules dir", + "doctorPathFlows": "Flows dir", + "doctorPathPid": "PID file", + "doctorPathsEmpty": "Daemon did not report file paths. Update the running hub via `fai daemon restart`.", + "doctorOpenError": "Could not open: {detail}", + "@doctorOpenError": { "placeholders": { "detail": { "type": "String" } } }, + "doctorUpdateAvailable": "Update available — {version}", + "@doctorUpdateAvailable": { "placeholders": { "version": { "type": "String" } } }, + "doctorUpdateBody": "Channel {channel} now offers {version}. Apply via the button — Studio shells out to `fai update apply --channel {channel}`.", + "@doctorUpdateBody": { + "placeholders": { + "channel": { "type": "String" }, + "version": { "type": "String" } + } + }, + "doctorUpdateUnreachable": "Release host unreachable", + "doctorUpdateUnreachableBody": "Could not contact the release feed for {channel}.", + "@doctorUpdateUnreachableBody": { "placeholders": { "channel": { "type": "String" } } }, + "doctorReleaseNotes": "notes: {url}", + "@doctorReleaseNotes": { "placeholders": { "url": { "type": "String" } } }, + "doctorPillNew": "new", + "doctorPillOffline": "offline", + "doctorApplyUpdate": "Apply update", + "doctorApplying": "Applying…", + "doctorApplyDone": "Update applied. Restart Studio to see the new daemon version.", "mcpHeader": "MCP CLIENTS", "mcpHint": "External MCP servers federate their tools as `mcp..` capabilities. Configure once, see them in the Store.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0a22542..f791e4f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -404,6 +404,24 @@ abstract class AppLocalizations { /// **'{n} result{n, plural, =1{} other{s}}'** String storeNResults(int n); + /// No description provided for @storeFederationNudgeTitle. + /// + /// In en, this message translates to: + /// **'Want more capabilities?'** + String get storeFederationNudgeTitle; + + /// No description provided for @storeFederationNudgeBody. + /// + /// In en, this message translates to: + /// **'Configure an MCP server or n8n endpoint in Settings → MCP Clients / N8N Endpoints. Discovered tools and workflows surface here as `mcp..` and `n8n..` capabilities — every Anthropic / community MCP server adds a handful at once.'** + String get storeFederationNudgeBody; + + /// No description provided for @storeFederationNudgeButton. + /// + /// In en, this message translates to: + /// **'Open Settings'** + String get storeFederationNudgeButton; + /// No description provided for @auditTitle. /// /// In en, this message translates to: @@ -434,12 +452,78 @@ abstract class AppLocalizations { /// **'No modules yet'** String get modulesNoneTitle; + /// No description provided for @modulesNoneHint. + /// + /// In en, this message translates to: + /// **'Run `fai install ` or check ~/.fai/modules/.'** + String get modulesNoneHint; + + /// No description provided for @modulesReloadTooltip. + /// + /// In en, this message translates to: + /// **'Reload'** + String get modulesReloadTooltip; + /// No description provided for @modulesRecentActivity. /// /// In en, this message translates to: /// **'RECENT ACTIVITY'** String get modulesRecentActivity; + /// No description provided for @modulesRecentActivityHint. + /// + /// In en, this message translates to: + /// **'last {n} install/uninstall events from the audit log'** + String modulesRecentActivityHint(int n); + + /// No description provided for @modulesActivityInstalled. + /// + /// In en, this message translates to: + /// **'installed'** + String get modulesActivityInstalled; + + /// No description provided for @modulesActivityUninstalled. + /// + /// In en, this message translates to: + /// **'uninstalled'** + String get modulesActivityUninstalled; + + /// No description provided for @modulesCapabilitiesLabel. + /// + /// In en, this message translates to: + /// **'Capabilities'** + String get modulesCapabilitiesLabel; + + /// No description provided for @modulesUninstallTitle. + /// + /// In en, this message translates to: + /// **'Uninstall module?'** + String get modulesUninstallTitle; + + /// No description provided for @modulesUninstallBody. + /// + /// In en, this message translates to: + /// **'Removes {name} v{version} from this hub. Flows that reference it will fail at the next run. A `module.uninstalled` audit event is recorded.'** + String modulesUninstallBody(String name, String version); + + /// No description provided for @modulesUninstalledToast. + /// + /// In en, this message translates to: + /// **'Uninstalled {name} v{version}.'** + String modulesUninstalledToast(String name, String version); + + /// No description provided for @modulesUninstallFailed. + /// + /// In en, this message translates to: + /// **'Uninstall failed: {error}'** + String modulesUninstallFailed(String error); + + /// No description provided for @modulesUninstalling. + /// + /// In en, this message translates to: + /// **'Uninstalling…'** + String get modulesUninstalling; + /// No description provided for @approvalsTitle. /// /// In en, this message translates to: @@ -476,6 +560,258 @@ abstract class AppLocalizations { /// **'Doctor'** String get doctorTitle; + /// No description provided for @doctorRecheckTooltip. + /// + /// In en, this message translates to: + /// **'Re-check'** + String get doctorRecheckTooltip; + + /// No description provided for @doctorEventLogSection. + /// + /// In en, this message translates to: + /// **'Event log'** + String get doctorEventLogSection; + + /// No description provided for @doctorModulesApprovalsSection. + /// + /// In en, this message translates to: + /// **'Modules & approvals'** + String get doctorModulesApprovalsSection; + + /// No description provided for @doctorHostServicesSection. + /// + /// In en, this message translates to: + /// **'Host services'** + String get doctorHostServicesSection; + + /// No description provided for @doctorDaemonFilesSection. + /// + /// In en, this message translates to: + /// **'Daemon files'** + String get doctorDaemonFilesSection; + + /// No description provided for @doctorDaemonControlSection. + /// + /// In en, this message translates to: + /// **'Daemon control'** + String get doctorDaemonControlSection; + + /// No description provided for @doctorChainIntact. + /// + /// In en, this message translates to: + /// **'Hash chain intact'** + String get doctorChainIntact; + + /// No description provided for @doctorChainIntactDetail. + /// + /// In en, this message translates to: + /// **'{n} events · prev_event_sha256 verified end-to-end'** + String doctorChainIntactDetail(int n); + + /// No description provided for @doctorChainTampered. + /// + /// In en, this message translates to: + /// **'Tampering detected'** + String get doctorChainTampered; + + /// No description provided for @doctorChainTamperedDetail. + /// + /// In en, this message translates to: + /// **'{verified} of {total} verified before mismatch at {id}'** + String doctorChainTamperedDetail(int verified, int total, String id); + + /// No description provided for @doctorChainPillOk. + /// + /// In en, this message translates to: + /// **'WORM-1'** + String get doctorChainPillOk; + + /// No description provided for @doctorChainPillTamper. + /// + /// In en, this message translates to: + /// **'TAMPER'** + String get doctorChainPillTamper; + + /// No description provided for @doctorVerifyNow. + /// + /// In en, this message translates to: + /// **'Verify now'** + String get doctorVerifyNow; + + /// No description provided for @doctorRestart. + /// + /// In en, this message translates to: + /// **'Restart'** + String get doctorRestart; + + /// No description provided for @doctorStart. + /// + /// In en, this message translates to: + /// **'Start'** + String get doctorStart; + + /// No description provided for @doctorStop. + /// + /// In en, this message translates to: + /// **'Stop'** + String get doctorStop; + + /// No description provided for @doctorStatusAction. + /// + /// In en, this message translates to: + /// **'Status'** + String get doctorStatusAction; + + /// No description provided for @doctorRunningOn. + /// + /// In en, this message translates to: + /// **'Running on {name}: {endpoint}'** + String doctorRunningOn(String name, String endpoint); + + /// No description provided for @doctorStoppedOn. + /// + /// In en, this message translates to: + /// **'{name} daemon stopped'** + String doctorStoppedOn(String name); + + /// No description provided for @doctorStatusUnknown. + /// + /// In en, this message translates to: + /// **'Daemon status: …'** + String get doctorStatusUnknown; + + /// No description provided for @doctorPillRunning. + /// + /// In en, this message translates to: + /// **'running'** + String get doctorPillRunning; + + /// No description provided for @doctorPillStopped. + /// + /// In en, this message translates to: + /// **'stopped'** + String get doctorPillStopped; + + /// No description provided for @doctorDaemonControlHint. + /// + /// In en, this message translates to: + /// **'Studio shells out to the platform binary — mirrors `fai daemon …` exactly so the CLI and UI stay in lockstep.'** + String get doctorDaemonControlHint; + + /// No description provided for @doctorDaemonControlWorking. + /// + /// In en, this message translates to: + /// **'Working…'** + String get doctorDaemonControlWorking; + + /// No description provided for @doctorPathLog. + /// + /// In en, this message translates to: + /// **'Log'** + String get doctorPathLog; + + /// No description provided for @doctorPathConfig. + /// + /// In en, this message translates to: + /// **'Config'** + String get doctorPathConfig; + + /// No description provided for @doctorPathDb. + /// + /// In en, this message translates to: + /// **'Audit DB'** + String get doctorPathDb; + + /// No description provided for @doctorPathModules. + /// + /// In en, this message translates to: + /// **'Modules dir'** + String get doctorPathModules; + + /// No description provided for @doctorPathFlows. + /// + /// In en, this message translates to: + /// **'Flows dir'** + String get doctorPathFlows; + + /// No description provided for @doctorPathPid. + /// + /// In en, this message translates to: + /// **'PID file'** + String get doctorPathPid; + + /// No description provided for @doctorPathsEmpty. + /// + /// In en, this message translates to: + /// **'Daemon did not report file paths. Update the running hub via `fai daemon restart`.'** + String get doctorPathsEmpty; + + /// No description provided for @doctorOpenError. + /// + /// In en, this message translates to: + /// **'Could not open: {detail}'** + String doctorOpenError(String detail); + + /// No description provided for @doctorUpdateAvailable. + /// + /// In en, this message translates to: + /// **'Update available — {version}'** + String doctorUpdateAvailable(String version); + + /// No description provided for @doctorUpdateBody. + /// + /// In en, this message translates to: + /// **'Channel {channel} now offers {version}. Apply via the button — Studio shells out to `fai update apply --channel {channel}`.'** + String doctorUpdateBody(String channel, String version); + + /// No description provided for @doctorUpdateUnreachable. + /// + /// In en, this message translates to: + /// **'Release host unreachable'** + String get doctorUpdateUnreachable; + + /// No description provided for @doctorUpdateUnreachableBody. + /// + /// In en, this message translates to: + /// **'Could not contact the release feed for {channel}.'** + String doctorUpdateUnreachableBody(String channel); + + /// No description provided for @doctorReleaseNotes. + /// + /// In en, this message translates to: + /// **'notes: {url}'** + String doctorReleaseNotes(String url); + + /// No description provided for @doctorPillNew. + /// + /// In en, this message translates to: + /// **'new'** + String get doctorPillNew; + + /// No description provided for @doctorPillOffline. + /// + /// In en, this message translates to: + /// **'offline'** + String get doctorPillOffline; + + /// No description provided for @doctorApplyUpdate. + /// + /// In en, this message translates to: + /// **'Apply update'** + String get doctorApplyUpdate; + + /// No description provided for @doctorApplying. + /// + /// In en, this message translates to: + /// **'Applying…'** + String get doctorApplying; + + /// No description provided for @doctorApplyDone. + /// + /// In en, this message translates to: + /// **'Update applied. Restart Studio to see the new daemon version.'** + String get doctorApplyDone; + /// No description provided for @mcpHeader. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b641124..abc2db2 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -166,6 +166,16 @@ class AppLocalizationsDe extends AppLocalizations { return '$n Treffer'; } + @override + String get storeFederationNudgeTitle => 'Mehr Capabilities?'; + + @override + String get storeFederationNudgeBody => + 'MCP-Server oder n8n-Endpunkt unter Einstellungen → MCP-Clients / N8N-Endpunkte konfigurieren. Entdeckte Tools und Workflows tauchen hier als `mcp..`- und `n8n..`-Capabilities auf — jeder Anthropic-/Community-MCP-Server bringt mehrere auf einmal.'; + + @override + String get storeFederationNudgeButton => 'Einstellungen öffnen'; + @override String get auditTitle => 'Protokoll'; @@ -182,9 +192,51 @@ class AppLocalizationsDe extends AppLocalizations { @override String get modulesNoneTitle => 'Noch keine Module'; + @override + String get modulesNoneHint => + '`fai install ` ausführen oder ~/.fai/modules/ prüfen.'; + + @override + String get modulesReloadTooltip => 'Aktualisieren'; + @override String get modulesRecentActivity => 'LETZTE AKTIVITÄT'; + @override + String modulesRecentActivityHint(int n) { + return 'letzte $n install/uninstall-Ereignisse aus dem Audit-Log'; + } + + @override + String get modulesActivityInstalled => 'installiert'; + + @override + String get modulesActivityUninstalled => 'deinstalliert'; + + @override + String get modulesCapabilitiesLabel => 'Capabilities'; + + @override + String get modulesUninstallTitle => 'Modul deinstallieren?'; + + @override + String modulesUninstallBody(String name, String version) { + return 'Entfernt $name v$version von diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl. Ein `module.uninstalled`-Audit-Event wird festgehalten.'; + } + + @override + String modulesUninstalledToast(String name, String version) { + return '$name v$version deinstalliert.'; + } + + @override + String modulesUninstallFailed(String error) { + return 'Deinstallation fehlgeschlagen: $error'; + } + + @override + String get modulesUninstalling => 'Wird deinstalliert…'; + @override String get approvalsTitle => 'Freigaben'; @@ -204,6 +256,153 @@ class AppLocalizationsDe extends AppLocalizations { @override String get doctorTitle => 'Diagnose'; + @override + String get doctorRecheckTooltip => 'Erneut prüfen'; + + @override + String get doctorEventLogSection => 'Audit-Log'; + + @override + String get doctorModulesApprovalsSection => 'Module & Freigaben'; + + @override + String get doctorHostServicesSection => 'Host-Dienste'; + + @override + String get doctorDaemonFilesSection => 'Daemon-Dateien'; + + @override + String get doctorDaemonControlSection => 'Daemon-Steuerung'; + + @override + String get doctorChainIntact => 'Hash-Chain intakt'; + + @override + String doctorChainIntactDetail(int n) { + return '$n Ereignisse · prev_event_sha256 lückenlos verifiziert'; + } + + @override + String get doctorChainTampered => 'Manipulation erkannt'; + + @override + String doctorChainTamperedDetail(int verified, int total, String id) { + return '$verified von $total verifiziert vor Mismatch bei $id'; + } + + @override + String get doctorChainPillOk => 'WORM-1'; + + @override + String get doctorChainPillTamper => 'MANIPULIERT'; + + @override + String get doctorVerifyNow => 'Jetzt prüfen'; + + @override + String get doctorRestart => 'Neustart'; + + @override + String get doctorStart => 'Starten'; + + @override + String get doctorStop => 'Stoppen'; + + @override + String get doctorStatusAction => 'Status'; + + @override + String doctorRunningOn(String name, String endpoint) { + return 'Läuft auf $name: $endpoint'; + } + + @override + String doctorStoppedOn(String name) { + return '$name-Daemon gestoppt'; + } + + @override + String get doctorStatusUnknown => 'Daemon-Status: …'; + + @override + String get doctorPillRunning => 'läuft'; + + @override + String get doctorPillStopped => 'gestoppt'; + + @override + String get doctorDaemonControlHint => + 'Studio ruft das Platform-Binary auf — entspricht `fai daemon …`, sodass CLI und UI im Gleichschritt bleiben.'; + + @override + String get doctorDaemonControlWorking => 'Wird ausgeführt…'; + + @override + String get doctorPathLog => 'Log'; + + @override + String get doctorPathConfig => 'Konfig'; + + @override + String get doctorPathDb => 'Audit-DB'; + + @override + String get doctorPathModules => 'Module-Verzeichnis'; + + @override + String get doctorPathFlows => 'Flow-Verzeichnis'; + + @override + String get doctorPathPid => 'PID-Datei'; + + @override + String get doctorPathsEmpty => + 'Daemon hat keine Datei-Pfade gemeldet. Hub aktualisieren via `fai daemon restart`.'; + + @override + String doctorOpenError(String detail) { + return 'Konnte nicht öffnen: $detail'; + } + + @override + String doctorUpdateAvailable(String version) { + return 'Update verfügbar — $version'; + } + + @override + String doctorUpdateBody(String channel, String version) { + return 'Kanal $channel bietet jetzt $version. Per Button anwenden — Studio ruft `fai update apply --channel $channel` auf.'; + } + + @override + String get doctorUpdateUnreachable => 'Release-Host nicht erreichbar'; + + @override + String doctorUpdateUnreachableBody(String channel) { + return 'Release-Feed für $channel konnte nicht erreicht werden.'; + } + + @override + String doctorReleaseNotes(String url) { + return 'Notes: $url'; + } + + @override + String get doctorPillNew => 'neu'; + + @override + String get doctorPillOffline => 'offline'; + + @override + String get doctorApplyUpdate => 'Update anwenden'; + + @override + String get doctorApplying => 'Wird angewendet…'; + + @override + String get doctorApplyDone => + 'Update angewendet. Studio neu starten, um die neue Daemon-Version zu sehen.'; + @override String get mcpHeader => 'MCP-CLIENTS'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a3132e0..749cb3c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -172,6 +172,16 @@ class AppLocalizationsEn extends AppLocalizations { return '$n result$_temp0'; } + @override + String get storeFederationNudgeTitle => 'Want more capabilities?'; + + @override + String get storeFederationNudgeBody => + 'Configure an MCP server or n8n endpoint in Settings → MCP Clients / N8N Endpoints. Discovered tools and workflows surface here as `mcp..` and `n8n..` capabilities — every Anthropic / community MCP server adds a handful at once.'; + + @override + String get storeFederationNudgeButton => 'Open Settings'; + @override String get auditTitle => 'Audit'; @@ -188,9 +198,51 @@ class AppLocalizationsEn extends AppLocalizations { @override String get modulesNoneTitle => 'No modules yet'; + @override + String get modulesNoneHint => + 'Run `fai install ` or check ~/.fai/modules/.'; + + @override + String get modulesReloadTooltip => 'Reload'; + @override String get modulesRecentActivity => 'RECENT ACTIVITY'; + @override + String modulesRecentActivityHint(int n) { + return 'last $n install/uninstall events from the audit log'; + } + + @override + String get modulesActivityInstalled => 'installed'; + + @override + String get modulesActivityUninstalled => 'uninstalled'; + + @override + String get modulesCapabilitiesLabel => 'Capabilities'; + + @override + String get modulesUninstallTitle => 'Uninstall module?'; + + @override + String modulesUninstallBody(String name, String version) { + return 'Removes $name v$version from this hub. Flows that reference it will fail at the next run. A `module.uninstalled` audit event is recorded.'; + } + + @override + String modulesUninstalledToast(String name, String version) { + return 'Uninstalled $name v$version.'; + } + + @override + String modulesUninstallFailed(String error) { + return 'Uninstall failed: $error'; + } + + @override + String get modulesUninstalling => 'Uninstalling…'; + @override String get approvalsTitle => 'Approvals'; @@ -210,6 +262,153 @@ class AppLocalizationsEn extends AppLocalizations { @override String get doctorTitle => 'Doctor'; + @override + String get doctorRecheckTooltip => 'Re-check'; + + @override + String get doctorEventLogSection => 'Event log'; + + @override + String get doctorModulesApprovalsSection => 'Modules & approvals'; + + @override + String get doctorHostServicesSection => 'Host services'; + + @override + String get doctorDaemonFilesSection => 'Daemon files'; + + @override + String get doctorDaemonControlSection => 'Daemon control'; + + @override + String get doctorChainIntact => 'Hash chain intact'; + + @override + String doctorChainIntactDetail(int n) { + return '$n events · prev_event_sha256 verified end-to-end'; + } + + @override + String get doctorChainTampered => 'Tampering detected'; + + @override + String doctorChainTamperedDetail(int verified, int total, String id) { + return '$verified of $total verified before mismatch at $id'; + } + + @override + String get doctorChainPillOk => 'WORM-1'; + + @override + String get doctorChainPillTamper => 'TAMPER'; + + @override + String get doctorVerifyNow => 'Verify now'; + + @override + String get doctorRestart => 'Restart'; + + @override + String get doctorStart => 'Start'; + + @override + String get doctorStop => 'Stop'; + + @override + String get doctorStatusAction => 'Status'; + + @override + String doctorRunningOn(String name, String endpoint) { + return 'Running on $name: $endpoint'; + } + + @override + String doctorStoppedOn(String name) { + return '$name daemon stopped'; + } + + @override + String get doctorStatusUnknown => 'Daemon status: …'; + + @override + String get doctorPillRunning => 'running'; + + @override + String get doctorPillStopped => 'stopped'; + + @override + String get doctorDaemonControlHint => + 'Studio shells out to the platform binary — mirrors `fai daemon …` exactly so the CLI and UI stay in lockstep.'; + + @override + String get doctorDaemonControlWorking => 'Working…'; + + @override + String get doctorPathLog => 'Log'; + + @override + String get doctorPathConfig => 'Config'; + + @override + String get doctorPathDb => 'Audit DB'; + + @override + String get doctorPathModules => 'Modules dir'; + + @override + String get doctorPathFlows => 'Flows dir'; + + @override + String get doctorPathPid => 'PID file'; + + @override + String get doctorPathsEmpty => + 'Daemon did not report file paths. Update the running hub via `fai daemon restart`.'; + + @override + String doctorOpenError(String detail) { + return 'Could not open: $detail'; + } + + @override + String doctorUpdateAvailable(String version) { + return 'Update available — $version'; + } + + @override + String doctorUpdateBody(String channel, String version) { + return 'Channel $channel now offers $version. Apply via the button — Studio shells out to `fai update apply --channel $channel`.'; + } + + @override + String get doctorUpdateUnreachable => 'Release host unreachable'; + + @override + String doctorUpdateUnreachableBody(String channel) { + return 'Could not contact the release feed for $channel.'; + } + + @override + String doctorReleaseNotes(String url) { + return 'notes: $url'; + } + + @override + String get doctorPillNew => 'new'; + + @override + String get doctorPillOffline => 'offline'; + + @override + String get doctorApplyUpdate => 'Apply update'; + + @override + String get doctorApplying => 'Applying…'; + + @override + String get doctorApplyDone => + 'Update applied. Restart Studio to see the new daemon version.'; + @override String get mcpHeader => 'MCP CLIENTS'; diff --git a/lib/main.dart b/lib/main.dart index d9388b1..6b5898d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ import 'widgets/widgets.dart'; /// Studio's own build version. Bump on every UI commit so the /// running app self-identifies — visible in the sidebar header /// and quick-glance proof that you're seeing the current build. -const String kStudioVersion = '0.24.0'; +const String kStudioVersion = '0.25.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/doctor.dart b/lib/pages/doctor.dart index 75f1711..89cf83d 100644 --- a/lib/pages/doctor.dart +++ b/lib/pages/doctor.dart @@ -36,7 +36,7 @@ class _DoctorPageState extends State { actions: [ IconButton( icon: const Icon(Icons.refresh, size: 18), - tooltip: 'Re-check', + tooltip: AppLocalizations.of(context)!.doctorRecheckTooltip, onPressed: _refresh, ), const SizedBox(width: FaiSpace.sm), @@ -49,14 +49,15 @@ class _DoctorPageState extends State { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { + final l = AppLocalizations.of(context)!; return FaiEmptyState( icon: Icons.cloud_off_outlined, iconColor: Theme.of(context).colorScheme.error, - title: 'Hub unreachable', - hint: 'Start the hub with `fai serve`.', + title: l.hubUnreachable, + hint: l.hubUnreachableHint, action: FilledButton.tonal( onPressed: _refresh, - child: const Text('Retry'), + child: Text(l.buttonRetry), ), ); } @@ -72,7 +73,7 @@ class _DoctorPageState extends State { _SummaryStrip(snapshot: s), const SizedBox(height: FaiSpace.xl), _Section( - title: 'Event log', + title: AppLocalizations.of(context)!.doctorEventLogSection, child: _EventLogPanel( snapshot: s, onRefresh: _refresh, @@ -80,22 +81,22 @@ class _DoctorPageState extends State { ), const SizedBox(height: FaiSpace.lg), _Section( - title: 'Modules & approvals', + title: AppLocalizations.of(context)!.doctorModulesApprovalsSection, child: _ModulesPanel(snapshot: s), ), const SizedBox(height: FaiSpace.lg), _Section( - title: 'Host services', + title: AppLocalizations.of(context)!.doctorHostServicesSection, child: _ServicesPanel(snapshot: s), ), const SizedBox(height: FaiSpace.lg), _Section( - title: 'Daemon files', + title: AppLocalizations.of(context)!.doctorDaemonFilesSection, child: _DaemonPathsPanel(paths: s.paths), ), const SizedBox(height: FaiSpace.lg), _Section( - title: 'Daemon control', + title: AppLocalizations.of(context)!.doctorDaemonControlSection, child: _DaemonActionsCard(), ), ], @@ -266,6 +267,7 @@ class _EventLogPanel extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final healthy = snapshot.chainHealthy; + final l = AppLocalizations.of(context)!; return FaiCard( child: Row( children: [ @@ -280,9 +282,7 @@ class _EventLogPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - healthy - ? 'Hash chain intact' - : 'Tampering detected', + healthy ? l.doctorChainIntact : l.doctorChainTampered, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), @@ -290,8 +290,12 @@ class _EventLogPanel extends StatelessWidget { const SizedBox(height: 2), Text( healthy - ? '${snapshot.eventChainTotal} events · prev_event_sha256 verified end-to-end' - : '${snapshot.eventChainVerified} of ${snapshot.eventChainTotal} verified before mismatch at ${snapshot.eventChainTamperedAt}', + ? l.doctorChainIntactDetail(snapshot.eventChainTotal) + : l.doctorChainTamperedDetail( + snapshot.eventChainVerified, + snapshot.eventChainTotal, + snapshot.eventChainTamperedAt ?? '', + ), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -302,11 +306,11 @@ class _EventLogPanel extends StatelessWidget { OutlinedButton.icon( onPressed: onRefresh, icon: const Icon(Icons.fact_check_outlined, size: 16), - label: const Text('Verify now'), + label: Text(l.doctorVerifyNow), ), const SizedBox(width: FaiSpace.sm), FaiPill( - label: healthy ? 'WORM-1' : 'TAMPER', + label: healthy ? l.doctorChainPillOk : l.doctorChainPillTamper, tone: healthy ? FaiPillTone.success : FaiPillTone.danger, ), ], @@ -326,20 +330,20 @@ class _DaemonPathsPanel extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; final entries = <(String, String, IconData)>[ - ('Log', paths.logPath, Icons.description_outlined), - ('Config', paths.configPath, Icons.settings_outlined), - ('Audit DB', paths.dbPath, Icons.storage_outlined), - ('Modules dir', paths.modulesDir, Icons.extension_outlined), - ('Flows dir', paths.flowsDir, Icons.account_tree_outlined), - ('PID file', paths.pidPath, Icons.fingerprint), + (l.doctorPathLog, paths.logPath, Icons.description_outlined), + (l.doctorPathConfig, paths.configPath, Icons.settings_outlined), + (l.doctorPathDb, paths.dbPath, Icons.storage_outlined), + (l.doctorPathModules, paths.modulesDir, Icons.extension_outlined), + (l.doctorPathFlows, paths.flowsDir, Icons.account_tree_outlined), + (l.doctorPathPid, paths.pidPath, Icons.fingerprint), ].where((e) => e.$2.isNotEmpty).toList(); if (entries.isEmpty) { return FaiCard( child: Text( - 'Daemon did not report file paths. ' - 'Update the running hub via `fai daemon restart`.', + l.doctorPathsEmpty, style: Theme.of(context).textTheme.bodySmall, ), ); @@ -396,13 +400,14 @@ class _PathRow extends StatelessWidget { final r = await SystemActions.openInOs(path); if (!context.mounted) return; if (!r.ok) { + final l = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Could not open: ${r.stderr}')), + SnackBar(content: Text(l.doctorOpenError(r.stderr))), ); } }, icon: const Icon(Icons.open_in_new, size: 14), - label: const Text('Open'), + label: Text(AppLocalizations.of(context)!.buttonOpen), style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, ), @@ -468,6 +473,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; ChannelInfo? active; if (_channels != null) { for (final c in _channels!.channels) { @@ -502,10 +508,10 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { Expanded( child: Text( active == null - ? 'Daemon status: …' + ? l.doctorStatusUnknown : active.running - ? 'Running on ${active.name}: ${active.endpoint}' - : '${active.name} daemon stopped', + ? l.doctorRunningOn(active.name, active.endpoint) + : l.doctorStoppedOn(active.name), style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), @@ -514,22 +520,20 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { ), if (active != null) ...[ const SizedBox(width: FaiSpace.sm), - Tooltip( - message: 'PID file at the channel\'s run/.pid', - child: FaiPill( - label: active.running ? 'running' : 'stopped', - tone: active.running - ? FaiPillTone.success - : FaiPillTone.neutral, - ), + FaiPill( + label: active.running + ? l.doctorPillRunning + : l.doctorPillStopped, + tone: active.running + ? FaiPillTone.success + : FaiPillTone.neutral, ), ], ], ), const SizedBox(height: FaiSpace.sm), Text( - 'Studio shells out to the platform binary — mirrors ' - '`fai daemon …` exactly so the CLI and UI stay in lockstep.', + l.doctorDaemonControlHint, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -547,7 +551,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { () => SystemActions.faiDaemon(['restart']), ), icon: const Icon(Icons.restart_alt, size: 16), - label: const Text('Restart'), + label: Text(l.doctorRestart), ), if (active?.running != true) FilledButton.icon( @@ -558,7 +562,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { () => SystemActions.faiDaemon(['start']), ), icon: const Icon(Icons.play_arrow, size: 16), - label: const Text('Start'), + label: Text(l.doctorStart), ), OutlinedButton.icon( onPressed: _busy @@ -568,7 +572,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { () => SystemActions.faiDaemon(['stop']), ), icon: const Icon(Icons.stop_circle_outlined, size: 16), - label: const Text('Stop'), + label: Text(l.doctorStop), ), OutlinedButton.icon( onPressed: _busy @@ -578,7 +582,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { () => SystemActions.faiDaemon(['status']), ), icon: const Icon(Icons.health_and_safety_outlined, size: 16), - label: const Text('Status'), + label: Text(l.doctorStatusAction), ), ], ), @@ -593,7 +597,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { ), const SizedBox(width: FaiSpace.sm), Text( - 'Working…', + l.doctorDaemonControlWorking, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -797,7 +801,7 @@ class _UpdateBannerState extends State<_UpdateBanner> { setState(() { _applying = false; _applyOutput = r.ok - ? 'Update applied. Restart Studio to see the new daemon version.' + ? AppLocalizations.of(context)!.doctorApplyDone : (r.stderr.isEmpty ? r.stdout : r.stderr).trim(); }); } @@ -805,6 +809,7 @@ class _UpdateBannerState extends State<_UpdateBanner> { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final status = widget.status; final available = status.updateAvailable; final iconData = available @@ -814,13 +819,11 @@ class _UpdateBannerState extends State<_UpdateBanner> { ? theme.colorScheme.primary : theme.colorScheme.onSurfaceVariant; final title = available - ? 'Update available — ${status.latestVersion}' - : 'Release host unreachable'; + ? l.doctorUpdateAvailable(status.latestVersion) + : l.doctorUpdateUnreachable; final body = available - ? 'Channel ${status.channel} now offers ${status.latestVersion}. ' - 'Apply via the button — Studio shells out to ' - '`fai update apply --channel ${status.channel}`.' - : (status.reason ?? 'Could not contact the release feed for ${status.channel}.'); + ? l.doctorUpdateBody(status.channel, status.latestVersion) + : (status.reason ?? l.doctorUpdateUnreachableBody(status.channel)); return FaiCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -851,7 +854,7 @@ class _UpdateBannerState extends State<_UpdateBanner> { if (available && status.releaseNotesUrl != null) ...[ const SizedBox(height: 4), Text( - 'notes: ${status.releaseNotesUrl}', + l.doctorReleaseNotes(status.releaseNotesUrl!), style: FaiTheme.mono( size: 11, color: theme.colorScheme.primary, @@ -871,11 +874,11 @@ class _UpdateBannerState extends State<_UpdateBanner> { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.system_update_alt, size: 16), - label: Text(_applying ? 'Applying…' : 'Apply update'), + label: Text(_applying ? l.doctorApplying : l.doctorApplyUpdate), ), const SizedBox(width: FaiSpace.sm), FaiPill( - label: available ? 'new' : 'offline', + label: available ? l.doctorPillNew : l.doctorPillOffline, tone: available ? FaiPillTone.success : FaiPillTone.neutral, ), ], diff --git a/lib/pages/modules.dart b/lib/pages/modules.dart index aa06399..d81587e 100644 --- a/lib/pages/modules.dart +++ b/lib/pages/modules.dart @@ -45,7 +45,7 @@ class _ModulesPageState extends State { actions: [ IconButton( icon: const Icon(Icons.refresh, size: 18), - tooltip: 'Reload', + tooltip: AppLocalizations.of(context)!.modulesReloadTooltip, onPressed: _refresh, ), const SizedBox(width: FaiSpace.sm), @@ -54,6 +54,7 @@ class _ModulesPageState extends State { body: FutureBuilder>( future: _future, builder: (context, snapshot) { + final l = AppLocalizations.of(context)!; if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } @@ -61,22 +62,20 @@ class _ModulesPageState extends State { return FaiEmptyState( icon: Icons.cloud_off_outlined, iconColor: Theme.of(context).colorScheme.error, - title: 'Hub unreachable', - hint: - 'Start the hub with `fai serve`. Studio will retry automatically.', + title: l.hubUnreachable, + hint: l.hubUnreachableHint, action: FilledButton.tonal( onPressed: _refresh, - child: const Text('Retry'), + child: Text(l.buttonRetry), ), ); } final modules = snapshot.data ?? []; if (modules.isEmpty) { - return const FaiEmptyState( + return FaiEmptyState( icon: Icons.extension_outlined, - title: 'No modules yet', - hint: - 'Run `fai install ` or check ~/.fai/modules/.', + title: l.modulesNoneTitle, + hint: l.modulesNoneHint, ); } return ListView( @@ -142,7 +141,7 @@ class _ModuleCard extends StatelessWidget { SizedBox( width: 110, child: Text( - 'Capabilities', + AppLocalizations.of(context)!.modulesCapabilitiesLabel, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, letterSpacing: 0.4, @@ -212,7 +211,7 @@ class _RecentActivityPanel extends StatelessWidget { ), const SizedBox(width: 6), Text( - 'RECENT ACTIVITY', + AppLocalizations.of(context)!.modulesRecentActivity, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, letterSpacing: 0.6, @@ -221,7 +220,8 @@ class _RecentActivityPanel extends StatelessWidget { ), const SizedBox(width: FaiSpace.sm), Text( - 'last ${events.length} install/uninstall events from the audit log', + AppLocalizations.of(context)! + .modulesRecentActivityHint(events.length), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -269,7 +269,7 @@ class _ActivityRow extends StatelessWidget { ), Expanded( child: Text( - '${installed ? "installed" : "uninstalled"} ' + '${installed ? AppLocalizations.of(context)!.modulesActivityInstalled : AppLocalizations.of(context)!.modulesActivityUninstalled} ' '${event.moduleName ?? "(unknown)"}' '${event.moduleVersion != null ? " v${event.moduleVersion}" : ""}', style: theme.textTheme.bodySmall, diff --git a/lib/pages/store.dart b/lib/pages/store.dart index 132342f..c5e3276 100644 --- a/lib/pages/store.dart +++ b/lib/pages/store.dart @@ -10,6 +10,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import '../data/hub.dart'; import '../data/system_actions.dart'; +import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import '../widgets/widgets.dart'; @@ -220,9 +221,25 @@ class _StorePageState extends State { _status.isEmpty && !_installedOnly && items.any((e) => e.featured); + // Sparse-store nudge: when no filter is set + // and the store carries no federated entries, + // hint that adding an MCP / n8n endpoint + // brings dozens of capabilities at once. + // Hides the moment any federated entry shows + // up — the nudge has done its job. + final showFederationNudge = + _queryCtrl.text.trim().isEmpty && + _category.isEmpty && + _status.isEmpty && + !_installedOnly && + !items.any((e) => e.isFederated); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (showFederationNudge) ...[ + const _FederationNudge(), + const SizedBox(height: FaiSpace.md), + ], if (showFeatured) ...[ _FeaturedStrip( items: items.where((e) => e.featured).toList(), @@ -1773,3 +1790,66 @@ class _ScreenshotPlaceholder extends StatelessWidget { ); } } + +/// Sparse-store nudge: the operator's store has only the +/// bundled native + planned modules and no federated entries. +/// The single biggest store-fullness lever is configuring an +/// MCP server or n8n endpoint, so the banner says exactly +/// that. Tap → Settings dialog opens straight to the editor. +/// Dismisses automatically the next render after a federated +/// entry shows up — no per-user "don't show again". +class _FederationNudge extends StatelessWidget { + const _FederationNudge(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(FaiSpace.md), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(FaiRadius.md), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.hub_outlined, + size: 22, + color: theme.colorScheme.primary, + ), + const SizedBox(width: FaiSpace.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l.storeFederationNudgeTitle, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + l.storeFederationNudgeBody, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: FaiSpace.md), + FilledButton.tonalIcon( + onPressed: () => FaiSettingsDialog.show(context), + icon: const Icon(Icons.settings_outlined, size: 16), + label: Text(l.storeFederationNudgeButton), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/fai_module_sheet.dart b/lib/widgets/fai_module_sheet.dart index 4aebd59..049bb52 100644 --- a/lib/widgets/fai_module_sheet.dart +++ b/lib/widgets/fai_module_sheet.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../data/hub.dart'; +import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import 'fai_pill.dart'; @@ -48,19 +49,16 @@ class _FaiModuleSheetState extends State { } Future _confirmAndUninstall(ModuleDetail detail) async { + final l = AppLocalizations.of(context)!; final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Uninstall module?'), - content: Text( - 'Removes ${detail.name} v${detail.version} from this hub. ' - 'Flows that reference it will fail at the next run. ' - 'A `module.uninstalled` audit event is recorded.', - ), + title: Text(l.modulesUninstallTitle), + content: Text(l.modulesUninstallBody(detail.name, detail.version)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(l.buttonCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), @@ -68,7 +66,7 @@ class _FaiModuleSheetState extends State { backgroundColor: Theme.of(ctx).colorScheme.error, foregroundColor: Theme.of(ctx).colorScheme.onError, ), - child: const Text('Uninstall'), + child: Text(l.buttonUninstall), ), ], ), @@ -79,14 +77,16 @@ class _FaiModuleSheetState extends State { final r = await HubService.instance.uninstallModule(detail.name); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Uninstalled ${r.name} v${r.version}.')), + SnackBar( + content: Text(l.modulesUninstalledToast(r.name, r.version)), + ), ); Navigator.pop(context, true); } catch (e) { if (!mounted) return; setState(() => _uninstalling = false); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Uninstall failed: $e')), + SnackBar(content: Text(l.modulesUninstallFailed(e.toString()))), ); } } @@ -269,7 +269,11 @@ class _Body extends StatelessWidget { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.delete_outline, size: 16), - label: Text(uninstalling ? 'Uninstalling…' : 'Uninstall'), + label: Text( + uninstalling + ? AppLocalizations.of(context)!.modulesUninstalling + : AppLocalizations.of(context)!.buttonUninstall, + ), style: OutlinedButton.styleFrom( foregroundColor: theme.colorScheme.error, side: BorderSide( diff --git a/pubspec.yaml b/pubspec.yaml index 03eaa7b..7d88930 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fai_studio description: "F∆I Studio — desktop GUI for the F∆I hub" publish_to: 'none' -version: 0.24.0 +version: 0.25.0 environment: sdk: ^3.11.0-200.1.beta