Compare commits

...

7 Commits

Author SHA1 Message Date
85b5d4dc7c updated gitignore and changed content directory 2024-12-18 18:21:37 +01:00
ebf1246a55 added month localizations 2024-12-18 18:17:22 +01:00
8e91388ecf Localized static text 2024-12-18 18:05:14 +01:00
b9033014c8 added more localized terms 2024-12-18 18:01:41 +01:00
0a877525aa added localization 2024-12-18 17:56:33 +01:00
ff97898b90 simple language dropdown without function 2024-12-18 17:36:24 +01:00
c523b7495f Fixed content not being responsive 2024-12-18 17:16:39 +01:00
14 changed files with 250 additions and 64 deletions

2
.gitignore vendored
View File

@@ -156,4 +156,4 @@ app.*.symbols
!.vscode/settings.json !.vscode/settings.json
# Custom # Custom
content.json content/

3
l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View File

@@ -1,19 +1,3 @@
/// List of months 0-11
const months = [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'July',
'August',
'September',
'Oktober',
'November',
'Dezember'
];
enum ContentType { enum ContentType {
experience, experience,
education, education,

28
lib/l10n/app_de.arb Normal file
View File

@@ -0,0 +1,28 @@
{
"german": "Deutsch",
"english": "Englisch",
"resume": "Lebenslauf",
"skills": "Fähigkeiten",
"languages": "Sprachen",
"mother_tongue": "Muttersprache",
"very_good": "Sehr gut",
"about_me": "Über mich",
"additional_skills": "Weitere Kenntnisse",
"work_experience": "Arbeitserfahrung",
"education": "Bildungsweg",
"source_code": "Quellcode",
"@_months": {},
"january": "Januar",
"february": "Februar",
"march": "März",
"april": "April",
"may": "Mai",
"june": "Juni",
"july": "Juli",
"august": "August",
"september": "September",
"october": "Oktober",
"november": "November",
"december": "Dezember"
}

28
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,28 @@
{
"german": "German",
"english": "English",
"resume": "Resume",
"skills": "Skills",
"languages": "Languages",
"mother_tongue": "Native",
"very_good": "Very goods",
"about_me": "About me",
"additional_skills": "Additional Skills",
"work_experience": "Work experience",
"education": "Education",
"source_code": "Source Code",
"@_months": {},
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December"
}

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.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 'theme.dart' show darkTheme; import 'theme.dart' show darkTheme;
void main() { void main() {
@@ -14,6 +16,17 @@ class Resume extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: 'Resume', title: 'Resume',
theme: darkTheme, theme: darkTheme,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('de'),
],
locale: const Locale('en'),
routes: { routes: {
'/': (context) => const LandingPage(), '/': (context) => const LandingPage(),
}, },

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:resume/services/breakpoints.dart'; import 'package:resume/services/breakpoints.dart';
import 'package:resume/widgets/language_dropdown.dart';
import 'package:resume/widgets/profile.dart'; import 'package:resume/widgets/profile.dart';
import 'package:url_launcher/url_launcher.dart'; 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 '../services/content_provider.dart'; import '../services/content_provider.dart';
import '../widgets/content_block.dart'; import '../widgets/content_block.dart';
@@ -42,15 +44,15 @@ class _LandingPageState extends State<LandingPage> {
(MediaQuery.of(context).size.width - _getMainContentWidth()) / 2; (MediaQuery.of(context).size.width - _getMainContentWidth()) / 2;
Widget _getSideBar() { Widget _getSideBar() {
return const Column( return Column(
children: [ children: [
ContentBlock( ContentBlock(
blockTitle: 'Fähigkeiten', blockTitle: AppLocalizations.of(context)!.skills,
contentType: ContentType.skills, contentType: ContentType.skills,
), ),
Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: 'Sprachen', blockTitle: AppLocalizations.of(context)!.languages,
contentType: ContentType.language, contentType: ContentType.language,
), ),
], ],
@@ -58,25 +60,25 @@ class _LandingPageState extends State<LandingPage> {
} }
Widget _getMainContent() { Widget _getMainContent() {
return const Column( return Column(
children: [ children: [
ContentBlock( ContentBlock(
blockTitle: 'Arbeitserfahrung', blockTitle: AppLocalizations.of(context)!.work_experience,
contentType: ContentType.experience, contentType: ContentType.experience,
), ),
Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: 'Bildungsweg', blockTitle: AppLocalizations.of(context)!.education,
contentType: ContentType.education, contentType: ContentType.education,
), ),
Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: 'Weitere Kenntnisse', blockTitle: AppLocalizations.of(context)!.additional_skills,
contentType: ContentType.generalSkills, contentType: ContentType.generalSkills,
), ),
Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock( ContentBlock(
blockTitle: "Über mich", blockTitle: AppLocalizations.of(context)!.about_me,
contentType: ContentType.text, contentType: ContentType.text,
), ),
], ],
@@ -188,9 +190,12 @@ class _LandingPageState extends State<LandingPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Lebenslauf'), title: Text(AppLocalizations.of(context)!.resume),
actions: const [ actions: [
TextButton(onPressed: _launchURL, child: Text('Source Code')), TextButton(
onPressed: _launchURL,
child: Text(AppLocalizations.of(context)!.source_code)),
const LanguageDropdown(),
], ],
), ),
body: !loadingDone body: !loadingDone

View File

@@ -6,7 +6,7 @@ import 'package:resume/constants.dart';
class ContentProvider { class ContentProvider {
ContentProvider._(); ContentProvider._();
static const String _jsonPath = 'assets/content.json'; static const String _jsonPath = 'assets/content/content.json';
static Future<bool> init() async { static Future<bool> init() async {
try { try {
@@ -23,7 +23,7 @@ class ContentProvider {
'education': <List<dynamic>>[], 'education': <List<dynamic>>[],
'skills': <List<dynamic>>[], 'skills': <List<dynamic>>[],
'text': <String>[], 'text': <String>[],
'general_skills': <List<dynamic>>[], 'general_skills': <String>[],
}; };
static T getContent<T>(ContentType contentType) { static T getContent<T>(ContentType contentType) {

View File

@@ -1,12 +1,97 @@
import 'package:resume/constants.dart' show months; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class Tools { class Tools {
/// builds timespan-String out of two date-Strings. /// Builds a formatted time string from two date strings in the format 'YYYY-MM'.
/// Date has to be formatted as yyyy-MM ///
static buildTimeString(String startDate, String endDate) { /// Returns a string in the format 'Month Year - Month Year' using localized month names.
if (startDate.isEmpty || endDate.isEmpty) return ''; /// Returns an empty string if either input is empty or invalid.
final firstDate = DateTime.parse('$startDate-01'); ///
final secondDate = DateTime.parse('$endDate-01'); /// Parameters:
return '${months[firstDate.month - 1]} ${firstDate.year} - ${months[secondDate.month - 1]} ${secondDate.year}'; /// - [startDate]: String in 'YYYY-MM' format (e.g., '2024-03')
/// - [endDate]: String in 'YYYY-MM' format (e.g., '2024-12')
/// - [context]: BuildContext for accessing localizations
///
/// Example:
/// ```dart
/// final timeString = buildTimeString('2024-03', '2024-12', context);
/// // Returns "March 2024 - December 2024" (or localized equivalent)
/// ```
static String buildTimeString(
String startDate, String endDate, BuildContext context) {
// Check for empty or null inputs
if (startDate.isEmpty || endDate.isEmpty) {
return '';
}
try {
// Validate date format
if (!startDate.contains(RegExp(r'^\d{4}-\d{2}$')) ||
!endDate.contains(RegExp(r'^\d{4}-\d{2}$'))) {
return '';
}
// Parse dates with validation
final firstDate = DateTime.tryParse('$startDate-01');
final secondDate = DateTime.tryParse('$endDate-01');
// Check if parsing was successful
if (firstDate == null || secondDate == null) {
return '';
}
// Build the formatted string
return '${getLocalizedMonth(context, firstDate.month)} ${firstDate.year} - '
'${getLocalizedMonth(context, secondDate.month)} ${secondDate.year}';
} catch (e) {
// Handle any unexpected errors
debugPrint('Error building time string: $e');
return '';
}
}
/// Returns the localized month name as a [String] based on the provided month number.
///
/// The [context] is used to access the app's localizations.
/// The [monthNumber] must be between 1 and 12, where 1 represents January
/// and 12 represents December.
///
/// Throws an [ArgumentError] if the month number is not between 1 and 12.
///
/// Example:
/// ```dart
/// final monthName = getLocalizedMonth(context, 3); // Returns "March" or "März"
/// ```
static String getLocalizedMonth(BuildContext context, int monthNumber) {
final localizations = AppLocalizations.of(context)!;
switch (monthNumber) {
case 1:
return localizations.january;
case 2:
return localizations.february;
case 3:
return localizations.march;
case 4:
return localizations.april;
case 5:
return localizations.may;
case 6:
return localizations.june;
case 7:
return localizations.july;
case 8:
return localizations.august;
case 9:
return localizations.september;
case 10:
return localizations.october;
case 11:
return localizations.november;
case 12:
return localizations.december;
default:
throw ArgumentError('Month number must be between 1 and 12');
}
} }
} }

View File

@@ -18,23 +18,13 @@ class ContentBlock extends StatelessWidget {
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 ||
final content = ContentProvider.getContent<String>(ContentType.text); 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),
); );
} else if (contentType == ContentType.generalSkills) {
final content =
ContentProvider.getContent<List<dynamic>>(ContentType.generalSkills);
List<Widget> widgets = [];
for (int i = 0; i < content.length; i++) {
String text = i == content.length - 1
? content.elementAt(i)
: content.elementAt(i) + ', ';
widgets.add(Text(text));
}
return Row(children: widgets);
} }
// List-based content-blocks // List-based content-blocks
List<dynamic> content = List<dynamic> content =

View File

@@ -97,7 +97,7 @@ class ContentListTile extends StatelessWidget {
? Padding( ? Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Text( child: Text(
Tools.buildTimeString(startDate, endDate), Tools.buildTimeString(startDate, endDate, context),
style: Theme.of(context).textTheme.labelSmall!.copyWith( style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
@@ -106,7 +106,7 @@ class ContentListTile extends StatelessWidget {
: null, : null,
trailing: Breakpoints.sm < screenWidth trailing: Breakpoints.sm < screenWidth
? Text( ? Text(
Tools.buildTimeString(startDate, endDate), Tools.buildTimeString(startDate, endDate, context),
style: style:
TextStyle(color: Theme.of(context).colorScheme.secondary), TextStyle(color: Theme.of(context).colorScheme.secondary),
) )
@@ -149,7 +149,7 @@ class ContentListTile extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Text( child: Text(
Tools.buildTimeString(startDate, endDate), Tools.buildTimeString(startDate, endDate, context),
style: Theme.of(context).textTheme.labelSmall!.copyWith( style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
@@ -160,7 +160,7 @@ class ContentListTile extends StatelessWidget {
), ),
trailing: Breakpoints.sm < screenWidth trailing: Breakpoints.sm < screenWidth
? Text( ? Text(
Tools.buildTimeString(startDate, endDate), Tools.buildTimeString(startDate, endDate, context),
style: style:
TextStyle(color: Theme.of(context).colorScheme.secondary), TextStyle(color: Theme.of(context).colorScheme.secondary),
) )

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageDropdown extends StatelessWidget {
const LanguageDropdown({super.key});
@override
Widget build(BuildContext context) {
return DropdownButton(
value: 'de',
items: [
DropdownMenuItem(
value: 'de',
child: getMenuItem(
AppLocalizations.of(context)!.german,
'assets/de_icon.png',
),
),
DropdownMenuItem(
value: 'en',
child: getMenuItem(
AppLocalizations.of(context)!.english,
'assets/gb_icon.png',
),
),
],
onChanged: _onChanged,
);
}
void _onChanged(dynamic value) {}
Widget getMenuItem(String label, String imagePath) {
return Row(
children: [
Text(label),
const Padding(padding: EdgeInsets.only(right: 8)),
Image.asset(imagePath, width: 30),
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageWidget extends StatelessWidget { class LanguageWidget extends StatelessWidget {
const LanguageWidget({super.key}); const LanguageWidget({super.key});
@@ -18,8 +19,9 @@ class LanguageWidget extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Deutsch', style: Theme.of(context).textTheme.bodyLarge), Text(AppLocalizations.of(context)!.german,
Text('Muttersprache', style: Theme.of(context).textTheme.bodyLarge),
Text(AppLocalizations.of(context)!.mother_tongue,
style: Theme.of(context).textTheme.bodyMedium), style: Theme.of(context).textTheme.bodyMedium),
], ],
), ),
@@ -36,8 +38,10 @@ class LanguageWidget extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Englisch', style: Theme.of(context).textTheme.bodyLarge), Text(AppLocalizations.of(context)!.english,
Text('Sehr gut', style: Theme.of(context).textTheme.bodyMedium), style: Theme.of(context).textTheme.bodyLarge),
Text(AppLocalizations.of(context)!.very_good,
style: Theme.of(context).textTheme.bodyMedium),
], ],
), ),
], ],

View File

@@ -12,6 +12,9 @@ dependencies:
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
url_launcher: ^6.3.1 url_launcher: ^6.3.1
flutter_localizations:
sdk: flutter
intl: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -20,10 +23,11 @@ dev_dependencies:
flutter_lints: ^4.0.0 flutter_lints: ^4.0.0
flutter: flutter:
generate: true
uses-material-design: true uses-material-design: true
assets: assets:
- assets/content.json - assets/content/content.json
- assets/profile.jpg - assets/profile.jpg
- assets/de_icon.png - assets/de_icon.png
- assets/gb_icon.png - assets/gb_icon.png