content localized on locale change

This commit is contained in:
2024-12-18 19:30:13 +01:00
parent a984fc15b0
commit 6587828a0b
9 changed files with 158 additions and 94 deletions

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -1,19 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:resume/pages/landing_page.dart'; import 'package:resume/pages/landing_page.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:resume/providers/locale_provider.dart';
import 'package:resume/providers/content_provider.dart';
import 'theme.dart' show darkTheme; import 'theme.dart' show darkTheme;
void main() { void main() {
runApp(const Resume()); runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => LocaleProvider()),
ChangeNotifierProvider(create: (context) => ContentProvider()),
],
child: const Resume(),
),
);
} }
class Resume extends StatelessWidget { class Resume extends StatefulWidget {
const Resume({super.key}); const Resume({super.key});
@override
State<Resume> createState() => _ResumeState();
}
class _ResumeState extends State<Resume> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Consumer<LocaleProvider>(
builder: (context, localeProvider, child) => MaterialApp(
title: 'Resume', title: 'Resume',
theme: darkTheme, theme: darkTheme,
localizationsDelegates: const [ localizationsDelegates: const [
@@ -26,11 +43,12 @@ class Resume extends StatelessWidget {
Locale('en'), Locale('en'),
Locale('de'), Locale('de'),
], ],
locale: const Locale('en'), locale: localeProvider.locale,
routes: { routes: {
'/': (context) => const LandingPage(), '/': (context) => const LandingPage(),
}, },
initialRoute: '/', initialRoute: '/',
),
); );
} }
} }

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:resume/providers/locale_provider.dart';
import 'package:resume/services/breakpoints.dart'; import 'package:resume/services/breakpoints.dart';
import 'package:resume/widgets/language_dropdown.dart'; import 'package:resume/widgets/language_dropdown.dart';
import 'package:resume/widgets/profile.dart'; import 'package:resume/widgets/profile.dart';
@@ -6,7 +8,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:resume/constants.dart' show ContentType; import 'package:resume/constants.dart' show ContentType;
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../services/content_provider.dart'; import '../providers/content_provider.dart';
import '../widgets/content_block.dart'; import '../widgets/content_block.dart';
class LandingPage extends StatefulWidget { class LandingPage extends StatefulWidget {
@@ -20,14 +22,18 @@ class LandingPage extends StatefulWidget {
class _LandingPageState extends State<LandingPage> { class _LandingPageState extends State<LandingPage> {
bool loadingDone = false; bool loadingDone = false;
late ContentProvider contentProvider;
late Locale currentLocale;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await ContentProvider.init(context); await Provider.of<ContentProvider>(context, listen: false)
.loadContent(context);
setState(() => loadingDone = true); setState(() => loadingDone = true);
}); });
currentLocale = context.read<LocaleProvider>().locale;
} }
double _getMainContentWidth() { double _getMainContentWidth() {
@@ -49,11 +55,13 @@ class _LandingPageState extends State<LandingPage> {
ContentBlock( ContentBlock(
blockTitle: AppLocalizations.of(context)!.skills, blockTitle: AppLocalizations.of(context)!.skills,
contentType: ContentType.skills, contentType: ContentType.skills,
content: contentProvider.getContent(ContentType.skills),
), ),
const Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: AppLocalizations.of(context)!.languages, blockTitle: AppLocalizations.of(context)!.languages,
contentType: ContentType.language, contentType: ContentType.language,
content: contentProvider.getContent(ContentType.language),
), ),
], ],
); );
@@ -65,21 +73,25 @@ class _LandingPageState extends State<LandingPage> {
ContentBlock( ContentBlock(
blockTitle: AppLocalizations.of(context)!.work_experience, blockTitle: AppLocalizations.of(context)!.work_experience,
contentType: ContentType.experience, contentType: ContentType.experience,
content: contentProvider.getContent(ContentType.experience),
), ),
const Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: AppLocalizations.of(context)!.education, blockTitle: AppLocalizations.of(context)!.education,
contentType: ContentType.education, contentType: ContentType.education,
content: contentProvider.getContent(ContentType.education),
), ),
const Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: AppLocalizations.of(context)!.additional_skills, blockTitle: AppLocalizations.of(context)!.additional_skills,
contentType: ContentType.generalSkills, contentType: ContentType.generalSkills,
content: contentProvider.getContent(ContentType.generalSkills),
), ),
const Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: AppLocalizations.of(context)!.about_me, blockTitle: AppLocalizations.of(context)!.about_me,
contentType: ContentType.text, contentType: ContentType.text,
content: contentProvider.getContent(ContentType.text),
), ),
], ],
); );
@@ -188,6 +200,20 @@ class _LandingPageState extends State<LandingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
contentProvider = context.read<ContentProvider>();
if (currentLocale != context.read<LocaleProvider>().locale) {
loadingDone = false;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Provider.of<ContentProvider>(context, listen: false)
.loadContent(context);
setState(() {
currentLocale = context.read<LocaleProvider>().locale;
loadingDone = true;
});
});
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.resume), title: Text(AppLocalizations.of(context)!.resume),

View File

@@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:resume/constants.dart';
import 'package:resume/providers/locale_provider.dart';
class ContentProvider extends ChangeNotifier {
static final ContentProvider _instance = ContentProvider._();
factory ContentProvider() => _instance;
ContentProvider._();
static const String _baseJsonPath = 'assets/content/content';
Map<String, dynamic> _content = {
'experience': <List<dynamic>>[],
'education': <List<dynamic>>[],
'skills': <List<dynamic>>[],
'text': <String>[],
'general_skills': <String>[],
};
Future<bool> loadContent(BuildContext context) async {
String currentLocale = context.read<LocaleProvider>().locale.languageCode;
final String localizedPath = '${_baseJsonPath}_$currentLocale.json';
try {
String file = await rootBundle.loadString(localizedPath);
_content = json.decode(file);
notifyListeners();
return true;
} catch (e) {
// If localized version fails, fall back to default German
String file = await rootBundle.loadString('${_baseJsonPath}_de.json');
_content = json.decode(file);
notifyListeners();
return true;
}
}
T getContent<T>(ContentType contentType) {
switch (contentType) {
case ContentType.experience:
return _content['experience'] as T;
case ContentType.education:
return _content['education'] as T;
case ContentType.skills:
return _content['skills'] as T;
case ContentType.text:
return _content['text'] as T;
case ContentType.generalSkills:
return _content['general_skills'] as T;
default:
return [] as T;
}
}
}

View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('de');
Locale get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}

View File

@@ -1,61 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:resume/constants.dart';
class ContentProvider {
ContentProvider._();
static const String _baseJsonPath = 'assets/content/content';
static Future<bool> init(BuildContext context) async {
try {
// Get the current locale
final String currentLocale = Localizations.localeOf(context).languageCode;
// Construct the path with the locale
final String localizedPath = '${_baseJsonPath}_$currentLocale.json';
// Try to load the localized version first
try {
String file = await rootBundle.loadString(localizedPath);
_content = json.decode(file);
return true;
} catch (e) {
// If localized version fails, fall back to default English
String file = await rootBundle.loadString('${_baseJsonPath}_de.json');
_content = json.decode(file);
return true;
}
} catch (e) {
print('Error loading content: $e');
return false;
}
}
static Map<String, dynamic> _content = {
'experience': <List<dynamic>>[],
'education': <List<dynamic>>[],
'skills': <List<dynamic>>[],
'text': <String>[],
'general_skills': <String>[],
};
static T getContent<T>(ContentType contentType) {
switch (contentType) {
case ContentType.experience:
return _content['experience'] as T;
case ContentType.education:
return _content['education'] as T;
case ContentType.skills:
return _content['skills'] as T;
case ContentType.text:
return _content['text'] as T;
case ContentType.generalSkills:
return _content['general_skills'] as T;
default:
return [] as T;
}
}
}

View File

@@ -3,32 +3,29 @@ import 'package:resume/widgets/content_list_tile.dart';
import 'package:resume/widgets/language_widget.dart'; import 'package:resume/widgets/language_widget.dart';
import 'package:resume/constants.dart' show ContentType; import 'package:resume/constants.dart' show ContentType;
import '../services/content_provider.dart';
class ContentBlock extends StatelessWidget { class ContentBlock extends StatelessWidget {
const ContentBlock({ const ContentBlock({
super.key, super.key,
required this.blockTitle, required this.blockTitle,
required this.contentType, required this.contentType,
required this.content,
}); });
final String blockTitle; final String blockTitle;
final ContentType contentType; final ContentType contentType;
final dynamic content;
Widget get _getContentWidget { Widget get _getContentWidget {
if (contentType == ContentType.language) { if (contentType == ContentType.language) {
return const LanguageWidget(); return const LanguageWidget();
} else if (contentType == ContentType.text || } else if (contentType == ContentType.text ||
contentType == ContentType.generalSkills) { contentType == ContentType.generalSkills) {
final content = ContentProvider.getContent<String>(contentType);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Text(content), child: Text(content),
); );
} }
// List-based content-blocks // List-based content-blocks
List<dynamic> content =
ContentProvider.getContent<List<dynamic>>(contentType);
List<Widget> widgets = []; List<Widget> widgets = [];
for (var item in content) { for (var item in content) {
widgets.add(_buildListTile(item)); widgets.add(_buildListTile(item));

View File

@@ -1,13 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class LanguageDropdown extends StatelessWidget { import '../providers/locale_provider.dart';
class LanguageDropdown extends StatefulWidget {
const LanguageDropdown({super.key}); const LanguageDropdown({super.key});
@override
State<LanguageDropdown> createState() => _LanguageDropdownState();
}
class _LanguageDropdownState extends State<LanguageDropdown> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButton( LocaleProvider localeProvider = Provider.of<LocaleProvider>(context);
value: 'de', return DropdownButton<String>(
value: localeProvider.locale.languageCode,
items: [ items: [
DropdownMenuItem( DropdownMenuItem(
value: 'de', value: 'de',
@@ -24,12 +33,13 @@ class LanguageDropdown extends StatelessWidget {
), ),
), ),
], ],
onChanged: _onChanged, onChanged: (value) {
if (value == null) return;
localeProvider.setLocale(Locale(value));
},
); );
} }
void _onChanged(dynamic value) {}
Widget getMenuItem(String label, String imagePath) { Widget getMenuItem(String label, String imagePath) {
return Row( return Row(
children: [ children: [

View File

@@ -15,6 +15,7 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
intl: any intl: any
provider: ^6.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: