feat(studio): Store + Cmd+K palette i18n (v0.27.0)

- 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 <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-08 02:49:18 +02:00
parent d25b4c87ae
commit e2b2639a86
9 changed files with 990 additions and 109 deletions

View file

@ -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<FaiSearchPalette> {
/// first character.
Future<List<FaiSearchHit>> _loadDynamic() async {
final svc = HubService.instance;
final l = AppLocalizations.of(context)!;
final results = await Future.wait([
svc.listModules().catchError((_) => <ModuleSummary>[]),
svc.searchStore(limit: 200).catchError((_) => <StoreItem>[]),
@ -103,23 +105,26 @@ class _FaiSearchPaletteState extends State<FaiSearchPalette> {
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<FaiSearchPalette> {
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<FaiSearchPalette> {
},
));
}
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<FaiSearchPalette> {
@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<FaiSearchPalette> {
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<FaiSearchPalette> {
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<FaiSearchPalette> {
),
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,
),