content localized on locale change
This commit is contained in:
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal 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:
|
||||||
@@ -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: '/',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
58
lib/providers/content_provider.dart
Normal file
58
lib/providers/content_provider.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/providers/locale_provider.dart
Normal file
12
lib/providers/locale_provider.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user