diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ddcbd47..5d60dc1 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -263,6 +263,25 @@ "modulesUninstallFailed": "Deinstallation fehlgeschlagen: {error}", "@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } }, "modulesUninstalling": "Wird deinstalliert…", + "moduleSheetFailedToLoad": "Laden fehlgeschlagen: {error}", + "@moduleSheetFailedToLoad": { "placeholders": { "error": { "type": "String" } } }, + "moduleSheetCapabilities": "Capabilities", + "moduleSheetPermissions": "Deklarierte Berechtigungen", + "moduleSheetNoPermissions": "(keine — reines Berechnungsmodul)", + + "flowsTitle": "Flows", + "flowsReloadTooltip": "Aktualisieren", + "flowsNoneTitle": "Keine gespeicherten Flows", + "flowsNoneHint": "Flow speichern mit `fai admin flows save `, dann taucht er hier auf.", + "flowsRunDialogTitle": "{name} ausführen", + "@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } }, + "flowsRunDialogBody": "Eingaben als key=value, eine pro Zeile. Alle Werte werden als Text-Payloads gesendet. Für binäre Eingaben die `fai run` CLI nutzen.", + "flowsInputsHint": "name=World\ntarget_language=English\nsummary_style=three bullet points", + "flowsRunButton": "Starten", + "flowsRunningTitle": "{name} läuft", + "@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } }, + "flowsRunning": "Flow läuft…", + "flowsNoOutputs": "Flow abgeschlossen ohne deklarierte Outputs.", "approvalsTitle": "Freigaben", "approvalsTabPending": "Offen", @@ -411,5 +430,65 @@ "@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } }, "searchStoreHintWithCategory": "Store · {category}", "@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } }, - "searchSavedFlowHint": "gespeicherter Flow" + "searchSavedFlowHint": "gespeicherter Flow", + + "systemAiTitle": "System-AI", + "systemAiIntro": "Die hub-interne LLM, die die Plattform selbst für Fehlererklärungen und Operator-Hilfe nutzt. Standardmäßig aus; hier konfigurieren. Siehe docs/architecture/system-ai.md, was über die Leitung geht.", + "systemAiProviderLabel": "Provider", + "systemAiEndpointLabel": "Endpunkt-URL (bis /v1)", + "systemAiApiKeyRequired": "API-Key-Umgebungsvariable (erforderlich)", + "systemAiApiKeyOptional": "API-Key-Umgebungsvariable (optional)", + "systemAiApiKeyDisclaimer": "Studio liest oder speichert den Key-Wert nie — nur den Namen der Umgebungsvariable. Der Hub liest `${name}` zur Anfragezeit.", + "@systemAiApiKeyDisclaimer": { "placeholders": { "name": { "type": "String" } } }, + "systemAiTestConnection": "Verbindung testen", + "systemAiTesting": "Teste…", + "systemAiSave": "Speichern", + "systemAiPrivacyHeader": "PRIVACY-MODUS", + "systemAiPrivacyOffLabel": "Aus", + "systemAiPrivacyOffDesc": "Feature deaktiviert. Keine Anfragen verlassen den Hub.", + "systemAiPrivacyRedactedLabel": "Redigiert", + "systemAiPrivacyRedactedDesc": "Nur event_type / error / Modul-Namen. KRITIS-freundlicher Standard.", + "systemAiPrivacyFullLabel": "Voll", + "systemAiPrivacyFullDesc": "Enthält das Audit-Detail-JSON (nur Hash — keine Roh-Payloads).", + "systemAiConnectionOk": "Verbindung ok", + "systemAiConnectionFailed": "Verbindung fehlgeschlagen", + "systemAiReplyPrefix": "Antwort: {text}", + "@systemAiReplyPrefix": { "placeholders": { "text": { "type": "String" } } }, + "systemAiModelLabel": "Modell (erforderlich)", + "systemAiModelHintFallback": "Modell-Identifier, den der Provider erwartet", + "systemAiModelHelperBase": "Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen.", + "systemAiModelHelperOllama": "Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen. Mit Pull von Ollama herunterladen.", + "systemAiRefresh": "Aktualisieren", + "systemAiPull": "Pull", + "systemAiPulling": "Lade…", + "systemAiCouldNotListModels": "Modelle konnten nicht aufgelistet werden: {error}", + "@systemAiCouldNotListModels": { "placeholders": { "error": { "type": "String" } } }, + "systemAiNoModelsOllama": "Noch keine Modelle auf dem Ollama-Server. Oben eintippen und Pull klicken.", + "systemAiNoModelsGeneric": "Provider lieferte keine Modelle.", + "systemAiSuitabilityRecommended": "empfohlen", + "systemAiSuitabilityBalanced": "ausgewogen", + "systemAiSuitabilitySmall": "klein — Qualität evtl. eingeschränkt", + "systemAiSuitabilityLarge": "groß — langsamer", + "systemAiSuitabilityHuge": "sehr groß — auf einem Laptop wahrscheinlich zu langsam", + "systemAiSuitabilityUnknown": "Größe unbekannt", + "systemAiHwDetected": "Erkannt: {summary}", + "@systemAiHwDetected": { "placeholders": { "summary": { "type": "String" } } }, + "systemAiHwReviewed": " · Kuratierung geprüft {date}", + "@systemAiHwReviewed": { "placeholders": { "date": { "type": "String" } } }, + "systemAiLegendRecommended": "empfohlen für diese Hardware{tier}", + "@systemAiLegendRecommended": { "placeholders": { "tier": { "type": "String" } } }, + "systemAiLegendBalanced": "ausgewogen (4-8B)", + "systemAiLegendSmallLarge": "klein <3B / groß 9-15B / über Hardware", + "systemAiLegendHuge": "sehr groß >15B", + "systemAiLegendUnknown": "Größe unbekannt", + "systemAiCacheRow": "Cache: {n} {n, plural, =1{gespeicherte Erklärung} other{gespeicherte Erklärungen}}. Identische Prompts treffen den Cache; Modell- oder Privacy-Modus-Wechsel leeren ihn automatisch.", + "@systemAiCacheRow": { "placeholders": { "n": { "type": "int" } } }, + "systemAiCacheClear": "Leeren", + "systemAiCacheClearedToast": "{n} gespeicherte System-AI-Erklärungen gelöscht.", + "@systemAiCacheClearedToast": { "placeholders": { "n": { "type": "int" } } }, + "systemAiCacheClearFailed": "Cache-Löschen fehlgeschlagen: {error}", + "@systemAiCacheClearFailed": { "placeholders": { "error": { "type": "String" } } }, + "systemAiPullEmptyError": "Erst eine Modell-ID eintippen (z.B. gemma3:4b).", + "systemAiPullFailedError": "Pull fehlgeschlagen: {error}", + "@systemAiPullFailedError": { "placeholders": { "error": { "type": "String" } } } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 13ac351..a1ef61c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -264,6 +264,25 @@ "modulesUninstallFailed": "Uninstall failed: {error}", "@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } }, "modulesUninstalling": "Uninstalling…", + "moduleSheetFailedToLoad": "Failed to load: {error}", + "@moduleSheetFailedToLoad": { "placeholders": { "error": { "type": "String" } } }, + "moduleSheetCapabilities": "Capabilities", + "moduleSheetPermissions": "Declared permissions", + "moduleSheetNoPermissions": "(none — pure-computation module)", + + "flowsTitle": "Flows", + "flowsReloadTooltip": "Reload", + "flowsNoneTitle": "No saved flows", + "flowsNoneHint": "Save a flow with `fai admin flows save ` and it shows up here.", + "flowsRunDialogTitle": "Run {name}", + "@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } }, + "flowsRunDialogBody": "Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.", + "flowsInputsHint": "name=World\ntarget_language=English\nsummary_style=three bullet points", + "flowsRunButton": "Run", + "flowsRunningTitle": "Running {name}", + "@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } }, + "flowsRunning": "Flow running…", + "flowsNoOutputs": "Flow completed with no declared outputs.", "approvalsTitle": "Approvals", "approvalsTabPending": "Pending", @@ -412,5 +431,65 @@ "@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } }, "searchStoreHintWithCategory": "store · {category}", "@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } }, - "searchSavedFlowHint": "saved flow" + "searchSavedFlowHint": "saved flow", + + "systemAiTitle": "System AI", + "systemAiIntro": "The hub-internal LLM the platform itself uses for inline failure explanations and operator help. Off by default; configure here. See docs/architecture/system-ai.md for what crosses the wire.", + "systemAiProviderLabel": "Provider", + "systemAiEndpointLabel": "Endpoint URL (up to /v1)", + "systemAiApiKeyRequired": "API key env var (required)", + "systemAiApiKeyOptional": "API key env var (optional)", + "systemAiApiKeyDisclaimer": "Studio never reads or stores the key value — only its env-var name. The hub reads `${name}` at request time.", + "@systemAiApiKeyDisclaimer": { "placeholders": { "name": { "type": "String" } } }, + "systemAiTestConnection": "Test connection", + "systemAiTesting": "Testing…", + "systemAiSave": "Save", + "systemAiPrivacyHeader": "PRIVACY MODE", + "systemAiPrivacyOffLabel": "Off", + "systemAiPrivacyOffDesc": "Feature disabled. No requests leave the hub.", + "systemAiPrivacyRedactedLabel": "Redacted", + "systemAiPrivacyRedactedDesc": "Only event_type / error / module names. KRITIS-friendly default.", + "systemAiPrivacyFullLabel": "Full", + "systemAiPrivacyFullDesc": "Includes the audit detail JSON (hash-only — no raw payloads).", + "systemAiConnectionOk": "Connection ok", + "systemAiConnectionFailed": "Connection failed", + "systemAiReplyPrefix": "Reply: {text}", + "@systemAiReplyPrefix": { "placeholders": { "text": { "type": "String" } } }, + "systemAiModelLabel": "Model (required)", + "systemAiModelHintFallback": "model identifier the provider expects", + "systemAiModelHelperBase": "Required: pick from the list (Refresh) or type one.", + "systemAiModelHelperOllama": "Required: pick from the list (Refresh) or type one. Use Pull to download from Ollama.", + "systemAiRefresh": "Refresh", + "systemAiPull": "Pull", + "systemAiPulling": "Pulling…", + "systemAiCouldNotListModels": "Could not list models: {error}", + "@systemAiCouldNotListModels": { "placeholders": { "error": { "type": "String" } } }, + "systemAiNoModelsOllama": "No models on the Ollama server yet. Type one above and click Pull.", + "systemAiNoModelsGeneric": "Provider returned no models.", + "systemAiSuitabilityRecommended": "recommended", + "systemAiSuitabilityBalanced": "balanced", + "systemAiSuitabilitySmall": "small — quality may be limited", + "systemAiSuitabilityLarge": "large — slower", + "systemAiSuitabilityHuge": "huge — likely too slow on a laptop", + "systemAiSuitabilityUnknown": "size unknown", + "systemAiHwDetected": "Detected: {summary}", + "@systemAiHwDetected": { "placeholders": { "summary": { "type": "String" } } }, + "systemAiHwReviewed": " · curation reviewed {date}", + "@systemAiHwReviewed": { "placeholders": { "date": { "type": "String" } } }, + "systemAiLegendRecommended": "recommended for this hardware{tier}", + "@systemAiLegendRecommended": { "placeholders": { "tier": { "type": "String" } } }, + "systemAiLegendBalanced": "balanced (4-8B)", + "systemAiLegendSmallLarge": "small <3B / large 9-15B / above hardware", + "systemAiLegendHuge": "huge >15B", + "systemAiLegendUnknown": "size unknown", + "systemAiCacheRow": "Cache: {n} cached {n, plural, =1{explanation} other{explanations}}. Identical prompts hit the cache; switching model or privacy mode flushes automatically.", + "@systemAiCacheRow": { "placeholders": { "n": { "type": "int" } } }, + "systemAiCacheClear": "Clear", + "systemAiCacheClearedToast": "Cleared {n} cached System-AI explanations.", + "@systemAiCacheClearedToast": { "placeholders": { "n": { "type": "int" } } }, + "systemAiCacheClearFailed": "Cache clear failed: {error}", + "@systemAiCacheClearFailed": { "placeholders": { "error": { "type": "String" } } }, + "systemAiPullEmptyError": "Type a model id (e.g. gemma3:4b) first.", + "systemAiPullFailedError": "Pull failed: {error}", + "@systemAiPullFailedError": { "placeholders": { "error": { "type": "String" } } } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 94382a5..a3e7e03 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1196,6 +1196,96 @@ abstract class AppLocalizations { /// **'Uninstalling…'** String get modulesUninstalling; + /// No description provided for @moduleSheetFailedToLoad. + /// + /// In en, this message translates to: + /// **'Failed to load: {error}'** + String moduleSheetFailedToLoad(String error); + + /// No description provided for @moduleSheetCapabilities. + /// + /// In en, this message translates to: + /// **'Capabilities'** + String get moduleSheetCapabilities; + + /// No description provided for @moduleSheetPermissions. + /// + /// In en, this message translates to: + /// **'Declared permissions'** + String get moduleSheetPermissions; + + /// No description provided for @moduleSheetNoPermissions. + /// + /// In en, this message translates to: + /// **'(none — pure-computation module)'** + String get moduleSheetNoPermissions; + + /// No description provided for @flowsTitle. + /// + /// In en, this message translates to: + /// **'Flows'** + String get flowsTitle; + + /// No description provided for @flowsReloadTooltip. + /// + /// In en, this message translates to: + /// **'Reload'** + String get flowsReloadTooltip; + + /// No description provided for @flowsNoneTitle. + /// + /// In en, this message translates to: + /// **'No saved flows'** + String get flowsNoneTitle; + + /// No description provided for @flowsNoneHint. + /// + /// In en, this message translates to: + /// **'Save a flow with `fai admin flows save ` and it shows up here.'** + String get flowsNoneHint; + + /// No description provided for @flowsRunDialogTitle. + /// + /// In en, this message translates to: + /// **'Run {name}'** + String flowsRunDialogTitle(String name); + + /// No description provided for @flowsRunDialogBody. + /// + /// In en, this message translates to: + /// **'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.'** + String get flowsRunDialogBody; + + /// No description provided for @flowsInputsHint. + /// + /// In en, this message translates to: + /// **'name=World\ntarget_language=English\nsummary_style=three bullet points'** + String get flowsInputsHint; + + /// No description provided for @flowsRunButton. + /// + /// In en, this message translates to: + /// **'Run'** + String get flowsRunButton; + + /// No description provided for @flowsRunningTitle. + /// + /// In en, this message translates to: + /// **'Running {name}'** + String flowsRunningTitle(String name); + + /// No description provided for @flowsRunning. + /// + /// In en, this message translates to: + /// **'Flow running…'** + String get flowsRunning; + + /// No description provided for @flowsNoOutputs. + /// + /// In en, this message translates to: + /// **'Flow completed with no declared outputs.'** + String get flowsNoOutputs; + /// No description provided for @approvalsTitle. /// /// In en, this message translates to: @@ -1783,6 +1873,300 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'saved flow'** String get searchSavedFlowHint; + + /// No description provided for @systemAiTitle. + /// + /// In en, this message translates to: + /// **'System AI'** + String get systemAiTitle; + + /// No description provided for @systemAiIntro. + /// + /// In en, this message translates to: + /// **'The hub-internal LLM the platform itself uses for inline failure explanations and operator help. Off by default; configure here. See docs/architecture/system-ai.md for what crosses the wire.'** + String get systemAiIntro; + + /// No description provided for @systemAiProviderLabel. + /// + /// In en, this message translates to: + /// **'Provider'** + String get systemAiProviderLabel; + + /// No description provided for @systemAiEndpointLabel. + /// + /// In en, this message translates to: + /// **'Endpoint URL (up to /v1)'** + String get systemAiEndpointLabel; + + /// No description provided for @systemAiApiKeyRequired. + /// + /// In en, this message translates to: + /// **'API key env var (required)'** + String get systemAiApiKeyRequired; + + /// No description provided for @systemAiApiKeyOptional. + /// + /// In en, this message translates to: + /// **'API key env var (optional)'** + String get systemAiApiKeyOptional; + + /// No description provided for @systemAiApiKeyDisclaimer. + /// + /// In en, this message translates to: + /// **'Studio never reads or stores the key value — only its env-var name. The hub reads `\${name}` at request time.'** + String systemAiApiKeyDisclaimer(String name); + + /// No description provided for @systemAiTestConnection. + /// + /// In en, this message translates to: + /// **'Test connection'** + String get systemAiTestConnection; + + /// No description provided for @systemAiTesting. + /// + /// In en, this message translates to: + /// **'Testing…'** + String get systemAiTesting; + + /// No description provided for @systemAiSave. + /// + /// In en, this message translates to: + /// **'Save'** + String get systemAiSave; + + /// No description provided for @systemAiPrivacyHeader. + /// + /// In en, this message translates to: + /// **'PRIVACY MODE'** + String get systemAiPrivacyHeader; + + /// No description provided for @systemAiPrivacyOffLabel. + /// + /// In en, this message translates to: + /// **'Off'** + String get systemAiPrivacyOffLabel; + + /// No description provided for @systemAiPrivacyOffDesc. + /// + /// In en, this message translates to: + /// **'Feature disabled. No requests leave the hub.'** + String get systemAiPrivacyOffDesc; + + /// No description provided for @systemAiPrivacyRedactedLabel. + /// + /// In en, this message translates to: + /// **'Redacted'** + String get systemAiPrivacyRedactedLabel; + + /// No description provided for @systemAiPrivacyRedactedDesc. + /// + /// In en, this message translates to: + /// **'Only event_type / error / module names. KRITIS-friendly default.'** + String get systemAiPrivacyRedactedDesc; + + /// No description provided for @systemAiPrivacyFullLabel. + /// + /// In en, this message translates to: + /// **'Full'** + String get systemAiPrivacyFullLabel; + + /// No description provided for @systemAiPrivacyFullDesc. + /// + /// In en, this message translates to: + /// **'Includes the audit detail JSON (hash-only — no raw payloads).'** + String get systemAiPrivacyFullDesc; + + /// No description provided for @systemAiConnectionOk. + /// + /// In en, this message translates to: + /// **'Connection ok'** + String get systemAiConnectionOk; + + /// No description provided for @systemAiConnectionFailed. + /// + /// In en, this message translates to: + /// **'Connection failed'** + String get systemAiConnectionFailed; + + /// No description provided for @systemAiReplyPrefix. + /// + /// In en, this message translates to: + /// **'Reply: {text}'** + String systemAiReplyPrefix(String text); + + /// No description provided for @systemAiModelLabel. + /// + /// In en, this message translates to: + /// **'Model (required)'** + String get systemAiModelLabel; + + /// No description provided for @systemAiModelHintFallback. + /// + /// In en, this message translates to: + /// **'model identifier the provider expects'** + String get systemAiModelHintFallback; + + /// No description provided for @systemAiModelHelperBase. + /// + /// In en, this message translates to: + /// **'Required: pick from the list (Refresh) or type one.'** + String get systemAiModelHelperBase; + + /// No description provided for @systemAiModelHelperOllama. + /// + /// In en, this message translates to: + /// **'Required: pick from the list (Refresh) or type one. Use Pull to download from Ollama.'** + String get systemAiModelHelperOllama; + + /// No description provided for @systemAiRefresh. + /// + /// In en, this message translates to: + /// **'Refresh'** + String get systemAiRefresh; + + /// No description provided for @systemAiPull. + /// + /// In en, this message translates to: + /// **'Pull'** + String get systemAiPull; + + /// No description provided for @systemAiPulling. + /// + /// In en, this message translates to: + /// **'Pulling…'** + String get systemAiPulling; + + /// No description provided for @systemAiCouldNotListModels. + /// + /// In en, this message translates to: + /// **'Could not list models: {error}'** + String systemAiCouldNotListModels(String error); + + /// No description provided for @systemAiNoModelsOllama. + /// + /// In en, this message translates to: + /// **'No models on the Ollama server yet. Type one above and click Pull.'** + String get systemAiNoModelsOllama; + + /// No description provided for @systemAiNoModelsGeneric. + /// + /// In en, this message translates to: + /// **'Provider returned no models.'** + String get systemAiNoModelsGeneric; + + /// No description provided for @systemAiSuitabilityRecommended. + /// + /// In en, this message translates to: + /// **'recommended'** + String get systemAiSuitabilityRecommended; + + /// No description provided for @systemAiSuitabilityBalanced. + /// + /// In en, this message translates to: + /// **'balanced'** + String get systemAiSuitabilityBalanced; + + /// No description provided for @systemAiSuitabilitySmall. + /// + /// In en, this message translates to: + /// **'small — quality may be limited'** + String get systemAiSuitabilitySmall; + + /// No description provided for @systemAiSuitabilityLarge. + /// + /// In en, this message translates to: + /// **'large — slower'** + String get systemAiSuitabilityLarge; + + /// No description provided for @systemAiSuitabilityHuge. + /// + /// In en, this message translates to: + /// **'huge — likely too slow on a laptop'** + String get systemAiSuitabilityHuge; + + /// No description provided for @systemAiSuitabilityUnknown. + /// + /// In en, this message translates to: + /// **'size unknown'** + String get systemAiSuitabilityUnknown; + + /// No description provided for @systemAiHwDetected. + /// + /// In en, this message translates to: + /// **'Detected: {summary}'** + String systemAiHwDetected(String summary); + + /// No description provided for @systemAiHwReviewed. + /// + /// In en, this message translates to: + /// **' · curation reviewed {date}'** + String systemAiHwReviewed(String date); + + /// No description provided for @systemAiLegendRecommended. + /// + /// In en, this message translates to: + /// **'recommended for this hardware{tier}'** + String systemAiLegendRecommended(String tier); + + /// No description provided for @systemAiLegendBalanced. + /// + /// In en, this message translates to: + /// **'balanced (4-8B)'** + String get systemAiLegendBalanced; + + /// No description provided for @systemAiLegendSmallLarge. + /// + /// In en, this message translates to: + /// **'small <3B / large 9-15B / above hardware'** + String get systemAiLegendSmallLarge; + + /// No description provided for @systemAiLegendHuge. + /// + /// In en, this message translates to: + /// **'huge >15B'** + String get systemAiLegendHuge; + + /// No description provided for @systemAiLegendUnknown. + /// + /// In en, this message translates to: + /// **'size unknown'** + String get systemAiLegendUnknown; + + /// No description provided for @systemAiCacheRow. + /// + /// In en, this message translates to: + /// **'Cache: {n} cached {n, plural, =1{explanation} other{explanations}}. Identical prompts hit the cache; switching model or privacy mode flushes automatically.'** + String systemAiCacheRow(int n); + + /// No description provided for @systemAiCacheClear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get systemAiCacheClear; + + /// No description provided for @systemAiCacheClearedToast. + /// + /// In en, this message translates to: + /// **'Cleared {n} cached System-AI explanations.'** + String systemAiCacheClearedToast(int n); + + /// No description provided for @systemAiCacheClearFailed. + /// + /// In en, this message translates to: + /// **'Cache clear failed: {error}'** + String systemAiCacheClearFailed(String error); + + /// No description provided for @systemAiPullEmptyError. + /// + /// In en, this message translates to: + /// **'Type a model id (e.g. gemma3:4b) first.'** + String get systemAiPullEmptyError; + + /// No description provided for @systemAiPullFailedError. + /// + /// In en, this message translates to: + /// **'Pull failed: {error}'** + String systemAiPullFailedError(String error); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 3652ec7..c96c274 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -663,6 +663,60 @@ class AppLocalizationsDe extends AppLocalizations { @override String get modulesUninstalling => 'Wird deinstalliert…'; + @override + String moduleSheetFailedToLoad(String error) { + return 'Laden fehlgeschlagen: $error'; + } + + @override + String get moduleSheetCapabilities => 'Capabilities'; + + @override + String get moduleSheetPermissions => 'Deklarierte Berechtigungen'; + + @override + String get moduleSheetNoPermissions => '(keine — reines Berechnungsmodul)'; + + @override + String get flowsTitle => 'Flows'; + + @override + String get flowsReloadTooltip => 'Aktualisieren'; + + @override + String get flowsNoneTitle => 'Keine gespeicherten Flows'; + + @override + String get flowsNoneHint => + 'Flow speichern mit `fai admin flows save `, dann taucht er hier auf.'; + + @override + String flowsRunDialogTitle(String name) { + return '$name ausführen'; + } + + @override + String get flowsRunDialogBody => + 'Eingaben als key=value, eine pro Zeile. Alle Werte werden als Text-Payloads gesendet. Für binäre Eingaben die `fai run` CLI nutzen.'; + + @override + String get flowsInputsHint => + 'name=World\ntarget_language=English\nsummary_style=three bullet points'; + + @override + String get flowsRunButton => 'Starten'; + + @override + String flowsRunningTitle(String name) { + return '$name läuft'; + } + + @override + String get flowsRunning => 'Flow läuft…'; + + @override + String get flowsNoOutputs => 'Flow abgeschlossen ohne deklarierte Outputs.'; + @override String get approvalsTitle => 'Freigaben'; @@ -1005,4 +1059,189 @@ class AppLocalizationsDe extends AppLocalizations { @override String get searchSavedFlowHint => 'gespeicherter Flow'; + + @override + String get systemAiTitle => 'System-AI'; + + @override + String get systemAiIntro => + 'Die hub-interne LLM, die die Plattform selbst für Fehlererklärungen und Operator-Hilfe nutzt. Standardmäßig aus; hier konfigurieren. Siehe docs/architecture/system-ai.md, was über die Leitung geht.'; + + @override + String get systemAiProviderLabel => 'Provider'; + + @override + String get systemAiEndpointLabel => 'Endpunkt-URL (bis /v1)'; + + @override + String get systemAiApiKeyRequired => + 'API-Key-Umgebungsvariable (erforderlich)'; + + @override + String get systemAiApiKeyOptional => 'API-Key-Umgebungsvariable (optional)'; + + @override + String systemAiApiKeyDisclaimer(String name) { + return 'Studio liest oder speichert den Key-Wert nie — nur den Namen der Umgebungsvariable. Der Hub liest `\$$name` zur Anfragezeit.'; + } + + @override + String get systemAiTestConnection => 'Verbindung testen'; + + @override + String get systemAiTesting => 'Teste…'; + + @override + String get systemAiSave => 'Speichern'; + + @override + String get systemAiPrivacyHeader => 'PRIVACY-MODUS'; + + @override + String get systemAiPrivacyOffLabel => 'Aus'; + + @override + String get systemAiPrivacyOffDesc => + 'Feature deaktiviert. Keine Anfragen verlassen den Hub.'; + + @override + String get systemAiPrivacyRedactedLabel => 'Redigiert'; + + @override + String get systemAiPrivacyRedactedDesc => + 'Nur event_type / error / Modul-Namen. KRITIS-freundlicher Standard.'; + + @override + String get systemAiPrivacyFullLabel => 'Voll'; + + @override + String get systemAiPrivacyFullDesc => + 'Enthält das Audit-Detail-JSON (nur Hash — keine Roh-Payloads).'; + + @override + String get systemAiConnectionOk => 'Verbindung ok'; + + @override + String get systemAiConnectionFailed => 'Verbindung fehlgeschlagen'; + + @override + String systemAiReplyPrefix(String text) { + return 'Antwort: $text'; + } + + @override + String get systemAiModelLabel => 'Modell (erforderlich)'; + + @override + String get systemAiModelHintFallback => + 'Modell-Identifier, den der Provider erwartet'; + + @override + String get systemAiModelHelperBase => + 'Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen.'; + + @override + String get systemAiModelHelperOllama => + 'Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen. Mit Pull von Ollama herunterladen.'; + + @override + String get systemAiRefresh => 'Aktualisieren'; + + @override + String get systemAiPull => 'Pull'; + + @override + String get systemAiPulling => 'Lade…'; + + @override + String systemAiCouldNotListModels(String error) { + return 'Modelle konnten nicht aufgelistet werden: $error'; + } + + @override + String get systemAiNoModelsOllama => + 'Noch keine Modelle auf dem Ollama-Server. Oben eintippen und Pull klicken.'; + + @override + String get systemAiNoModelsGeneric => 'Provider lieferte keine Modelle.'; + + @override + String get systemAiSuitabilityRecommended => 'empfohlen'; + + @override + String get systemAiSuitabilityBalanced => 'ausgewogen'; + + @override + String get systemAiSuitabilitySmall => 'klein — Qualität evtl. eingeschränkt'; + + @override + String get systemAiSuitabilityLarge => 'groß — langsamer'; + + @override + String get systemAiSuitabilityHuge => + 'sehr groß — auf einem Laptop wahrscheinlich zu langsam'; + + @override + String get systemAiSuitabilityUnknown => 'Größe unbekannt'; + + @override + String systemAiHwDetected(String summary) { + return 'Erkannt: $summary'; + } + + @override + String systemAiHwReviewed(String date) { + return ' · Kuratierung geprüft $date'; + } + + @override + String systemAiLegendRecommended(String tier) { + return 'empfohlen für diese Hardware$tier'; + } + + @override + String get systemAiLegendBalanced => 'ausgewogen (4-8B)'; + + @override + String get systemAiLegendSmallLarge => + 'klein <3B / groß 9-15B / über Hardware'; + + @override + String get systemAiLegendHuge => 'sehr groß >15B'; + + @override + String get systemAiLegendUnknown => 'Größe unbekannt'; + + @override + String systemAiCacheRow(int n) { + String _temp0 = intl.Intl.pluralLogic( + n, + locale: localeName, + other: 'gespeicherte Erklärungen', + one: 'gespeicherte Erklärung', + ); + return 'Cache: $n $_temp0. Identische Prompts treffen den Cache; Modell- oder Privacy-Modus-Wechsel leeren ihn automatisch.'; + } + + @override + String get systemAiCacheClear => 'Leeren'; + + @override + String systemAiCacheClearedToast(int n) { + return '$n gespeicherte System-AI-Erklärungen gelöscht.'; + } + + @override + String systemAiCacheClearFailed(String error) { + return 'Cache-Löschen fehlgeschlagen: $error'; + } + + @override + String get systemAiPullEmptyError => + 'Erst eine Modell-ID eintippen (z.B. gemma3:4b).'; + + @override + String systemAiPullFailedError(String error) { + return 'Pull fehlgeschlagen: $error'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index eaa054f..04e1463 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -679,6 +679,60 @@ class AppLocalizationsEn extends AppLocalizations { @override String get modulesUninstalling => 'Uninstalling…'; + @override + String moduleSheetFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String get moduleSheetCapabilities => 'Capabilities'; + + @override + String get moduleSheetPermissions => 'Declared permissions'; + + @override + String get moduleSheetNoPermissions => '(none — pure-computation module)'; + + @override + String get flowsTitle => 'Flows'; + + @override + String get flowsReloadTooltip => 'Reload'; + + @override + String get flowsNoneTitle => 'No saved flows'; + + @override + String get flowsNoneHint => + 'Save a flow with `fai admin flows save ` and it shows up here.'; + + @override + String flowsRunDialogTitle(String name) { + return 'Run $name'; + } + + @override + String get flowsRunDialogBody => + 'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.'; + + @override + String get flowsInputsHint => + 'name=World\ntarget_language=English\nsummary_style=three bullet points'; + + @override + String get flowsRunButton => 'Run'; + + @override + String flowsRunningTitle(String name) { + return 'Running $name'; + } + + @override + String get flowsRunning => 'Flow running…'; + + @override + String get flowsNoOutputs => 'Flow completed with no declared outputs.'; + @override String get approvalsTitle => 'Approvals'; @@ -1017,4 +1071,187 @@ class AppLocalizationsEn extends AppLocalizations { @override String get searchSavedFlowHint => 'saved flow'; + + @override + String get systemAiTitle => 'System AI'; + + @override + String get systemAiIntro => + 'The hub-internal LLM the platform itself uses for inline failure explanations and operator help. Off by default; configure here. See docs/architecture/system-ai.md for what crosses the wire.'; + + @override + String get systemAiProviderLabel => 'Provider'; + + @override + String get systemAiEndpointLabel => 'Endpoint URL (up to /v1)'; + + @override + String get systemAiApiKeyRequired => 'API key env var (required)'; + + @override + String get systemAiApiKeyOptional => 'API key env var (optional)'; + + @override + String systemAiApiKeyDisclaimer(String name) { + return 'Studio never reads or stores the key value — only its env-var name. The hub reads `\$$name` at request time.'; + } + + @override + String get systemAiTestConnection => 'Test connection'; + + @override + String get systemAiTesting => 'Testing…'; + + @override + String get systemAiSave => 'Save'; + + @override + String get systemAiPrivacyHeader => 'PRIVACY MODE'; + + @override + String get systemAiPrivacyOffLabel => 'Off'; + + @override + String get systemAiPrivacyOffDesc => + 'Feature disabled. No requests leave the hub.'; + + @override + String get systemAiPrivacyRedactedLabel => 'Redacted'; + + @override + String get systemAiPrivacyRedactedDesc => + 'Only event_type / error / module names. KRITIS-friendly default.'; + + @override + String get systemAiPrivacyFullLabel => 'Full'; + + @override + String get systemAiPrivacyFullDesc => + 'Includes the audit detail JSON (hash-only — no raw payloads).'; + + @override + String get systemAiConnectionOk => 'Connection ok'; + + @override + String get systemAiConnectionFailed => 'Connection failed'; + + @override + String systemAiReplyPrefix(String text) { + return 'Reply: $text'; + } + + @override + String get systemAiModelLabel => 'Model (required)'; + + @override + String get systemAiModelHintFallback => + 'model identifier the provider expects'; + + @override + String get systemAiModelHelperBase => + 'Required: pick from the list (Refresh) or type one.'; + + @override + String get systemAiModelHelperOllama => + 'Required: pick from the list (Refresh) or type one. Use Pull to download from Ollama.'; + + @override + String get systemAiRefresh => 'Refresh'; + + @override + String get systemAiPull => 'Pull'; + + @override + String get systemAiPulling => 'Pulling…'; + + @override + String systemAiCouldNotListModels(String error) { + return 'Could not list models: $error'; + } + + @override + String get systemAiNoModelsOllama => + 'No models on the Ollama server yet. Type one above and click Pull.'; + + @override + String get systemAiNoModelsGeneric => 'Provider returned no models.'; + + @override + String get systemAiSuitabilityRecommended => 'recommended'; + + @override + String get systemAiSuitabilityBalanced => 'balanced'; + + @override + String get systemAiSuitabilitySmall => 'small — quality may be limited'; + + @override + String get systemAiSuitabilityLarge => 'large — slower'; + + @override + String get systemAiSuitabilityHuge => 'huge — likely too slow on a laptop'; + + @override + String get systemAiSuitabilityUnknown => 'size unknown'; + + @override + String systemAiHwDetected(String summary) { + return 'Detected: $summary'; + } + + @override + String systemAiHwReviewed(String date) { + return ' · curation reviewed $date'; + } + + @override + String systemAiLegendRecommended(String tier) { + return 'recommended for this hardware$tier'; + } + + @override + String get systemAiLegendBalanced => 'balanced (4-8B)'; + + @override + String get systemAiLegendSmallLarge => + 'small <3B / large 9-15B / above hardware'; + + @override + String get systemAiLegendHuge => 'huge >15B'; + + @override + String get systemAiLegendUnknown => 'size unknown'; + + @override + String systemAiCacheRow(int n) { + String _temp0 = intl.Intl.pluralLogic( + n, + locale: localeName, + other: 'explanations', + one: 'explanation', + ); + return 'Cache: $n cached $_temp0. Identical prompts hit the cache; switching model or privacy mode flushes automatically.'; + } + + @override + String get systemAiCacheClear => 'Clear'; + + @override + String systemAiCacheClearedToast(int n) { + return 'Cleared $n cached System-AI explanations.'; + } + + @override + String systemAiCacheClearFailed(String error) { + return 'Cache clear failed: $error'; + } + + @override + String get systemAiPullEmptyError => + 'Type a model id (e.g. gemma3:4b) first.'; + + @override + String systemAiPullFailedError(String error) { + return 'Pull failed: $error'; + } } diff --git a/lib/main.dart b/lib/main.dart index fdd6e3b..614c6a9 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.27.0'; +const String kStudioVersion = '0.28.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/flows.dart b/lib/pages/flows.dart index caef12f..3efa1f8 100644 --- a/lib/pages/flows.dart +++ b/lib/pages/flows.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../data/hub.dart'; +import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import '../widgets/widgets.dart'; @@ -38,14 +39,15 @@ class _FlowsPageState extends State { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - title: const Text('Flows'), + title: Text(l.flowsTitle), actions: [ IconButton( icon: const Icon(Icons.refresh, size: 18), - tooltip: 'Reload', + tooltip: l.flowsReloadTooltip, onPressed: _refresh, ), const SizedBox(width: FaiSpace.sm), @@ -61,21 +63,20 @@ class _FlowsPageState 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`.', + title: l.hubUnreachable, + hint: l.hubUnreachableHint, action: FilledButton.tonal( onPressed: _refresh, - child: const Text('Retry'), + child: Text(l.buttonRetry), ), ); } final flows = snapshot.data ?? []; if (flows.isEmpty) { - return const FaiEmptyState( + return FaiEmptyState( icon: Icons.account_tree_outlined, - title: 'No saved flows', - hint: - 'Save a flow with `fai admin flows save ` and it shows up here.', + title: l.flowsNoneTitle, + hint: l.flowsNoneHint, ); } return ListView.separated( @@ -139,7 +140,7 @@ class _FlowCard extends StatelessWidget { FilledButton.icon( onPressed: onRun, icon: const Icon(Icons.play_arrow, size: 16), - label: const Text('Run'), + label: Text(AppLocalizations.of(context)!.flowsRunButton), ), ], ), @@ -194,8 +195,9 @@ class _FlowInputDialogState extends State<_FlowInputDialog> { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return AlertDialog( - title: Text('Run ${widget.flow.name}'), + title: Text(l.flowsRunDialogTitle(widget.flow.name)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.md), ), @@ -206,7 +208,7 @@ class _FlowInputDialogState extends State<_FlowInputDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.', + l.flowsRunDialogBody, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -219,10 +221,9 @@ class _FlowInputDialogState extends State<_FlowInputDialog> { expands: true, textAlignVertical: TextAlignVertical.top, style: FaiTheme.mono(size: 12), - decoration: const InputDecoration( - hintText: - 'name=World\ntarget_language=English\nsummary_style=three bullet points', - border: OutlineInputBorder(), + decoration: InputDecoration( + hintText: l.flowsInputsHint, + border: const OutlineInputBorder(), isDense: true, ), ), @@ -233,11 +234,11 @@ class _FlowInputDialogState extends State<_FlowInputDialog> { actions: [ TextButton( onPressed: () => Navigator.pop(context, null), - child: const Text('Cancel'), + child: Text(l.buttonCancel), ), FilledButton.icon( icon: const Icon(Icons.play_arrow, size: 16), - label: const Text('Run'), + label: Text(l.flowsRunButton), onPressed: () => Navigator.pop(context, _parsePairs()), ), ], @@ -270,8 +271,9 @@ class _FlowRunDialogState extends State<_FlowRunDialog> { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return AlertDialog( - title: Text('Running ${widget.flow.name}'), + title: Text(l.flowsRunningTitle(widget.flow.name)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.md), ), @@ -288,7 +290,7 @@ class _FlowRunDialogState extends State<_FlowRunDialog> { const CircularProgressIndicator(), const SizedBox(height: FaiSpace.md), Text( - 'Flow running…', + l.flowsRunning, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -322,7 +324,7 @@ class _FlowRunDialogState extends State<_FlowRunDialog> { final outputs = snapshot.data ?? const {}; if (outputs.isEmpty) { return Text( - 'Flow completed with no declared outputs.', + l.flowsNoOutputs, style: theme.textTheme.bodyMedium, ); } @@ -368,7 +370,7 @@ class _FlowRunDialogState extends State<_FlowRunDialog> { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(l.buttonClose), ), ], ); diff --git a/lib/widgets/fai_module_sheet.dart b/lib/widgets/fai_module_sheet.dart index 049bb52..40ab70a 100644 --- a/lib/widgets/fai_module_sheet.dart +++ b/lib/widgets/fai_module_sheet.dart @@ -114,7 +114,8 @@ class _FaiModuleSheetState extends State { Padding( padding: const EdgeInsets.all(FaiSpace.xl), child: Text( - 'Failed to load: ${snapshot.error}', + AppLocalizations.of(context)! + .moduleSheetFailedToLoad(snapshot.error.toString()), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, ), @@ -168,6 +169,7 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return ListView( padding: const EdgeInsets.fromLTRB( FaiSpace.xxl, @@ -202,7 +204,7 @@ class _Body extends StatelessWidget { ), ), const SizedBox(height: FaiSpace.xl), - _SectionHeader('Capabilities'), + _SectionHeader(l.moduleSheetCapabilities), const SizedBox(height: FaiSpace.sm), Wrap( spacing: FaiSpace.xs, @@ -218,11 +220,11 @@ class _Body extends StatelessWidget { .toList(), ), const SizedBox(height: FaiSpace.xl), - _SectionHeader('Declared permissions'), + _SectionHeader(l.moduleSheetPermissions), const SizedBox(height: FaiSpace.sm), if (detail.permissions.isEmpty) Text( - '(none — pure-computation module)', + l.moduleSheetNoPermissions, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, diff --git a/lib/widgets/fai_system_ai_editor.dart b/lib/widgets/fai_system_ai_editor.dart index 55d75f4..39c262e 100644 --- a/lib/widgets/fai_system_ai_editor.dart +++ b/lib/widgets/fai_system_ai_editor.dart @@ -7,6 +7,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'; @@ -244,19 +245,18 @@ class _FaiSystemAiEditorState extends State { try { final purged = await HubService.instance.clearSystemLlmCache(); if (!mounted) return; + final l = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cleared $purged cached System-AI explanations.')), + SnackBar(content: Text(l.systemAiCacheClearedToast(purged))), ); - // Pop with the same status (since the editor's `initial` - // is now stale w.r.t. cacheCount); caller will re-fetch - // status on next open. setState(() { // Force a rebuild that hides the row; cheap. }); } catch (e) { if (!mounted) return; + final l = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cache clear failed: $e')), + SnackBar(content: Text(l.systemAiCacheClearFailed(e.toString()))), ); } } @@ -315,8 +315,9 @@ class _FaiSystemAiEditorState extends State { Future _pullModel() async { final wanted = _model.text.trim(); + final l = AppLocalizations.of(context)!; if (wanted.isEmpty) { - setState(() => _error = 'Type a model id (e.g. gemma3:4b) first.'); + setState(() => _error = l.systemAiPullEmptyError); return; } setState(() { @@ -332,7 +333,7 @@ class _FaiSystemAiEditorState extends State { setState(() { _pulling = false; if (r.errorKind.isNotEmpty) { - _error = 'Pull failed: ${r.text}'; + _error = l.systemAiPullFailedError(r.text); } else { _error = null; } @@ -347,8 +348,9 @@ class _FaiSystemAiEditorState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return AlertDialog( - title: const Text('System AI'), + title: Text(l.systemAiTitle), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.md), ), @@ -364,9 +366,7 @@ class _FaiSystemAiEditorState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'The hub-internal LLM the platform itself uses for inline failure ' - 'explanations and operator help. Off by default; configure here. ' - 'See docs/architecture/system-ai.md for what crosses the wire.', + l.systemAiIntro, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -391,9 +391,9 @@ class _FaiSystemAiEditorState extends State { TextField( controller: _endpoint, style: FaiTheme.mono(size: 12), - decoration: const InputDecoration( - labelText: 'Endpoint URL (up to /v1)', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: l.systemAiEndpointLabel, + border: const OutlineInputBorder(), isDense: true, ), ), @@ -420,8 +420,8 @@ class _FaiSystemAiEditorState extends State { style: FaiTheme.mono(size: 12), decoration: InputDecoration( labelText: _preset.wire == 'openai' - ? 'API key env var (required)' - : 'API key env var (optional)', + ? l.systemAiApiKeyRequired + : l.systemAiApiKeyOptional, hintText: 'OPENAI_API_KEY', prefixText: '\$', border: const OutlineInputBorder(), @@ -430,8 +430,7 @@ class _FaiSystemAiEditorState extends State { ), const SizedBox(height: 4), Text( - 'Studio never reads or stores the key value — only its env-var name. ' - 'The hub reads `\$` at request time.', + l.systemAiApiKeyDisclaimer(''), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -470,12 +469,12 @@ class _FaiSystemAiEditorState extends State { onPressed: _saving || _testing ? null : () => Navigator.pop(context, null), - child: const Text('Cancel'), + child: Text(l.buttonCancel), ), OutlinedButton.icon( onPressed: _saving || _testing ? null : _test, icon: const Icon(Icons.play_arrow, size: 16), - label: Text(_testing ? 'Testing…' : 'Test connection'), + label: Text(_testing ? l.systemAiTesting : l.systemAiTestConnection), ), FilledButton( onPressed: _saving || _testing ? null : () => _save(), @@ -485,7 +484,7 @@ class _FaiSystemAiEditorState extends State { height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Text('Save'), + : Text(l.systemAiSave), ), ], ); @@ -503,12 +502,13 @@ class _ProviderDropdown extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return DropdownButtonFormField( initialValue: value.wire, isDense: true, - decoration: const InputDecoration( - labelText: 'Provider', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: l.systemAiProviderLabel, + border: const OutlineInputBorder(), isDense: true, ), items: _ProviderPreset.all @@ -539,24 +539,25 @@ class _PrivacyModeChips extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - const modes = <(String, String, String)>[ - ('off', 'Off', 'Feature disabled. No requests leave the hub.'), + final l = AppLocalizations.of(context)!; + final modes = <(String, String, String)>[ + ('off', l.systemAiPrivacyOffLabel, l.systemAiPrivacyOffDesc), ( 'redacted', - 'Redacted', - 'Only event_type / error / module names. KRITIS-friendly default.', + l.systemAiPrivacyRedactedLabel, + l.systemAiPrivacyRedactedDesc, ), ( 'full', - 'Full', - 'Includes the audit detail JSON (hash-only — no raw payloads).', + l.systemAiPrivacyFullLabel, + l.systemAiPrivacyFullDesc, ), ]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'PRIVACY MODE', + l.systemAiPrivacyHeader, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, letterSpacing: 0.6, @@ -611,6 +612,7 @@ class _TestResultPanel extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final ok = result.isSuccess; final color = ok ? FaiColors.success : theme.colorScheme.error; return Container( @@ -633,7 +635,7 @@ class _TestResultPanel extends StatelessWidget { ), const SizedBox(width: FaiSpace.xs), Text( - ok ? 'Connection ok' : 'Connection failed', + ok ? l.systemAiConnectionOk : l.systemAiConnectionFailed, style: theme.textTheme.bodyMedium?.copyWith( color: color, ), @@ -649,7 +651,7 @@ class _TestResultPanel extends StatelessWidget { ), const SizedBox(height: 4), SelectableText( - ok ? 'Reply: ${result.text}' : result.text, + ok ? l.systemAiReplyPrefix(result.text) : result.text, style: FaiTheme.mono( size: 11, color: theme.colorScheme.onSurface, @@ -707,6 +709,7 @@ class _ModelPicker extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -720,12 +723,14 @@ class _ModelPicker extends StatelessWidget { controller: controller, style: FaiTheme.mono(size: 12), decoration: InputDecoration( - labelText: 'Model (required)', + labelText: l.systemAiModelLabel, hintText: preset.modelHint.isEmpty - ? 'model identifier the provider expects' + ? l.systemAiModelHintFallback : preset.modelHint, helperText: controller.text.trim().isEmpty - ? 'Required: pick from the list (Refresh) or type one. ${_isOllama ? "Use Pull to download from Ollama." : ""}' + ? (_isOllama + ? l.systemAiModelHelperOllama + : l.systemAiModelHelperBase) : null, helperMaxLines: 2, border: const OutlineInputBorder(), @@ -744,7 +749,7 @@ class _ModelPicker extends StatelessWidget { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh, size: 16), - label: const Text('Refresh'), + label: Text(l.systemAiRefresh), ), if (_isOllama) ...[ const SizedBox(width: FaiSpace.sm), @@ -757,7 +762,7 @@ class _ModelPicker extends StatelessWidget { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.download, size: 16), - label: Text(pulling ? 'Pulling…' : 'Pull'), + label: Text(pulling ? l.systemAiPulling : l.systemAiPull), ), ], ], @@ -765,7 +770,7 @@ class _ModelPicker extends StatelessWidget { if (modelsError != null) ...[ const SizedBox(height: 4), Text( - 'Could not list models: $modelsError', + l.systemAiCouldNotListModels(modelsError!), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), @@ -776,8 +781,8 @@ class _ModelPicker extends StatelessWidget { if (models!.isEmpty) Text( _isOllama - ? 'No models on the Ollama server yet. Type one above and click Pull.' - : 'Provider returned no models.', + ? l.systemAiNoModelsOllama + : l.systemAiNoModelsGeneric, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -827,20 +832,21 @@ enum _Suitability { } extension _SuitabilityCopy on _Suitability { - String get label { + String labelFor(BuildContext context) { + final l = AppLocalizations.of(context)!; switch (this) { case _Suitability.recommended: - return 'recommended'; + return l.systemAiSuitabilityRecommended; case _Suitability.balanced: - return 'balanced'; + return l.systemAiSuitabilityBalanced; case _Suitability.small: - return 'small — quality may be limited'; + return l.systemAiSuitabilitySmall; case _Suitability.large: - return 'large — slower'; + return l.systemAiSuitabilityLarge; case _Suitability.huge: - return 'huge — likely too slow on a laptop'; + return l.systemAiSuitabilityHuge; case _Suitability.unknown: - return 'size unknown'; + return l.systemAiSuitabilityUnknown; } } @@ -970,8 +976,8 @@ class _ModelChip extends StatelessWidget { required this.onTap, }); - String _tooltipFor(BuildContext _) { - final lines = [suitability.label]; + String _tooltipFor(BuildContext context) { + final lines = [suitability.labelFor(context)]; final c = curated; if (c != null) { lines.add( @@ -1023,8 +1029,9 @@ class _HardwareBanner extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final lr = (lastReviewed != null && lastReviewed!.isNotEmpty) - ? ' · curation reviewed $lastReviewed' + ? l.systemAiHwReviewed(lastReviewed!) : ''; return Container( padding: const EdgeInsets.symmetric( @@ -1046,7 +1053,7 @@ class _HardwareBanner extends StatelessWidget { const SizedBox(width: 6), Expanded( child: Text( - 'Detected: ${hw.summary}$lr', + '${l.systemAiHwDetected(hw.summary)}$lr', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -1065,6 +1072,7 @@ class _SuitabilityLegend extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; Widget dot(Color c, String label) => Padding( padding: const EdgeInsets.only(right: FaiSpace.md), child: Row( @@ -1094,7 +1102,7 @@ class _SuitabilityLegend extends StatelessWidget { Icon(Icons.star, size: 11, color: FaiColors.success), const SizedBox(width: 4), Text( - 'recommended for this hardware$tierTag', + l.systemAiLegendRecommended(tierTag), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontSize: 11, @@ -1103,10 +1111,10 @@ class _SuitabilityLegend extends StatelessWidget { ], ), ), - dot(theme.colorScheme.primary, 'balanced (4-8B)'), - dot(FaiColors.warning, 'small <3B / large 9-15B / above hardware'), - dot(theme.colorScheme.error, 'huge >15B'), - dot(theme.colorScheme.outline, 'size unknown'), + dot(theme.colorScheme.primary, l.systemAiLegendBalanced), + dot(FaiColors.warning, l.systemAiLegendSmallLarge), + dot(theme.colorScheme.error, l.systemAiLegendHuge), + dot(theme.colorScheme.outline, l.systemAiLegendUnknown), ], ); } @@ -1126,6 +1134,7 @@ class _CacheStatusRow extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return Container( padding: const EdgeInsets.symmetric( horizontal: FaiSpace.sm, @@ -1146,9 +1155,7 @@ class _CacheStatusRow extends StatelessWidget { const SizedBox(width: 6), Expanded( child: Text( - 'Cache: $cacheCount cached explanation${cacheCount == 1 ? "" : "s"}. ' - 'Identical prompts hit the cache; switching model or privacy ' - 'mode flushes automatically.', + l.systemAiCacheRow(cacheCount), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -1157,7 +1164,7 @@ class _CacheStatusRow extends StatelessWidget { TextButton.icon( onPressed: onClear, icon: const Icon(Icons.delete_outline, size: 14), - label: const Text('Clear'), + label: Text(l.systemAiCacheClear), ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 23d20d1..4e8fbe9 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.27.0 +version: 0.28.0 environment: sdk: ^3.11.0-200.1.beta