From e2b2639a866a84a79f737f88409fd40326a3d601 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 8 May 2026 02:49:18 +0200 Subject: [PATCH] feat(studio): Store + Cmd+K palette i18n (v0.27.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Localize Store page: search hint, category and status filter chips (All / Published / Alpha / Planned / Installed), pluralized result count, hub-unreachable / no-matches empty states, featured strip header, store-card pills (installed / update / version), Install / Update / Details / Read docs / Uninstall / Not installable buttons, install-progress dialog, uninstall confirm dialog, install / uninstall toasts, open-browser failure toast. - Localize detail-sheet sections: Tags, Required capabilities, Required host services, Screenshots, Documentation, Source. - Localize docs panel: Load documentation button, fetching spinner copy, Open repository in browser button, friendly error mapping (not_found / no_docs / auth / network / parse). - Localize Cmd+K palette: search hint, group labels (Pages / Modules / Store / Flows), keyboard hint footer, indexing / no-matches states, dynamic-hit copy ("installed module · v{version}", "saved flow", "uncategorized"). Static page hits now flow through the localized "Pages" group; Settings entry uses the localized label and hint. Status pill values ("published", "alpha", "planned") stay as data-derived English strings since they map directly to the store-index `status:` enum. Signed-off-by: flemming-it --- lib/l10n/app_de.arb | 85 +++++++- lib/l10n/app_en.arb | 85 +++++++- lib/l10n/app_localizations.dart | 312 ++++++++++++++++++++++++++++ lib/l10n/app_localizations_de.dart | 191 +++++++++++++++++ lib/l10n/app_localizations_en.dart | 190 +++++++++++++++++ lib/main.dart | 14 +- lib/pages/store.dart | 180 ++++++++-------- lib/widgets/fai_search_palette.dart | 40 ++-- pubspec.yaml | 2 +- 9 files changed, 990 insertions(+), 109 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 65c0627..ddcbd47 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -117,6 +117,66 @@ "@storeNResults": { "placeholders": { "n": { "type": "int" } } }, + "storeReloadTooltip": "Aktualisieren", + "storePillInstalled": "installiert", + "storePillUpdate": "Update", + "storeUpdateButton": "Update auf {version}", + "@storeUpdateButton": { "placeholders": { "version": { "type": "String" } } }, + "storeUpdateTooltip": "Installiert {installed} — Store bietet v{best}", + "@storeUpdateTooltip": { + "placeholders": { + "installed": { "type": "String" }, + "best": { "type": "String" } + } + }, + "storeNoTagline": "(keine Beschreibung)", + "storeUninstallTitle": "Modul deinstallieren?", + "storeUninstallBody": "Entfernt {name} aus diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl.", + "@storeUninstallBody": { "placeholders": { "name": { "type": "String" } } }, + "storeInstalledToast": "{name} v{version} installiert.", + "@storeInstalledToast": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "storeInstallFailedToast": "Installation fehlgeschlagen: {error}", + "@storeInstallFailedToast": { "placeholders": { "error": { "type": "String" } } }, + "storeUninstalledToast": "{name} v{version} deinstalliert.", + "@storeUninstalledToast": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "storeUninstallFailedToast": "Deinstallation fehlgeschlagen: {error}", + "@storeUninstallFailedToast": { "placeholders": { "error": { "type": "String" } } }, + "storeOpenBrowserFailed": "Browser konnte nicht geöffnet werden: {error}", + "@storeOpenBrowserFailed": { "placeholders": { "error": { "type": "String" } } }, + "storeSectionTags": "Tags", + "storeSectionRequiredCapabilities": "Erforderliche Capabilities", + "storeSectionRequiredHostServices": "Erforderliche Host-Dienste", + "storeSectionScreenshots": "Screenshots", + "storeSectionDocumentation": "Dokumentation", + "storeSectionSource": "Quelle", + "storeInstallingButton": "Installiere…", + "storeNotInstallable": "Nicht installierbar", + "storeNotInstallableTooltip": "Status „{status}\" — Installations-Pfad noch nicht verdrahtet.", + "@storeNotInstallableTooltip": { "placeholders": { "status": { "type": "String" } } }, + "storeInstallProgressTitle": "Installation von {name}", + "@storeInstallProgressTitle": { "placeholders": { "name": { "type": "String" } } }, + "storeInstallProgressBody": "Holen, verifizieren, entpacken…", + "storeLoadDocs": "Dokumentation laden", + "storeDocsHint": "README inline gerendert; nutzt das Registry-Token des Hubs, falls nötig.", + "storeDocsFetching": "README wird geladen…", + "storeOpenRepoButton": "Repository im Browser öffnen", + "storeDocsErrorNotFound": "Dieses Modul ist nicht im Store-Index des Hubs enthalten.", + "storeDocsErrorNoDocs": "Für dieses Modul ist keine Repository-URL hinterlegt.", + "storeDocsErrorAuth": "Die Registry erfordert Authentifizierung, um dieses README zu laden. Setze FAI_REGISTRY_TOKEN in der Umgebung des Daemons und starte den Hub neu.", + "storeDocsErrorNetwork": "Netzwerkfehler beim Laden des README. {detail}", + "@storeDocsErrorNetwork": { "placeholders": { "detail": { "type": "String" } } }, + "storeDocsErrorParse": "README ist kein gültiges UTF-8.", + "storeDocsErrorGeneric": "README konnte nicht geladen werden.", "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", @@ -328,5 +388,28 @@ "hubUnreachableHint": "Hub starten mit `fai serve`.", "languageEnglish": "English", - "languageGerman": "Deutsch" + "languageGerman": "Deutsch", + + "searchHint": "Springe überall hin — Module, Store, Flows, Seiten…", + "searchIndexing": "Indiziere…", + "searchNoMatches": "Keine Treffer.", + "searchHintNavigate": "↑↓ navigieren", + "searchHintOpen": "Enter öffnen", + "searchHintClose": "Esc schließen", + "searchGroupPages": "Seiten", + "searchGroupModules": "Module", + "searchGroupStore": "Store", + "searchGroupFlows": "Flows", + "searchSettingsLabel": "Einstellungen", + "searchPageHint": "Seite · ⌘{n}", + "@searchPageHint": { "placeholders": { "n": { "type": "int" } } }, + "searchSettingsHint": "Hub-Endpunkt, Kanäle, System-AI · ⌘;", + "searchInstalledModuleHint": "installiertes Modul · v{version}", + "@searchInstalledModuleHint": { "placeholders": { "version": { "type": "String" } } }, + "searchStoreUncategorized": "ohne Kategorie", + "searchStoreHintWithTagline": "Store · {tagline}", + "@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } }, + "searchStoreHintWithCategory": "Store · {category}", + "@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } }, + "searchSavedFlowHint": "gespeicherter Flow" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d913050..13ac351 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -118,6 +118,66 @@ "@storeNResults": { "placeholders": { "n": { "type": "int" } } }, + "storeReloadTooltip": "Reload", + "storePillInstalled": "installed", + "storePillUpdate": "update", + "storeUpdateButton": "Update to {version}", + "@storeUpdateButton": { "placeholders": { "version": { "type": "String" } } }, + "storeUpdateTooltip": "Installed {installed} — store has v{best}", + "@storeUpdateTooltip": { + "placeholders": { + "installed": { "type": "String" }, + "best": { "type": "String" } + } + }, + "storeNoTagline": "(no tagline)", + "storeUninstallTitle": "Uninstall module?", + "storeUninstallBody": "Removes {name} from this hub. Flows that reference it will fail at the next run.", + "@storeUninstallBody": { "placeholders": { "name": { "type": "String" } } }, + "storeInstalledToast": "{name} v{version} installed.", + "@storeInstalledToast": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "storeInstallFailedToast": "Install failed: {error}", + "@storeInstallFailedToast": { "placeholders": { "error": { "type": "String" } } }, + "storeUninstalledToast": "{name} v{version} uninstalled.", + "@storeUninstalledToast": { + "placeholders": { + "name": { "type": "String" }, + "version": { "type": "String" } + } + }, + "storeUninstallFailedToast": "Uninstall failed: {error}", + "@storeUninstallFailedToast": { "placeholders": { "error": { "type": "String" } } }, + "storeOpenBrowserFailed": "Could not open browser: {error}", + "@storeOpenBrowserFailed": { "placeholders": { "error": { "type": "String" } } }, + "storeSectionTags": "Tags", + "storeSectionRequiredCapabilities": "Required capabilities", + "storeSectionRequiredHostServices": "Required host services", + "storeSectionScreenshots": "Screenshots", + "storeSectionDocumentation": "Documentation", + "storeSectionSource": "Source", + "storeInstallingButton": "Installing…", + "storeNotInstallable": "Not installable", + "storeNotInstallableTooltip": "Status \"{status}\" — install path not yet wired.", + "@storeNotInstallableTooltip": { "placeholders": { "status": { "type": "String" } } }, + "storeInstallProgressTitle": "Installing {name}", + "@storeInstallProgressTitle": { "placeholders": { "name": { "type": "String" } } }, + "storeInstallProgressBody": "Fetching, verifying, unpacking…", + "storeLoadDocs": "Load documentation", + "storeDocsHint": "README rendered inline; uses the hub's registry token when needed.", + "storeDocsFetching": "Fetching README…", + "storeOpenRepoButton": "Open repository in browser", + "storeDocsErrorNotFound": "This module is not in the hub's store index.", + "storeDocsErrorNoDocs": "No repository URL is recorded for this module.", + "storeDocsErrorAuth": "The registry requires authentication to fetch this README. Set FAI_REGISTRY_TOKEN in the daemon's environment and restart the hub.", + "storeDocsErrorNetwork": "Network error while fetching the README. {detail}", + "@storeDocsErrorNetwork": { "placeholders": { "detail": { "type": "String" } } }, + "storeDocsErrorParse": "README is not valid UTF-8.", + "storeDocsErrorGeneric": "Could not load README.", "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", @@ -329,5 +389,28 @@ "hubUnreachableHint": "Start the hub with `fai serve`.", "languageEnglish": "English", - "languageGerman": "Deutsch" + "languageGerman": "Deutsch", + + "searchHint": "Jump anywhere — modules, store, flows, pages…", + "searchIndexing": "Indexing…", + "searchNoMatches": "No matches.", + "searchHintNavigate": "↑↓ navigate", + "searchHintOpen": "enter open", + "searchHintClose": "esc close", + "searchGroupPages": "Pages", + "searchGroupModules": "Modules", + "searchGroupStore": "Store", + "searchGroupFlows": "Flows", + "searchSettingsLabel": "Settings", + "searchPageHint": "page · ⌘{n}", + "@searchPageHint": { "placeholders": { "n": { "type": "int" } } }, + "searchSettingsHint": "hub endpoint, channels, system AI · ⌘;", + "searchInstalledModuleHint": "installed module · v{version}", + "@searchInstalledModuleHint": { "placeholders": { "version": { "type": "String" } } }, + "searchStoreUncategorized": "uncategorized", + "searchStoreHintWithTagline": "store · {tagline}", + "@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } }, + "searchStoreHintWithCategory": "store · {category}", + "@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } }, + "searchSavedFlowHint": "saved flow" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f819b65..94382a5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -686,6 +686,210 @@ abstract class AppLocalizations { /// **'{n} result{n, plural, =1{} other{s}}'** String storeNResults(int n); + /// No description provided for @storeReloadTooltip. + /// + /// In en, this message translates to: + /// **'Reload'** + String get storeReloadTooltip; + + /// No description provided for @storePillInstalled. + /// + /// In en, this message translates to: + /// **'installed'** + String get storePillInstalled; + + /// No description provided for @storePillUpdate. + /// + /// In en, this message translates to: + /// **'update'** + String get storePillUpdate; + + /// No description provided for @storeUpdateButton. + /// + /// In en, this message translates to: + /// **'Update to {version}'** + String storeUpdateButton(String version); + + /// No description provided for @storeUpdateTooltip. + /// + /// In en, this message translates to: + /// **'Installed {installed} — store has v{best}'** + String storeUpdateTooltip(String installed, String best); + + /// No description provided for @storeNoTagline. + /// + /// In en, this message translates to: + /// **'(no tagline)'** + String get storeNoTagline; + + /// No description provided for @storeUninstallTitle. + /// + /// In en, this message translates to: + /// **'Uninstall module?'** + String get storeUninstallTitle; + + /// No description provided for @storeUninstallBody. + /// + /// In en, this message translates to: + /// **'Removes {name} from this hub. Flows that reference it will fail at the next run.'** + String storeUninstallBody(String name); + + /// No description provided for @storeInstalledToast. + /// + /// In en, this message translates to: + /// **'{name} v{version} installed.'** + String storeInstalledToast(String name, String version); + + /// No description provided for @storeInstallFailedToast. + /// + /// In en, this message translates to: + /// **'Install failed: {error}'** + String storeInstallFailedToast(String error); + + /// No description provided for @storeUninstalledToast. + /// + /// In en, this message translates to: + /// **'{name} v{version} uninstalled.'** + String storeUninstalledToast(String name, String version); + + /// No description provided for @storeUninstallFailedToast. + /// + /// In en, this message translates to: + /// **'Uninstall failed: {error}'** + String storeUninstallFailedToast(String error); + + /// No description provided for @storeOpenBrowserFailed. + /// + /// In en, this message translates to: + /// **'Could not open browser: {error}'** + String storeOpenBrowserFailed(String error); + + /// No description provided for @storeSectionTags. + /// + /// In en, this message translates to: + /// **'Tags'** + String get storeSectionTags; + + /// No description provided for @storeSectionRequiredCapabilities. + /// + /// In en, this message translates to: + /// **'Required capabilities'** + String get storeSectionRequiredCapabilities; + + /// No description provided for @storeSectionRequiredHostServices. + /// + /// In en, this message translates to: + /// **'Required host services'** + String get storeSectionRequiredHostServices; + + /// No description provided for @storeSectionScreenshots. + /// + /// In en, this message translates to: + /// **'Screenshots'** + String get storeSectionScreenshots; + + /// No description provided for @storeSectionDocumentation. + /// + /// In en, this message translates to: + /// **'Documentation'** + String get storeSectionDocumentation; + + /// No description provided for @storeSectionSource. + /// + /// In en, this message translates to: + /// **'Source'** + String get storeSectionSource; + + /// No description provided for @storeInstallingButton. + /// + /// In en, this message translates to: + /// **'Installing…'** + String get storeInstallingButton; + + /// No description provided for @storeNotInstallable. + /// + /// In en, this message translates to: + /// **'Not installable'** + String get storeNotInstallable; + + /// No description provided for @storeNotInstallableTooltip. + /// + /// In en, this message translates to: + /// **'Status \"{status}\" — install path not yet wired.'** + String storeNotInstallableTooltip(String status); + + /// No description provided for @storeInstallProgressTitle. + /// + /// In en, this message translates to: + /// **'Installing {name}'** + String storeInstallProgressTitle(String name); + + /// No description provided for @storeInstallProgressBody. + /// + /// In en, this message translates to: + /// **'Fetching, verifying, unpacking…'** + String get storeInstallProgressBody; + + /// No description provided for @storeLoadDocs. + /// + /// In en, this message translates to: + /// **'Load documentation'** + String get storeLoadDocs; + + /// No description provided for @storeDocsHint. + /// + /// In en, this message translates to: + /// **'README rendered inline; uses the hub\'s registry token when needed.'** + String get storeDocsHint; + + /// No description provided for @storeDocsFetching. + /// + /// In en, this message translates to: + /// **'Fetching README…'** + String get storeDocsFetching; + + /// No description provided for @storeOpenRepoButton. + /// + /// In en, this message translates to: + /// **'Open repository in browser'** + String get storeOpenRepoButton; + + /// No description provided for @storeDocsErrorNotFound. + /// + /// In en, this message translates to: + /// **'This module is not in the hub\'s store index.'** + String get storeDocsErrorNotFound; + + /// No description provided for @storeDocsErrorNoDocs. + /// + /// In en, this message translates to: + /// **'No repository URL is recorded for this module.'** + String get storeDocsErrorNoDocs; + + /// No description provided for @storeDocsErrorAuth. + /// + /// In en, this message translates to: + /// **'The registry requires authentication to fetch this README. Set FAI_REGISTRY_TOKEN in the daemon\'s environment and restart the hub.'** + String get storeDocsErrorAuth; + + /// No description provided for @storeDocsErrorNetwork. + /// + /// In en, this message translates to: + /// **'Network error while fetching the README. {detail}'** + String storeDocsErrorNetwork(String detail); + + /// No description provided for @storeDocsErrorParse. + /// + /// In en, this message translates to: + /// **'README is not valid UTF-8.'** + String get storeDocsErrorParse; + + /// No description provided for @storeDocsErrorGeneric. + /// + /// In en, this message translates to: + /// **'Could not load README.'** + String get storeDocsErrorGeneric; + /// No description provided for @storeFederationNudgeTitle. /// /// In en, this message translates to: @@ -1471,6 +1675,114 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Deutsch'** String get languageGerman; + + /// No description provided for @searchHint. + /// + /// In en, this message translates to: + /// **'Jump anywhere — modules, store, flows, pages…'** + String get searchHint; + + /// No description provided for @searchIndexing. + /// + /// In en, this message translates to: + /// **'Indexing…'** + String get searchIndexing; + + /// No description provided for @searchNoMatches. + /// + /// In en, this message translates to: + /// **'No matches.'** + String get searchNoMatches; + + /// No description provided for @searchHintNavigate. + /// + /// In en, this message translates to: + /// **'↑↓ navigate'** + String get searchHintNavigate; + + /// No description provided for @searchHintOpen. + /// + /// In en, this message translates to: + /// **'enter open'** + String get searchHintOpen; + + /// No description provided for @searchHintClose. + /// + /// In en, this message translates to: + /// **'esc close'** + String get searchHintClose; + + /// No description provided for @searchGroupPages. + /// + /// In en, this message translates to: + /// **'Pages'** + String get searchGroupPages; + + /// No description provided for @searchGroupModules. + /// + /// In en, this message translates to: + /// **'Modules'** + String get searchGroupModules; + + /// No description provided for @searchGroupStore. + /// + /// In en, this message translates to: + /// **'Store'** + String get searchGroupStore; + + /// No description provided for @searchGroupFlows. + /// + /// In en, this message translates to: + /// **'Flows'** + String get searchGroupFlows; + + /// No description provided for @searchSettingsLabel. + /// + /// In en, this message translates to: + /// **'Settings'** + String get searchSettingsLabel; + + /// No description provided for @searchPageHint. + /// + /// In en, this message translates to: + /// **'page · ⌘{n}'** + String searchPageHint(int n); + + /// No description provided for @searchSettingsHint. + /// + /// In en, this message translates to: + /// **'hub endpoint, channels, system AI · ⌘;'** + String get searchSettingsHint; + + /// No description provided for @searchInstalledModuleHint. + /// + /// In en, this message translates to: + /// **'installed module · v{version}'** + String searchInstalledModuleHint(String version); + + /// No description provided for @searchStoreUncategorized. + /// + /// In en, this message translates to: + /// **'uncategorized'** + String get searchStoreUncategorized; + + /// No description provided for @searchStoreHintWithTagline. + /// + /// In en, this message translates to: + /// **'store · {tagline}'** + String searchStoreHintWithTagline(String tagline); + + /// No description provided for @searchStoreHintWithCategory. + /// + /// In en, this message translates to: + /// **'store · {category}'** + String searchStoreHintWithCategory(String category); + + /// No description provided for @searchSavedFlowHint. + /// + /// In en, this message translates to: + /// **'saved flow'** + String get searchSavedFlowHint; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 921913d..3652ec7 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -342,6 +342,134 @@ class AppLocalizationsDe extends AppLocalizations { return '$n Treffer'; } + @override + String get storeReloadTooltip => 'Aktualisieren'; + + @override + String get storePillInstalled => 'installiert'; + + @override + String get storePillUpdate => 'Update'; + + @override + String storeUpdateButton(String version) { + return 'Update auf $version'; + } + + @override + String storeUpdateTooltip(String installed, String best) { + return 'Installiert $installed — Store bietet v$best'; + } + + @override + String get storeNoTagline => '(keine Beschreibung)'; + + @override + String get storeUninstallTitle => 'Modul deinstallieren?'; + + @override + String storeUninstallBody(String name) { + return 'Entfernt $name aus diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl.'; + } + + @override + String storeInstalledToast(String name, String version) { + return '$name v$version installiert.'; + } + + @override + String storeInstallFailedToast(String error) { + return 'Installation fehlgeschlagen: $error'; + } + + @override + String storeUninstalledToast(String name, String version) { + return '$name v$version deinstalliert.'; + } + + @override + String storeUninstallFailedToast(String error) { + return 'Deinstallation fehlgeschlagen: $error'; + } + + @override + String storeOpenBrowserFailed(String error) { + return 'Browser konnte nicht geöffnet werden: $error'; + } + + @override + String get storeSectionTags => 'Tags'; + + @override + String get storeSectionRequiredCapabilities => 'Erforderliche Capabilities'; + + @override + String get storeSectionRequiredHostServices => 'Erforderliche Host-Dienste'; + + @override + String get storeSectionScreenshots => 'Screenshots'; + + @override + String get storeSectionDocumentation => 'Dokumentation'; + + @override + String get storeSectionSource => 'Quelle'; + + @override + String get storeInstallingButton => 'Installiere…'; + + @override + String get storeNotInstallable => 'Nicht installierbar'; + + @override + String storeNotInstallableTooltip(String status) { + return 'Status „$status\" — Installations-Pfad noch nicht verdrahtet.'; + } + + @override + String storeInstallProgressTitle(String name) { + return 'Installation von $name'; + } + + @override + String get storeInstallProgressBody => 'Holen, verifizieren, entpacken…'; + + @override + String get storeLoadDocs => 'Dokumentation laden'; + + @override + String get storeDocsHint => + 'README inline gerendert; nutzt das Registry-Token des Hubs, falls nötig.'; + + @override + String get storeDocsFetching => 'README wird geladen…'; + + @override + String get storeOpenRepoButton => 'Repository im Browser öffnen'; + + @override + String get storeDocsErrorNotFound => + 'Dieses Modul ist nicht im Store-Index des Hubs enthalten.'; + + @override + String get storeDocsErrorNoDocs => + 'Für dieses Modul ist keine Repository-URL hinterlegt.'; + + @override + String get storeDocsErrorAuth => + 'Die Registry erfordert Authentifizierung, um dieses README zu laden. Setze FAI_REGISTRY_TOKEN in der Umgebung des Daemons und starte den Hub neu.'; + + @override + String storeDocsErrorNetwork(String detail) { + return 'Netzwerkfehler beim Laden des README. $detail'; + } + + @override + String get storeDocsErrorParse => 'README ist kein gültiges UTF-8.'; + + @override + String get storeDocsErrorGeneric => 'README konnte nicht geladen werden.'; + @override String get storeFederationNudgeTitle => 'Mehr Capabilities?'; @@ -814,4 +942,67 @@ class AppLocalizationsDe extends AppLocalizations { @override String get languageGerman => 'Deutsch'; + + @override + String get searchHint => + 'Springe überall hin — Module, Store, Flows, Seiten…'; + + @override + String get searchIndexing => 'Indiziere…'; + + @override + String get searchNoMatches => 'Keine Treffer.'; + + @override + String get searchHintNavigate => '↑↓ navigieren'; + + @override + String get searchHintOpen => 'Enter öffnen'; + + @override + String get searchHintClose => 'Esc schließen'; + + @override + String get searchGroupPages => 'Seiten'; + + @override + String get searchGroupModules => 'Module'; + + @override + String get searchGroupStore => 'Store'; + + @override + String get searchGroupFlows => 'Flows'; + + @override + String get searchSettingsLabel => 'Einstellungen'; + + @override + String searchPageHint(int n) { + return 'Seite · ⌘$n'; + } + + @override + String get searchSettingsHint => 'Hub-Endpunkt, Kanäle, System-AI · ⌘;'; + + @override + String searchInstalledModuleHint(String version) { + return 'installiertes Modul · v$version'; + } + + @override + String get searchStoreUncategorized => 'ohne Kategorie'; + + @override + String searchStoreHintWithTagline(String tagline) { + return 'Store · $tagline'; + } + + @override + String searchStoreHintWithCategory(String category) { + return 'Store · $category'; + } + + @override + String get searchSavedFlowHint => 'gespeicherter Flow'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 457c2a1..eaa054f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -360,6 +360,134 @@ class AppLocalizationsEn extends AppLocalizations { return '$n result$_temp0'; } + @override + String get storeReloadTooltip => 'Reload'; + + @override + String get storePillInstalled => 'installed'; + + @override + String get storePillUpdate => 'update'; + + @override + String storeUpdateButton(String version) { + return 'Update to $version'; + } + + @override + String storeUpdateTooltip(String installed, String best) { + return 'Installed $installed — store has v$best'; + } + + @override + String get storeNoTagline => '(no tagline)'; + + @override + String get storeUninstallTitle => 'Uninstall module?'; + + @override + String storeUninstallBody(String name) { + return 'Removes $name from this hub. Flows that reference it will fail at the next run.'; + } + + @override + String storeInstalledToast(String name, String version) { + return '$name v$version installed.'; + } + + @override + String storeInstallFailedToast(String error) { + return 'Install failed: $error'; + } + + @override + String storeUninstalledToast(String name, String version) { + return '$name v$version uninstalled.'; + } + + @override + String storeUninstallFailedToast(String error) { + return 'Uninstall failed: $error'; + } + + @override + String storeOpenBrowserFailed(String error) { + return 'Could not open browser: $error'; + } + + @override + String get storeSectionTags => 'Tags'; + + @override + String get storeSectionRequiredCapabilities => 'Required capabilities'; + + @override + String get storeSectionRequiredHostServices => 'Required host services'; + + @override + String get storeSectionScreenshots => 'Screenshots'; + + @override + String get storeSectionDocumentation => 'Documentation'; + + @override + String get storeSectionSource => 'Source'; + + @override + String get storeInstallingButton => 'Installing…'; + + @override + String get storeNotInstallable => 'Not installable'; + + @override + String storeNotInstallableTooltip(String status) { + return 'Status \"$status\" — install path not yet wired.'; + } + + @override + String storeInstallProgressTitle(String name) { + return 'Installing $name'; + } + + @override + String get storeInstallProgressBody => 'Fetching, verifying, unpacking…'; + + @override + String get storeLoadDocs => 'Load documentation'; + + @override + String get storeDocsHint => + 'README rendered inline; uses the hub\'s registry token when needed.'; + + @override + String get storeDocsFetching => 'Fetching README…'; + + @override + String get storeOpenRepoButton => 'Open repository in browser'; + + @override + String get storeDocsErrorNotFound => + 'This module is not in the hub\'s store index.'; + + @override + String get storeDocsErrorNoDocs => + 'No repository URL is recorded for this module.'; + + @override + String get storeDocsErrorAuth => + 'The registry requires authentication to fetch this README. Set FAI_REGISTRY_TOKEN in the daemon\'s environment and restart the hub.'; + + @override + String storeDocsErrorNetwork(String detail) { + return 'Network error while fetching the README. $detail'; + } + + @override + String get storeDocsErrorParse => 'README is not valid UTF-8.'; + + @override + String get storeDocsErrorGeneric => 'Could not load README.'; + @override String get storeFederationNudgeTitle => 'Want more capabilities?'; @@ -827,4 +955,66 @@ class AppLocalizationsEn extends AppLocalizations { @override String get languageGerman => 'Deutsch'; + + @override + String get searchHint => 'Jump anywhere — modules, store, flows, pages…'; + + @override + String get searchIndexing => 'Indexing…'; + + @override + String get searchNoMatches => 'No matches.'; + + @override + String get searchHintNavigate => '↑↓ navigate'; + + @override + String get searchHintOpen => 'enter open'; + + @override + String get searchHintClose => 'esc close'; + + @override + String get searchGroupPages => 'Pages'; + + @override + String get searchGroupModules => 'Modules'; + + @override + String get searchGroupStore => 'Store'; + + @override + String get searchGroupFlows => 'Flows'; + + @override + String get searchSettingsLabel => 'Settings'; + + @override + String searchPageHint(int n) { + return 'page · ⌘$n'; + } + + @override + String get searchSettingsHint => 'hub endpoint, channels, system AI · ⌘;'; + + @override + String searchInstalledModuleHint(String version) { + return 'installed module · v$version'; + } + + @override + String get searchStoreUncategorized => 'uncategorized'; + + @override + String searchStoreHintWithTagline(String tagline) { + return 'store · $tagline'; + } + + @override + String searchStoreHintWithCategory(String category) { + return 'store · $category'; + } + + @override + String get searchSavedFlowHint => 'saved flow'; } diff --git a/lib/main.dart b/lib/main.dart index 6b5898d..fdd6e3b 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.25.0'; +const String kStudioVersion = '0.27.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -215,23 +215,25 @@ class _StudioShellState extends State { } void _openSearchPalette() { + final l = AppLocalizations.of(context)!; + final pagesGroup = l.searchGroupPages; final hits = [ for (var i = 0; i < _pages.length; i++) FaiSearchHit( label: _pages[i].labelOf(context), - hint: 'page · ⌘${i + 1}', + hint: l.searchPageHint(i + 1), icon: _pages[i].icon, - group: 'Pages', + group: pagesGroup, onSelect: () { Navigator.of(context).pop(); setState(() => _selectedIndex = i); }, ), FaiSearchHit( - label: 'Settings', - hint: 'hub endpoint, channels, system AI · ⌘;', + label: l.searchSettingsLabel, + hint: l.searchSettingsHint, icon: Icons.settings_outlined, - group: 'Pages', + group: pagesGroup, onSelect: () { Navigator.of(context).pop(); FaiSettingsDialog.show(context); diff --git a/lib/pages/store.dart b/lib/pages/store.dart index c5e3276..4707ef2 100644 --- a/lib/pages/store.dart +++ b/lib/pages/store.dart @@ -99,10 +99,11 @@ class _StorePageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( - title: const Text('Store'), + title: Text(l.searchGroupStore), actions: [ // The DE/EN toggle now lives in the sidebar footer // and flips the entire app's locale. The Store @@ -110,7 +111,7 @@ class _StorePageState extends State { // _locale reads from `Localizations.localeOf`. IconButton( icon: const Icon(Icons.refresh, size: 18), - tooltip: 'Reload', + tooltip: l.storeReloadTooltip, onPressed: _runSearch, ), const SizedBox(width: FaiSpace.sm), @@ -181,11 +182,11 @@ class _StorePageState extends State { return FaiEmptyState( icon: Icons.cloud_off_outlined, iconColor: theme.colorScheme.error, - title: 'Hub unreachable', - hint: 'Start the hub with `fai daemon start`.', + title: l.hubUnreachable, + hint: l.hubUnreachableHint, action: FilledButton.tonal( onPressed: _runSearch, - child: const Text('Retry'), + child: Text(l.buttonRetry), ), ); } @@ -193,9 +194,8 @@ class _StorePageState extends State { if (items.isEmpty) { return FaiEmptyState( icon: Icons.search_off, - title: 'No matches', - hint: - 'Adjust the filters or clear the search to see all entries.', + title: l.storeNoMatches, + hint: l.storeNoMatchesHint, action: FilledButton.tonal( onPressed: () { setState(() { @@ -206,7 +206,7 @@ class _StorePageState extends State { }); _runSearch(); }, - child: const Text('Clear filters'), + child: Text(l.storeClearFilters), ), ); } @@ -293,6 +293,7 @@ class _SearchField extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return ValueListenableBuilder( valueListenable: controller, builder: (_, value, _) { @@ -300,7 +301,7 @@ class _SearchField extends StatelessWidget { controller: controller, decoration: InputDecoration( prefixIcon: const Icon(Icons.search, size: 20), - hintText: 'Search modules — name, tagline, tag, capability…', + hintText: l.storeSearchHint, border: const OutlineInputBorder(), isDense: true, suffixIcon: value.text.isEmpty @@ -334,6 +335,7 @@ class _CategoryStrip extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; if (categories.isEmpty) return const SizedBox.shrink(); return SizedBox( height: 36, @@ -341,7 +343,7 @@ class _CategoryStrip extends StatelessWidget { scrollDirection: Axis.horizontal, children: [ _ChoiceChip( - label: 'All', + label: l.storeStatusAll, selected: selected.isEmpty, onSelected: () => onSelected(''), ), @@ -375,38 +377,39 @@ class _FilterRow extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return Row( children: [ _ChoiceChip( - label: 'All', + label: l.storeStatusAll, selected: status.isEmpty, onSelected: () => onStatus(''), ), _ChoiceChip( - label: 'Published', + label: l.storeStatusPublished, selected: status == 'published', onSelected: () => onStatus('published'), ), _ChoiceChip( - label: 'Alpha', + label: l.storeStatusAlpha, selected: status == 'alpha', onSelected: () => onStatus('alpha'), ), _ChoiceChip( - label: 'Planned', + label: l.storeStatusPlanned, selected: status == 'planned', onSelected: () => onStatus('planned'), ), const SizedBox(width: FaiSpace.md), FilterChip( - label: const Text('Installed'), + label: Text(l.storeInstalledOnly), selected: installedOnly, onSelected: onInstalledOnly, showCheckmark: true, ), const Spacer(), Text( - '$resultCount result${resultCount == 1 ? "" : "s"}', + l.storeNResults(resultCount), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -512,6 +515,7 @@ class _FeaturedStrip extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -520,7 +524,7 @@ class _FeaturedStrip extends StatelessWidget { Icon(Icons.star, size: 14, color: FaiColors.success), const SizedBox(width: 6), Text( - 'FEATURED', + l.storeFeatured, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, letterSpacing: 0.6, @@ -529,7 +533,7 @@ class _FeaturedStrip extends StatelessWidget { ), const SizedBox(width: FaiSpace.sm), Text( - 'curated picks · click for details', + l.storeFeaturedHint, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -582,6 +586,7 @@ class _FeaturedTile extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final tagline = locale == 'de' && item.taglineDe.isNotEmpty ? item.taglineDe : item.taglineEn; @@ -649,7 +654,7 @@ class _FeaturedTile extends StatelessWidget { const SizedBox(height: FaiSpace.md), Expanded( child: Text( - tagline.isEmpty ? '(no tagline)' : tagline, + tagline.isEmpty ? l.storeNoTagline : tagline, style: theme.textTheme.bodyLarge, maxLines: 3, overflow: TextOverflow.ellipsis, @@ -672,8 +677,8 @@ class _FeaturedTile extends StatelessWidget { ), const Spacer(), if (item.installed) - const FaiPill( - label: 'installed', + FaiPill( + label: l.storePillInstalled, tone: FaiPillTone.success, icon: Icons.check, ) @@ -681,7 +686,7 @@ class _FeaturedTile extends StatelessWidget { FilledButton.icon( onPressed: onInstall, icon: const Icon(Icons.download, size: 16), - label: const Text('Install'), + label: Text(l.buttonInstall), ), ], ), @@ -724,6 +729,7 @@ class _StoreCard extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final tagline = locale == 'de' && item.taglineDe.isNotEmpty ? item.taglineDe : item.taglineEn; @@ -775,17 +781,19 @@ class _StoreCard extends StatelessWidget { ), if (_hasUpdate) Tooltip( - message: - 'Installed ${installedVersion ?? "?"} — store has v${item.bestVersion}', - child: const FaiPill( - label: 'update', + message: l.storeUpdateTooltip( + installedVersion ?? '?', + item.bestVersion, + ), + child: FaiPill( + label: l.storePillUpdate, tone: FaiPillTone.warning, icon: Icons.system_update_alt, ), ) else if (item.installed) - const FaiPill( - label: 'installed', + FaiPill( + label: l.storePillInstalled, tone: FaiPillTone.success, icon: Icons.check, ) @@ -799,7 +807,7 @@ class _StoreCard extends StatelessWidget { const SizedBox(height: FaiSpace.sm), Expanded( child: Text( - tagline.isEmpty ? '(no tagline)' : tagline, + tagline.isEmpty ? l.storeNoTagline : tagline, style: theme.textTheme.bodySmall, maxLines: 3, overflow: TextOverflow.ellipsis, @@ -819,7 +827,7 @@ class _StoreCard extends StatelessWidget { FilledButton.icon( onPressed: onInstall, icon: const Icon(Icons.system_update_alt, size: 14), - label: Text('Update to ${item.bestVersion}'), + label: Text(l.storeUpdateButton(item.bestVersion)), style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), @@ -828,13 +836,13 @@ class _StoreCard extends StatelessWidget { TextButton.icon( onPressed: onTap, icon: const Icon(Icons.info_outline, size: 14), - label: const Text('Details'), + label: Text(l.buttonDetails), ) else if (installable) FilledButton.icon( onPressed: onInstall, icon: const Icon(Icons.download, size: 14), - label: const Text('Install'), + label: Text(l.buttonInstall), style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), @@ -843,7 +851,7 @@ class _StoreCard extends StatelessWidget { OutlinedButton.icon( onPressed: onTap, icon: const Icon(Icons.info_outline, size: 14), - label: const Text('Details'), + label: Text(l.buttonDetails), style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, ), @@ -906,33 +914,33 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { try { final r = await HubService.instance.installModule(source: widget.item.name); if (!mounted) return; + final l = AppLocalizations.of(context)!; setState(() { _busy = false; - _toast = '${r.name} v${r.version} installed.'; + _toast = l.storeInstalledToast(r.name, r.version); }); Navigator.pop(context, true); } catch (e) { if (!mounted) return; + final l = AppLocalizations.of(context)!; setState(() { _busy = false; - _toast = 'Install failed: $e'; + _toast = l.storeInstallFailedToast(e.toString()); }); } } Future _uninstall() async { + final l = AppLocalizations.of(context)!; final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Uninstall module?'), - content: Text( - 'Removes ${widget.item.name} from this hub. Flows that ' - 'reference it will fail at the next run.', - ), + title: Text(l.storeUninstallTitle), + content: Text(l.storeUninstallBody(widget.item.name)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(l.buttonCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), @@ -940,7 +948,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { backgroundColor: Theme.of(ctx).colorScheme.error, foregroundColor: Theme.of(ctx).colorScheme.onError, ), - child: const Text('Uninstall'), + child: Text(l.buttonUninstall), ), ], ), @@ -953,16 +961,18 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { try { final r = await HubService.instance.uninstallModule(widget.item.name); if (!mounted) return; + final l2 = AppLocalizations.of(context)!; setState(() { _busy = false; - _toast = '${r.name} v${r.version} uninstalled.'; + _toast = l2.storeUninstalledToast(r.name, r.version); }); Navigator.pop(context, true); } catch (e) { if (!mounted) return; + final l2 = AppLocalizations.of(context)!; setState(() { _busy = false; - _toast = 'Uninstall failed: $e'; + _toast = l2.storeUninstallFailedToast(e.toString()); }); } } @@ -972,8 +982,9 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { final r = await SystemActions.openInOs(widget.item.repository); if (!mounted) return; if (!r.ok) { + final l = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Could not open browser: ${r.stderr}')), + SnackBar(content: Text(l.storeOpenBrowserFailed(r.stderr))), ); } } @@ -990,6 +1001,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final item = widget.item; final tagline = _locale == 'de' && item.taglineDe.isNotEmpty ? item.taglineDe @@ -1069,8 +1081,8 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { tone: FaiPillTone.neutral, ), if (item.installed) - const FaiPill( - label: 'installed', + FaiPill( + label: l.storePillInstalled, tone: FaiPillTone.success, icon: Icons.check, ), @@ -1107,7 +1119,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { const SizedBox(height: FaiSpace.xl), ], if (item.tags.isNotEmpty) ...[ - _SectionHeader('Tags'), + _SectionHeader(l.storeSectionTags), const SizedBox(height: FaiSpace.sm), Wrap( spacing: FaiSpace.xs, @@ -1120,7 +1132,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { const SizedBox(height: FaiSpace.lg), ], if (item.requiresCapabilities.isNotEmpty) ...[ - _SectionHeader('Required capabilities'), + _SectionHeader(l.storeSectionRequiredCapabilities), const SizedBox(height: FaiSpace.sm), Wrap( spacing: FaiSpace.xs, @@ -1137,7 +1149,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { const SizedBox(height: FaiSpace.lg), ], if (item.requiresServices.isNotEmpty) ...[ - _SectionHeader('Required host services'), + _SectionHeader(l.storeSectionRequiredHostServices), const SizedBox(height: FaiSpace.sm), Wrap( spacing: FaiSpace.xs, @@ -1154,7 +1166,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { const SizedBox(height: FaiSpace.lg), ], if (item.screenshotUrls.isNotEmpty) ...[ - _SectionHeader('Screenshots'), + _SectionHeader(l.storeSectionScreenshots), const SizedBox(height: FaiSpace.sm), SizedBox( height: 220, @@ -1171,7 +1183,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { const SizedBox(height: FaiSpace.lg), ], if (item.repository.isNotEmpty || item.docsUrl.isNotEmpty) ...[ - _SectionHeader('Documentation'), + _SectionHeader(l.storeSectionDocumentation), const SizedBox(height: FaiSpace.sm), _DocsPanel( future: _docsFuture, @@ -1179,7 +1191,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { onOpenRepo: _openRepository, ), const SizedBox(height: FaiSpace.lg), - _SectionHeader('Source'), + _SectionHeader(l.storeSectionSource), const SizedBox(height: FaiSpace.sm), InkWell( onTap: _openRepository, @@ -1225,7 +1237,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { OutlinedButton.icon( onPressed: _openRepository, icon: const Icon(Icons.menu_book_outlined, size: 16), - label: const Text('Read docs'), + label: Text(l.buttonReadDocs), ), const Spacer(), if (item.installed) ...[ @@ -1239,7 +1251,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.delete_outline, size: 16), - label: const Text('Uninstall'), + label: Text(l.buttonUninstall), style: OutlinedButton.styleFrom( foregroundColor: theme.colorScheme.error, side: BorderSide( @@ -1259,15 +1271,16 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.download, size: 16), - label: Text(_busy ? 'Installing…' : 'Install'), + label: Text( + _busy ? l.storeInstallingButton : l.buttonInstall, + ), ) else Tooltip( - message: - 'Status "${item.status}" — install path not yet wired.', - child: const FilledButton( + message: l.storeNotInstallableTooltip(item.status), + child: FilledButton( onPressed: null, - child: Text('Not installable'), + child: Text(l.storeNotInstallable), ), ), ], @@ -1324,8 +1337,9 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return AlertDialog( - title: Text('Installing ${widget.item.name}'), + title: Text(l.storeInstallProgressTitle(widget.item.name)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(FaiRadius.md), ), @@ -1342,7 +1356,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> { const CircularProgressIndicator(), const SizedBox(height: FaiSpace.md), Text( - 'Fetching, verifying, unpacking…', + l.storeInstallProgressBody, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -1384,7 +1398,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> { ), const SizedBox(height: FaiSpace.md), Text( - '${r.name} v${r.version} installed.', + l.storeInstalledToast(r.name, r.version), style: theme.textTheme.titleMedium, ), ], @@ -1395,7 +1409,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> { actions: [ TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Close'), + child: Text(l.buttonClose), ), ], ); @@ -1458,17 +1472,18 @@ class _DocsPanel extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; if (future == null) { return Row( children: [ OutlinedButton.icon( onPressed: onLoad, icon: const Icon(Icons.menu_book_outlined, size: 16), - label: const Text('Load documentation'), + label: Text(l.storeLoadDocs), ), const SizedBox(width: FaiSpace.sm), Text( - 'README rendered inline; uses the hub\'s registry token when needed.', + l.storeDocsHint, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -1480,17 +1495,17 @@ class _DocsPanel extends StatelessWidget { future: future, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: FaiSpace.md), + return Padding( + padding: const EdgeInsets.symmetric(vertical: FaiSpace.md), child: Row( children: [ - SizedBox( + const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ), - SizedBox(width: FaiSpace.sm), - Text('Fetching README…'), + const SizedBox(width: FaiSpace.sm), + Text(l.storeDocsFetching), ], ), ); @@ -1504,7 +1519,7 @@ class _DocsPanel extends StatelessWidget { final r = snap.data!; if (r.errorKind.isNotEmpty) { return _DocsErrorPanel( - message: _friendlyDocsError(r.errorKind, r.text), + message: _friendlyDocsError(context, r.errorKind, r.text), onOpenRepo: onOpenRepo, sourceUrl: r.sourceUrl, ); @@ -1599,7 +1614,7 @@ class _DocsErrorPanel extends StatelessWidget { OutlinedButton.icon( onPressed: onOpenRepo, icon: const Icon(Icons.open_in_new, size: 14), - label: const Text('Open repository in browser'), + label: Text(AppLocalizations.of(context)!.storeOpenRepoButton), style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, ), @@ -1636,22 +1651,21 @@ int _versionCmp(String a, String b) { return 0; } -String _friendlyDocsError(String kind, String detail) { +String _friendlyDocsError(BuildContext context, String kind, String detail) { + final l = AppLocalizations.of(context)!; switch (kind) { case 'not_found': - return 'This module is not in the hub\'s store index.'; + return l.storeDocsErrorNotFound; case 'no_docs': - return 'No repository URL is recorded for this module.'; + return l.storeDocsErrorNoDocs; case 'auth': - return 'The registry requires authentication to fetch this README. ' - 'Set FAI_REGISTRY_TOKEN in the daemon\'s environment ' - 'and restart the hub.'; + return l.storeDocsErrorAuth; case 'network': - return 'Network error while fetching the README. $detail'; + return l.storeDocsErrorNetwork(detail); case 'parse': - return 'README is not valid UTF-8.'; + return l.storeDocsErrorParse; default: - return detail.isEmpty ? 'Could not load README.' : detail; + return detail.isEmpty ? l.storeDocsErrorGeneric : detail; } } diff --git a/lib/widgets/fai_search_palette.dart b/lib/widgets/fai_search_palette.dart index 53bf3c6..9850caf 100644 --- a/lib/widgets/fai_search_palette.dart +++ b/lib/widgets/fai_search_palette.dart @@ -11,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../data/hub.dart'; +import '../l10n/app_localizations.dart'; import 'fai_module_sheet.dart'; /// One row in the palette. Carries everything needed to render @@ -90,6 +91,7 @@ class _FaiSearchPaletteState extends State { /// first character. Future> _loadDynamic() async { final svc = HubService.instance; + final l = AppLocalizations.of(context)!; final results = await Future.wait([ svc.listModules().catchError((_) => []), svc.searchStore(limit: 200).catchError((_) => []), @@ -103,23 +105,26 @@ class _FaiSearchPaletteState extends State { for (final m in modules) { hits.add(FaiSearchHit( label: m.name, - hint: 'installed module · v${m.version}', + hint: l.searchInstalledModuleHint(m.version), icon: Icons.extension, - group: 'Modules', + group: l.searchGroupModules, onSelect: () { Navigator.pop(context); FaiModuleSheet.show(context, m.name); }, )); } + final storeGroup = l.searchGroupStore; + final pagesGroup = l.searchGroupPages; for (final s in store) { hits.add(FaiSearchHit( label: s.name, hint: s.taglineEn.isEmpty - ? 'store · ${s.category.isEmpty ? "uncategorized" : s.category}' - : 'store · ${s.taglineEn}', + ? l.searchStoreHintWithCategory( + s.category.isEmpty ? l.searchStoreUncategorized : s.category) + : l.searchStoreHintWithTagline(s.taglineEn), icon: Icons.storefront_outlined, - group: 'Store', + group: storeGroup, // Selecting a store hit just closes the palette and // focuses the Store tab; deep-linking to the detail // sheet would require thread-through plumbing we don't @@ -127,7 +132,7 @@ class _FaiSearchPaletteState extends State { onSelect: () { Navigator.pop(context); for (final h in widget.staticHits) { - if (h.label == 'Store' && h.group == 'Pages') { + if (h.label == storeGroup && h.group == pagesGroup) { h.onSelect(); return; } @@ -135,16 +140,17 @@ class _FaiSearchPaletteState extends State { }, )); } + final flowsGroup = l.searchGroupFlows; for (final f in flows) { hits.add(FaiSearchHit( label: f.name, - hint: 'saved flow', + hint: l.searchSavedFlowHint, icon: Icons.account_tree_outlined, - group: 'Flows', + group: flowsGroup, onSelect: () { Navigator.pop(context); for (final h in widget.staticHits) { - if (h.label == 'Flows' && h.group == 'Pages') { + if (h.label == flowsGroup && h.group == pagesGroup) { h.onSelect(); return; } @@ -201,6 +207,7 @@ class _FaiSearchPaletteState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; return Dialog( alignment: Alignment.topCenter, insetPadding: const EdgeInsets.only(top: 80, left: 24, right: 24), @@ -235,8 +242,7 @@ class _FaiSearchPaletteState extends State { decoration: InputDecoration( prefixIcon: const Icon(Icons.search, size: 20), - hintText: - 'Jump anywhere — modules, store, flows, pages…', + hintText: l.searchHint, border: InputBorder.none, suffixIcon: snap.connectionState == ConnectionState.waiting @@ -264,8 +270,8 @@ class _FaiSearchPaletteState extends State { padding: const EdgeInsets.all(24), child: Text( snap.connectionState == ConnectionState.waiting - ? 'Indexing…' - : 'No matches.', + ? l.searchIndexing + : l.searchNoMatches, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -380,14 +386,14 @@ class _FaiSearchPaletteState extends State { ), child: Row( children: [ - _Hint(text: '↑↓ navigate'), + _Hint(text: l.searchHintNavigate), const SizedBox(width: 12), - _Hint(text: 'enter open'), + _Hint(text: l.searchHintOpen), const SizedBox(width: 12), - _Hint(text: 'esc close'), + _Hint(text: l.searchHintClose), const Spacer(), Text( - '${hits.length} result${hits.length == 1 ? "" : "s"}', + l.storeNResults(hits.length), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), diff --git a/pubspec.yaml b/pubspec.yaml index fe05860..23d20d1 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.26.0 +version: 0.27.0 environment: sdk: ^3.11.0-200.1.beta