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,36 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:resume/pages/landing_page.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;
|
||||
|
||||
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});
|
||||
|
||||
@override
|
||||
State<Resume> createState() => _ResumeState();
|
||||
}
|
||||
|
||||
class _ResumeState extends State<Resume> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Resume',
|
||||
theme: darkTheme,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('de'),
|
||||
],
|
||||
locale: const Locale('en'),
|
||||
routes: {
|
||||
'/': (context) => const LandingPage(),
|
||||
},
|
||||
initialRoute: '/',
|
||||
return Consumer<LocaleProvider>(
|
||||
builder: (context, localeProvider, child) => MaterialApp(
|
||||
title: 'Resume',
|
||||
theme: darkTheme,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('de'),
|
||||
],
|
||||
locale: localeProvider.locale,
|
||||
routes: {
|
||||
'/': (context) => const LandingPage(),
|
||||
},
|
||||
initialRoute: '/',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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/widgets/language_dropdown.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:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../services/content_provider.dart';
|
||||
import '../providers/content_provider.dart';
|
||||
import '../widgets/content_block.dart';
|
||||
|
||||
class LandingPage extends StatefulWidget {
|
||||
@@ -20,14 +22,18 @@ class LandingPage extends StatefulWidget {
|
||||
|
||||
class _LandingPageState extends State<LandingPage> {
|
||||
bool loadingDone = false;
|
||||
late ContentProvider contentProvider;
|
||||
late Locale currentLocale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await ContentProvider.init(context);
|
||||
await Provider.of<ContentProvider>(context, listen: false)
|
||||
.loadContent(context);
|
||||
setState(() => loadingDone = true);
|
||||
});
|
||||
currentLocale = context.read<LocaleProvider>().locale;
|
||||
}
|
||||
|
||||
double _getMainContentWidth() {
|
||||
@@ -49,11 +55,13 @@ class _LandingPageState extends State<LandingPage> {
|
||||
ContentBlock(
|
||||
blockTitle: AppLocalizations.of(context)!.skills,
|
||||
contentType: ContentType.skills,
|
||||
content: contentProvider.getContent(ContentType.skills),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(bottom: 25)),
|
||||
ContentBlock(
|
||||
blockTitle: AppLocalizations.of(context)!.languages,
|
||||
contentType: ContentType.language,
|
||||
content: contentProvider.getContent(ContentType.language),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -65,21 +73,25 @@ class _LandingPageState extends State<LandingPage> {
|
||||
ContentBlock(
|
||||
blockTitle: AppLocalizations.of(context)!.work_experience,
|
||||
contentType: ContentType.experience,
|
||||
content: contentProvider.getContent(ContentType.experience),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(bottom: 25)),
|
||||
ContentBlock(
|
||||
blockTitle: AppLocalizations.of(context)!.education,
|
||||
contentType: ContentType.education,
|
||||
content: contentProvider.getContent(ContentType.education),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(bottom: 25)),
|
||||
ContentBlock(
|
||||
blockTitle: AppLocalizations.of(context)!.additional_skills,
|
||||
contentType: ContentType.generalSkills,
|
||||
content: contentProvider.getContent(ContentType.generalSkills),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(bottom: 25)),
|
||||
ContentBlock(
|
||||
blockTitle: AppLocalizations.of(context)!.about_me,
|
||||
contentType: ContentType.text,
|
||||
content: contentProvider.getContent(ContentType.text),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -188,6 +200,20 @@ class _LandingPageState extends State<LandingPage> {
|
||||
|
||||
@override
|
||||
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(
|
||||
appBar: AppBar(
|
||||
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/constants.dart' show ContentType;
|
||||
|
||||
import '../services/content_provider.dart';
|
||||
|
||||
class ContentBlock extends StatelessWidget {
|
||||
const ContentBlock({
|
||||
super.key,
|
||||
required this.blockTitle,
|
||||
required this.contentType,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
final String blockTitle;
|
||||
final ContentType contentType;
|
||||
final dynamic content;
|
||||
|
||||
Widget get _getContentWidget {
|
||||
if (contentType == ContentType.language) {
|
||||
return const LanguageWidget();
|
||||
} else if (contentType == ContentType.text ||
|
||||
contentType == ContentType.generalSkills) {
|
||||
final content = ContentProvider.getContent<String>(contentType);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(content),
|
||||
);
|
||||
}
|
||||
// List-based content-blocks
|
||||
List<dynamic> content =
|
||||
ContentProvider.getContent<List<dynamic>>(contentType);
|
||||
List<Widget> widgets = [];
|
||||
for (var item in content) {
|
||||
widgets.add(_buildListTile(item));
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import 'package:flutter/material.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});
|
||||
|
||||
@override
|
||||
State<LanguageDropdown> createState() => _LanguageDropdownState();
|
||||
}
|
||||
|
||||
class _LanguageDropdownState extends State<LanguageDropdown> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButton(
|
||||
value: 'de',
|
||||
LocaleProvider localeProvider = Provider.of<LocaleProvider>(context);
|
||||
return DropdownButton<String>(
|
||||
value: localeProvider.locale.languageCode,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
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) {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
provider: ^6.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user