Add Hungarian English language switcher

This commit is contained in:
zsolt
2026-08-13 13:43:57 +02:00
parent 1b0734cf7b
commit aea92b0eed
8 changed files with 262 additions and 35 deletions
+138 -16
View File
@@ -18,6 +18,7 @@ class _OtthonAppState extends State<OtthonApp> {
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<OtthonApp> {
jarvisSettings = settings.jarvisSettings;
googleServerClientId = settings.googleServerClientId;
themeId = settings.themeId;
languageCode = settings.languageCode;
loaded = true;
});
_loadRemoteAppConfig();
@@ -53,6 +55,7 @@ class _OtthonAppState extends State<OtthonApp> {
AppSettings(
people: people,
themeId: themeId,
languageCode: languageCode,
googleConnections: googleConnections,
syncedEvents: syncedEvents,
todoSuggestions: todoSuggestions,
@@ -100,6 +103,11 @@ class _OtthonAppState extends State<OtthonApp> {
_saveSettings();
}
void _updateLanguage(String nextLanguageCode) {
setState(() => languageCode = nextLanguageCode == 'en' ? 'en' : 'hu');
_saveSettings();
}
void _updateGoogleConnections(
List<GoogleCalendarConnection> nextConnections,
) {
@@ -132,9 +140,17 @@ class _OtthonAppState extends State<OtthonApp> {
@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<OtthonApp> {
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<List<Person>> onPeopleChanged;
final ValueChanged<String> onThemeChanged;
final ValueChanged<String> onLanguageChanged;
final ValueChanged<List<GoogleCalendarConnection>> onGoogleConnectionsChanged;
final ValueChanged<List<CalendarEvent>> onSyncedEventsChanged;
final ValueChanged<List<String>> onTodoSuggestionsChanged;
@@ -328,15 +350,6 @@ class _HomeShellState extends State<HomeShell> {
List<CalendarEvent> 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<HomeShell> {
@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<HomeShell> {
),
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<HomeShell> {
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<HomeShell> {
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<HomeShell> {
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<HomeShell> {
),
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<HomeShell> {
),
),
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<HomeShell> {
),
),
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<HomeShell> {
}
}
}
class LanguageSwitcher extends StatelessWidget {
const LanguageSwitcher({
super.key,
required this.languageCode,
required this.onChanged,
required this.labels,
});
final String languageCode;
final ValueChanged<String> 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,
),
),
],
),
),
);
}
}
+6
View File
@@ -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<Person> people;
final String themeId;
final String languageCode;
final List<GoogleCalendarConnection> googleConnections;
final List<CalendarEvent> syncedEvents;
final List<String> 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<Map<String, dynamic>>()
@@ -218,6 +223,7 @@ class AppSettings {
Map<String, dynamic> toJson() => {
'themeId': themeId,
'languageCode': languageCode,
'people': people.map((person) => person.toJson()).toList(),
'googleConnections': googleConnections
.map((connection) => connection.toJson())
+6 -3
View File
@@ -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<Person> people;
final JarvisSettings jarvisSettings;
final String serverClientId;
final String languageCode;
final ValueChanged<List<GoogleCalendarConnection>> onChanged;
final ValueChanged<List<CalendarEvent>> onSyncedEventsChanged;
@@ -354,6 +356,7 @@ class _AccountsPageState extends State<AccountsPage> 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<AccountsPage> 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(
+68
View File
@@ -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';
}
+2
View File
@@ -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';
+27 -16
View File
@@ -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<Person> people;
final List<CalendarEvent> events;
final String languageCode;
final VoidCallback onAdd;
final ValueChanged<Person> onEdit;
final ValueChanged<Person> 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<String> 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<ShoppingItem> 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<StockItem> items;
final String languageCode;
final VoidCallback onChanged;
final VoidCallback onAdd;
final ValueChanged<StockItem> onEdit;
@@ -299,10 +309,11 @@ class _StockPageState extends State<StockPage> {
@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),