feat(studio): app-wide i18n via flutter_localizations (v0.24.0)

Replaces the Store-only DE/EN toggle with an app-wide one
parked in the sidebar footer next to the theme button.
Pressing it flips every translated string at once: nav
labels, page titles, common buttons, the bilingual
store-index content.

Implementation:
- Adds `flutter_localizations` + `intl` to pubspec, plus
  `flutter.generate: true` so `flutter gen-l10n` runs in the
  build pipeline.
- ARB sources at `lib/l10n/app_en.arb` and `app_de.arb`. The
  EN file is the template; DE carries the German strings.
  Initial coverage: navigation, common buttons, page titles,
  channels / store / audit / modules / approvals headers,
  hub-unreachable copy, MCP + n8n panel headers + hints.
  Rest of the UI strings are still English-literal — those
  fall in incrementally as we touch each surface.
- Generated `AppLocalizations` lives at
  `lib/l10n/app_localizations*.dart` (regenerated via
  `flutter gen-l10n` on every ARB edit).
- `StudioAppState` gains `localeNotifier` alongside
  `modeNotifier`; persisted via SharedPreferences key
  `locale.code`.
- Sidebar `_LanguageToggle` reads/writes through the
  notifier. The Store's per-page locale state is gone:
  `_locale` now reads `Localizations.localeOf(context)
  .languageCode`, so the bilingual store-index content
  follows the global setting without a second toggle.
- `_NavPage.label` becomes `_NavPage.id` + `labelOf(context)`;
  Cmd+K palette and Sidebar both read the localized label.

Out of scope this iteration: localizing the remaining
~80% of UI strings (Settings dialog labels, Store search
hint, error messages). Those land incrementally — the i18n
infrastructure now means each is a one-line ARB edit + one
call-site swap.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-08 01:19:32 +02:00
parent d2a6ed32e2
commit ee83eb851a
16 changed files with 1440 additions and 67 deletions

View file

@ -26,9 +26,15 @@ class _StorePageState extends State<StorePage> {
String _category = '';
String _status = '';
bool _installedOnly = false;
String _locale = 'en';
late Future<List<StoreItem>> _future;
/// Active UI locale, read from the app-wide MaterialApp
/// `locale` setting. Drives which side of the bilingual
/// store-index entries renders (taglineEn vs taglineDe,
/// descriptionEn vs descriptionDe). The sidebar's
/// language toggle flips this for the whole app.
String get _locale => Localizations.localeOf(context).languageCode;
@override
void initState() {
super.initState();
@ -97,13 +103,10 @@ class _StorePageState extends State<StorePage> {
appBar: AppBar(
title: const Text('Store'),
actions: [
// Tiny DE/EN toggle the store-index ships bilingual
// copy and operators in regulated environments often
// share screens with non-English colleagues.
_LocaleToggle(
value: _locale,
onChanged: (v) => setState(() => _locale = v),
),
// The DE/EN toggle now lives in the sidebar footer
// and flips the entire app's locale. The Store
// detail-sheet content follows automatically because
// _locale reads from `Localizations.localeOf`.
IconButton(
icon: const Icon(Icons.refresh, size: 18),
tooltip: 'Reload',
@ -421,29 +424,6 @@ class _ChoiceChip extends StatelessWidget {
}
}
class _LocaleToggle extends StatelessWidget {
final String value;
final ValueChanged<String> onChanged;
const _LocaleToggle({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: FaiSpace.sm),
child: Tooltip(
message: value == 'en'
? 'Showing English copy. Click to switch to German.'
: 'Deutsche Beschreibung — Click for English.',
child: TextButton.icon(
onPressed: () => onChanged(value == 'en' ? 'de' : 'en'),
icon: const Icon(Icons.translate, size: 16),
label: Text(value.toUpperCase()),
),
),
);
}
}
/// Responsive grid wraps to as many columns as the viewport
/// allows. Cards are click-to-expand; install is also a top-
/// level action so single-click installs are still possible.
@ -893,7 +873,9 @@ class _StoreDetailSheet extends StatefulWidget {
}
class _StoreDetailSheetState extends State<_StoreDetailSheet> {
late String _locale = widget.locale;
/// Locale comes from MaterialApp via `Localizations.localeOf`;
/// the per-sheet state from before is no longer needed.
String get _locale => Localizations.localeOf(context).languageCode;
bool _busy = false;
String? _toast;
Future<({String errorKind, String text, String sourceUrl})>? _docsFuture;
@ -1085,10 +1067,9 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
],
),
),
_LocaleToggle(
value: _locale,
onChanged: (v) => setState(() => _locale = v),
),
// Locale follows the app-wide setting
// from the sidebar; the detail sheet
// does not host its own toggle anymore.
],
),
const SizedBox(height: FaiSpace.lg),