From aea92b0eed09f80bb69b80e1cdf65dd96552a12f Mon Sep 17 00:00:00 2001 From: zsolt Date: Thu, 13 Aug 2026 13:43:57 +0200 Subject: [PATCH] Add Hungarian English language switcher --- lib/app_shell.dart | 154 ++++++++++++++++++++++++++++++---- lib/data.dart | 6 ++ lib/google_calendar_page.dart | 9 +- lib/localization.dart | 68 +++++++++++++++ lib/main.dart | 2 + lib/pages.dart | 43 ++++++---- pubspec.lock | 13 +++ pubspec.yaml | 2 + 8 files changed, 262 insertions(+), 35 deletions(-) create mode 100644 lib/localization.dart diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 7f7d624..053c720 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -18,6 +18,7 @@ class _OtthonAppState extends State { String googleServerClientId = googleCalendarServerClientId; WeatherSnapshot? weather; String themeId = 'sage'; + String languageCode = defaultLanguageCode; bool loaded = false; AppThemeStyle get activeTheme => appThemes.firstWhere( @@ -43,6 +44,7 @@ class _OtthonAppState extends State { jarvisSettings = settings.jarvisSettings; googleServerClientId = settings.googleServerClientId; themeId = settings.themeId; + languageCode = settings.languageCode; loaded = true; }); _loadRemoteAppConfig(); @@ -53,6 +55,7 @@ class _OtthonAppState extends State { AppSettings( people: people, themeId: themeId, + languageCode: languageCode, googleConnections: googleConnections, syncedEvents: syncedEvents, todoSuggestions: todoSuggestions, @@ -100,6 +103,11 @@ class _OtthonAppState extends State { _saveSettings(); } + void _updateLanguage(String nextLanguageCode) { + setState(() => languageCode = nextLanguageCode == 'en' ? 'en' : 'hu'); + _saveSettings(); + } + void _updateGoogleConnections( List nextConnections, ) { @@ -132,9 +140,17 @@ class _OtthonAppState extends State { @override Widget build(BuildContext context) { final theme = activeTheme; + final text = AppText.of(languageCode); return MaterialApp( debugShowCheckedModeBanner: false, - title: 'K\u00f6z\u00f6sT\u00e9r', + title: text.appTitle, + locale: text.locale, + supportedLocales: const [Locale('hu'), Locale('en')], + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( @@ -168,9 +184,11 @@ class _OtthonAppState extends State { googleServerClientId: googleServerClientId, weather: weather, themeId: themeId, + languageCode: languageCode, loaded: loaded, onPeopleChanged: _updatePeople, onThemeChanged: _updateTheme, + onLanguageChanged: _updateLanguage, onGoogleConnectionsChanged: _updateGoogleConnections, onSyncedEventsChanged: _updateSyncedEvents, onTodoSuggestionsChanged: _updateTodoSuggestions, @@ -191,9 +209,11 @@ class HomeShell extends StatefulWidget { required this.googleServerClientId, required this.weather, required this.themeId, + required this.languageCode, required this.loaded, required this.onPeopleChanged, required this.onThemeChanged, + required this.onLanguageChanged, required this.onGoogleConnectionsChanged, required this.onSyncedEventsChanged, required this.onTodoSuggestionsChanged, @@ -208,9 +228,11 @@ class HomeShell extends StatefulWidget { final String googleServerClientId; final WeatherSnapshot? weather; final String themeId; + final String languageCode; final bool loaded; final ValueChanged> onPeopleChanged; final ValueChanged onThemeChanged; + final ValueChanged onLanguageChanged; final ValueChanged> onGoogleConnectionsChanged; final ValueChanged> onSyncedEventsChanged; final ValueChanged> onTodoSuggestionsChanged; @@ -328,15 +350,6 @@ class _HomeShellState extends State { List get allEvents => [...events, ...widget.syncedEvents]; - final labels = const [ - 'Napt\u00e1r', - 'Bev\u00e1s\u00e1rl\u00e1s', - 'K\u00e9szlet', - 'Csal\u00e1dtagok', - 'St\u00edlus', - 'Fi\u00f3kok', - ]; - final icons = const [ Icons.calendar_month_rounded, Icons.shopping_basket_outlined, @@ -349,6 +362,15 @@ class _HomeShellState extends State { @override Widget build(BuildContext context) { final wide = MediaQuery.sizeOf(context).width >= 850; + final text = AppText.of(widget.languageCode); + final labels = [ + text.calendar, + text.shopping, + text.stock, + text.familyMembers, + text.style, + text.accounts, + ]; final body = IndexedStack( index: selected, children: [ @@ -362,11 +384,13 @@ class _HomeShellState extends State { ), ShoppingPage( items: shopping, + languageCode: widget.languageCode, onChanged: () => setState(() {}), onAdd: _addShopping, ), StockPage( items: stock, + languageCode: widget.languageCode, onChanged: () => setState(() {}), onAdd: _addStock, onEdit: _editStock, @@ -375,12 +399,14 @@ class _HomeShellState extends State { PersonsPage( people: widget.people, events: allEvents, + languageCode: widget.languageCode, onAdd: _addPerson, onEdit: _editPerson, onDelete: _deletePerson, ), StylePage( themeId: widget.themeId, + languageCode: widget.languageCode, onThemeChanged: widget.onThemeChanged, ), AccountsPage( @@ -389,6 +415,7 @@ class _HomeShellState extends State { people: widget.people, jarvisSettings: widget.jarvisSettings, serverClientId: widget.googleServerClientId, + languageCode: widget.languageCode, onChanged: widget.onGoogleConnectionsChanged, onSyncedEventsChanged: widget.onSyncedEventsChanged, ), @@ -451,11 +478,11 @@ class _HomeShellState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'K\u00f6z\u00f6sT\u00e9r', + Text( + text.appTitle, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: ink, @@ -464,8 +491,8 @@ class _HomeShellState extends State { ), Text( widget.loaded - ? 'Tervez\u0151' - : 'JSON bet\u00f6lt\u00e9se\u2026', + ? text.appSubtitle + : text.loadingJson, style: const TextStyle( fontSize: 11, color: Colors.black45, @@ -473,6 +500,12 @@ class _HomeShellState extends State { ), ), const SizedBox(height: 2), + LanguageSwitcher( + languageCode: widget.languageCode, + onChanged: widget.onLanguageChanged, + labels: text, + ), + const SizedBox(height: 2), const Text( appVersionLabel, style: TextStyle( @@ -485,7 +518,7 @@ class _HomeShellState extends State { ), ), IconButton( - tooltip: 'JARVIS be\u00e1ll\u00edt\u00e1sok', + tooltip: text.jarvisSettings, onPressed: _openJarvisSettings, icon: const Icon( Icons.settings_outlined, @@ -1215,3 +1248,92 @@ class _HomeShellState extends State { } } } + +class LanguageSwitcher extends StatelessWidget { + const LanguageSwitcher({ + super.key, + required this.languageCode, + required this.onChanged, + required this.labels, + }); + + final String languageCode; + final ValueChanged onChanged; + final AppText labels; + + @override + Widget build(BuildContext context) { + return Container( + height: 24, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: sage.withValues(alpha: .16)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _LanguagePill( + selected: languageCode != 'en', + flag: '🇭🇺', + label: labels.hunShort, + onTap: () => onChanged('hu'), + ), + _LanguagePill( + selected: languageCode == 'en', + flag: '🇬🇧', + label: labels.engShort, + onTap: () => onChanged('en'), + ), + ], + ), + ); + } +} + +class _LanguagePill extends StatelessWidget { + const _LanguagePill({ + required this.selected, + required this.flag, + required this.label, + required this.onTap, + }); + + final bool selected; + final String flag; + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(999), + onTap: selected ? null : onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: selected ? sage : Colors.transparent, + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(flag, style: const TextStyle(fontSize: 11)), + const SizedBox(width: 3), + Text( + label, + style: TextStyle( + fontSize: 9, + height: 1, + fontWeight: FontWeight.w900, + color: selected ? Colors.white : Colors.black45, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/data.dart b/lib/data.dart index 70df0b0..51af728 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -122,6 +122,7 @@ class AppJsonStore { final settings = AppSettings( people: defaultPeople(), themeId: 'sage', + languageCode: defaultLanguageCode, googleConnections: defaultGoogleConnections(), syncedEvents: const [], todoSuggestions: const [], @@ -139,6 +140,7 @@ class AppJsonStore { return AppSettings( people: defaultPeople(), themeId: 'sage', + languageCode: defaultLanguageCode, googleConnections: defaultGoogleConnections(), syncedEvents: const [], todoSuggestions: const [], @@ -160,6 +162,7 @@ class AppSettings { AppSettings({ required this.people, required this.themeId, + required this.languageCode, required this.googleConnections, required this.syncedEvents, required this.todoSuggestions, @@ -169,6 +172,7 @@ class AppSettings { final List people; final String themeId; + final String languageCode; final List googleConnections; final List syncedEvents; final List todoSuggestions; @@ -191,6 +195,7 @@ class AppSettings { ) : defaultPeople(), themeId: json['themeId'] as String? ?? 'sage', + languageCode: json['languageCode'] == 'en' ? 'en' : defaultLanguageCode, googleConnections: rawConnections is List ? rawConnections .whereType>() @@ -218,6 +223,7 @@ class AppSettings { Map toJson() => { 'themeId': themeId, + 'languageCode': languageCode, 'people': people.map((person) => person.toJson()).toList(), 'googleConnections': googleConnections .map((connection) => connection.toJson()) diff --git a/lib/google_calendar_page.dart b/lib/google_calendar_page.dart index bae2269..16bdc6a 100644 --- a/lib/google_calendar_page.dart +++ b/lib/google_calendar_page.dart @@ -8,6 +8,7 @@ class AccountsPage extends StatefulWidget { required this.people, required this.jarvisSettings, required this.serverClientId, + required this.languageCode, required this.onChanged, required this.onSyncedEventsChanged, }); @@ -17,6 +18,7 @@ class AccountsPage extends StatefulWidget { final List people; final JarvisSettings jarvisSettings; final String serverClientId; + final String languageCode; final ValueChanged> onChanged; final ValueChanged> onSyncedEventsChanged; @@ -354,6 +356,7 @@ class _AccountsPageState extends State with WidgetsBindingObserver @override Widget build(BuildContext context) { + final text = AppText.of(widget.languageCode); final current = connection; final hasConnection = current != null && current.accountEmail.isNotEmpty; final cachedUncategorized = widget.cachedEvents @@ -361,13 +364,13 @@ class _AccountsPageState extends State with WidgetsBindingObserver .length; return PageFrame( - eyebrow: 'JARVIS Google Calendar szinkron', - title: 'Google napt\u00e1rak', + eyebrow: text.googleSyncEyebrow, + title: text.googleCalendars, action: FilledButton.icon( onPressed: loadingCalendars ? null : _startGoogleConnection, icon: const Icon(Icons.open_in_browser), label: Text( - hasConnection ? 'Google \u00f6sszek\u00f6t\u00e9s \u00fajranyit\u00e1sa' : 'Google \u00f6sszek\u00f6t\u00e9s', + hasConnection ? text.googleReconnect : text.googleConnect, ), ), child: ListView( diff --git a/lib/localization.dart b/lib/localization.dart new file mode 100644 index 0000000..bf3f3ad --- /dev/null +++ b/lib/localization.dart @@ -0,0 +1,68 @@ +part of 'main.dart'; + +const defaultLanguageCode = 'hu'; + +class AppText { + const AppText._(this.languageCode); + + final String languageCode; + + static AppText of(String languageCode) => + languageCode == 'en' ? const AppText._('en') : const AppText._('hu'); + + Locale get locale => Locale(languageCode); + bool get isHungarian => languageCode == 'hu'; + + String get appTitle => isHungarian ? 'KözösTér' : 'CommonSpace'; + String get appSubtitle => isHungarian ? 'Tervező' : 'Organizer'; + String get loadingJson => isHungarian ? 'JSON betöltése…' : 'Loading JSON…'; + String get jarvisSettings => isHungarian + ? 'JARVIS beállítások' + : 'JARVIS settings'; + + String get calendar => isHungarian ? 'Naptár' : 'Calendar'; + String get shopping => isHungarian ? 'Bevásárlás' : 'Shopping'; + String get stock => isHungarian ? 'Készlet' : 'Stock'; + String get familyMembers => isHungarian ? 'Családtagok' : 'Family'; + String get style => isHungarian ? 'Stílus' : 'Style'; + String get accounts => isHungarian ? 'Fiókok' : 'Accounts'; + + String familyCount(int count) => + isHungarian ? '$count családtag' : '$count family members'; + String get noFamilyMembers => isHungarian + ? 'Még nincs családtag' + : 'No family members yet'; + String get noFamilyMembersSubtitle => isHungarian + ? 'Vegyél fel családtagokat, gyerekeket vagy bárkit.' + : 'Add family members, kids, or anyone you plan around.'; + String get newFamilyMember => isHungarian ? 'Új családtag' : 'New member'; + String get renameAvatar => isHungarian + ? 'Átnevezés / avatar' + : 'Rename / avatar'; + String get delete => isHungarian ? 'Törlés' : 'Delete'; + + String get themeAndStyles => isHungarian + ? 'Téma és stílusok' + : 'Theme and styles'; + String get appearance => isHungarian ? 'Megjelenés' : 'Appearance'; + + String remainingItems(int count) => + isHungarian ? '$count tétel van hátra' : '$count items left'; + String get shoppingList => isHungarian ? 'Bevásárlólista' : 'Shopping list'; + + String stockCount(int count) => + isHungarian ? '$count termék nyilvántartva' : '$count products tracked'; + String get homeStock => isHungarian ? 'Otthoni készlet' : 'Home stock'; + + String get googleSyncEyebrow => isHungarian + ? 'JARVIS Google Calendar szinkron' + : 'JARVIS Google Calendar sync'; + String get googleCalendars => isHungarian ? 'Google naptárak' : 'Google calendars'; + String get googleConnect => isHungarian ? 'Google összekötés' : 'Connect Google'; + String get googleReconnect => isHungarian + ? 'Google összekötés újranyitása' + : 'Reconnect Google'; + + String get hunShort => 'HUN'; + String get engShort => 'ENG'; +} diff --git a/lib/main.dart b/lib/main.dart index 7b831f3..361dad7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:geocoding/geocoding.dart' as geocoding; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; @@ -14,6 +15,7 @@ import 'package:timezone/data/latest.dart' as timezone_data; import 'package:url_launcher/url_launcher.dart'; part 'data.dart'; +part 'localization.dart'; part 'app_shell.dart'; part 'calendar_widgets.dart'; part 'pages.dart'; diff --git a/lib/pages.dart b/lib/pages.dart index c5dee3b..7559509 100644 --- a/lib/pages.dart +++ b/lib/pages.dart @@ -5,6 +5,7 @@ class PersonsPage extends StatelessWidget { super.key, required this.people, required this.events, + required this.languageCode, required this.onAdd, required this.onEdit, required this.onDelete, @@ -12,26 +13,27 @@ class PersonsPage extends StatelessWidget { final List people; final List events; + final String languageCode; final VoidCallback onAdd; final ValueChanged onEdit; final ValueChanged onDelete; @override Widget build(BuildContext context) { + final text = AppText.of(languageCode); return PageFrame( - eyebrow: '${people.length} csal\u00e1dtag', - title: 'Csal\u00e1dtagok', + eyebrow: text.familyCount(people.length), + title: text.familyMembers, action: FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.add), - label: const Text('\u00daj csal\u00e1dtag'), + label: Text(text.newFamilyMember), ), child: people.isEmpty - ? const EmptyState( + ? EmptyState( icon: Icons.group_add_outlined, - title: 'M\u00e9g nincs csal\u00e1dtag', - subtitle: - 'Vegy\u00e9l fel csal\u00e1dtagokat, gyerekeket vagy b\u00e1rkit.', + title: text.noFamilyMembers, + subtitle: text.noFamilyMembersSubtitle, ) : ListView.separated( padding: const EdgeInsets.only(bottom: 24), @@ -90,14 +92,14 @@ class PersonsPage extends StatelessWidget { if (value == 'edit') onEdit(person); if (value == 'delete') onDelete(person); }, - itemBuilder: (_) => const [ + itemBuilder: (_) => [ PopupMenuItem( value: 'edit', - child: Text('\u00c1tnevez\u00e9s / avatar'), + child: Text(text.renameAvatar), ), PopupMenuItem( value: 'delete', - child: Text('T\u00f6rl\u00e9s'), + child: Text(text.delete), ), ], ), @@ -115,17 +117,20 @@ class StylePage extends StatelessWidget { const StylePage({ super.key, required this.themeId, + required this.languageCode, required this.onThemeChanged, }); final String themeId; + final String languageCode; final ValueChanged onThemeChanged; @override Widget build(BuildContext context) { + final text = AppText.of(languageCode); return PageFrame( - eyebrow: 'T\u00e9ma \u00e9s st\u00edlusok', - title: 'Megjelen\u00e9s', + eyebrow: text.themeAndStyles, + title: text.appearance, child: ListView( padding: const EdgeInsets.only(bottom: 24), children: [ @@ -187,20 +192,23 @@ class ShoppingPage extends StatelessWidget { const ShoppingPage({ super.key, required this.items, + required this.languageCode, required this.onChanged, required this.onAdd, }); final List items; + final String languageCode; final VoidCallback onChanged; final VoidCallback onAdd; @override Widget build(BuildContext context) { + final text = AppText.of(languageCode); final remaining = items.where((e) => !e.done).length; return PageFrame( - eyebrow: '$remaining t\u00e9tel van h\u00e1tra', - title: 'Bev\u00e1s\u00e1rl\u00f3lista', + eyebrow: text.remainingItems(remaining), + title: text.shoppingList, action: IconButton.filled(onPressed: onAdd, icon: const Icon(Icons.add)), child: Column( children: [ @@ -278,6 +286,7 @@ class StockPage extends StatefulWidget { const StockPage({ super.key, required this.items, + required this.languageCode, required this.onChanged, required this.onAdd, required this.onEdit, @@ -285,6 +294,7 @@ class StockPage extends StatefulWidget { }); final List items; + final String languageCode; final VoidCallback onChanged; final VoidCallback onAdd; final ValueChanged onEdit; @@ -299,10 +309,11 @@ class _StockPageState extends State { @override Widget build(BuildContext context) { + final text = AppText.of(widget.languageCode); final filtered = widget.items.where((e) => e.location == location).toList(); return PageFrame( - eyebrow: '${widget.items.length} term\u00e9k nyilv\u00e1ntartva', - title: 'Otthoni k\u00e9szlet', + eyebrow: text.stockCount(widget.items.length), + title: text.homeStock, action: IconButton.filled( onPressed: widget.onAdd, icon: const Icon(Icons.add), diff --git a/pubspec.lock b/pubspec.lock index 67688c1..cd53513 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -190,6 +190,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -344,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" jni: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d22cb56..a0f590f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.