diff --git a/.gitignore b/.gitignore index e000548..72eb56e 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/devtools_options.yaml diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..f7805c8 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n/arb +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-dir: lib/l10n/generated +nullable-getter: false \ No newline at end of file diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 737882e..ce06f85 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; + /// Button types used for styling the [CustomWidthButton] enum ButtonType { primary, secondary, tertiary } @@ -30,16 +33,17 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.leastPoints]: The player with the fewest points wins. enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints } -/// Translates a [Ruleset] enum value to its corresponding string representation. -String translateRulesetToString(Ruleset ruleset) { +/// Translates a [Ruleset] enum value to its corresponding localized string. +String translateRulesetToString(Ruleset ruleset, BuildContext context) { + final loc = AppLocalizations.of(context); switch (ruleset) { case Ruleset.singleWinner: - return 'Single Winner'; + return loc.single_winner; case Ruleset.singleLoser: - return 'Single Loser'; + return loc.single_loser; case Ruleset.mostPoints: - return 'Most Points'; + return loc.most_points; case Ruleset.leastPoints: - return 'Least Points'; + return loc.least_points; } } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb new file mode 100644 index 0000000..89354bd --- /dev/null +++ b/lib/l10n/arb/app_de.arb @@ -0,0 +1,83 @@ +{ + "@@locale": "de", + "all_players": "Alle Spieler:innen:", + "all_players_selected": "Alle Spieler:innen ausgewählt", + "amount_of_matches": "Anzahl der Matches", + "cancel": "Abbrechen", + "choose_game": "Spielvorlage wählen", + "choose_group": "Gruppe wählen", + "choose_ruleset": "Regelwerk wählen", + "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "create_group": "Gruppe erstellen", + "create_match": "Match erstellen", + "create_new_group": "Neue Gruppe erstellen", + "create_new_match": "Neues Match erstellen", + "data_successfully_deleted": "Daten erfolgreich gelöscht", + "data_successfully_exported": "Daten erfolgreich exportiert", + "data_successfully_imported": "Daten erfolgreich importiert", + "days_ago": "vor {count} Tagen", + "delete": "Löschen", + "delete_all_data": "Alle Daten löschen?", + "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "error_reading_file": "Fehler beim Lesen der Datei", + "export_canceled": "Export abgebrochen", + "export_data": "Daten exportieren", + "format_exception": "Formatfehler (siehe Konsole)", + "game": "Spielvorlage", + "game_name": "Spielvorlagenname", + "group": "Gruppe", + "group_name": "Gruppenname", + "groups": "Gruppen", + "home": "Startseite", + "import_canceled": "Import abgebrochen", + "import_data": "Daten importieren", + "info": "Info", + "invalid_schema": "Ungültiges Schema", + "least_points": "Niedrigste Punkte", + "match_in_progress": "Match läuft...", + "match_name": "Matchname", + "matches": "Matches", + "menu": "Menü", + "most_points": "Höchste Punkte", + "no_data_available": "Keine Daten verfügbar", + "no_groups_created_yet": "Noch keine Gruppen erstellt", + "no_matches_created_yet": "Noch keine Matches erstellt", + "no_players_created_yet": "Noch keine Spieler:in erstellt", + "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", + "no_players_selected": "Keine Spieler:in ausgewählt", + "no_recent_matches_available": "Keine letzten Matches verfügbar", + "no_second_match_available": "Kein zweites Match verfügbar", + "no_statistics_available": "Keine Statistiken verfügbar", + "none": "Kein", + "none_group": "Keine", + "not_available": "Nicht verfügbar", + "player_name": "Spieler:innenname", + "players": "Spieler:in", + "players_count": "{count} Spieler", + "quick_create": "Schnellzugriff", + "recent_matches": "Letzte Matches", + "ruleset": "Regelwerk", + "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", + "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", + "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", + "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", + "search_for_groups": "Nach Gruppen suchen", + "search_for_players": "Nach Spieler:innen suchen", + "select_winner": "Gewinner:in wählen:", + "selected_players": "Ausgewählte Spieler:in: {count}", + "settings": "Einstellungen", + "single_loser": "Ein:e Verlierer:in", + "single_winner": "Ein:e Gewinner:in", + "statistics": "Statistiken", + "stats": "Statistiken", + "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", + "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden", + "today_at": "Heute um {time}", + "undo": "Rückgängig", + "unknown_exception": "Unbekannter Fehler (siehe Konsole)", + "winner": "Gewinner:in: {winnerName}", + "winrate": "Siegquote", + "wins": "Siege", + "yesterday_at": "Gestern um {time}" +} \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb new file mode 100644 index 0000000..d567f50 --- /dev/null +++ b/lib/l10n/arb/app_en.arb @@ -0,0 +1,367 @@ +{ + "@@locale": "en", + "@all_players": { + "description": "Label for all players list" + }, + "@all_players_selected": { + "description": "Message when all players are added to selection" + }, + "@amount_of_matches": { + "description": "Label for amount of matches statistic" + }, + "@cancel": { + "description": "Cancel button text" + }, + "@choose_game": { + "description": "Label for choosing a game" + }, + "@choose_group": { + "description": "Label for choosing a group" + }, + "@choose_ruleset": { + "description": "Label for choosing a ruleset" + }, + "@could_not_add_player": { + "description": "Error message when adding a player fails", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, + "@create_group": { + "description": "Button text to create a group" + }, + "@create_match": { + "description": "Button text to create a match" + }, + "@create_new_group": { + "description": "Button text to create a new group" + }, + "@create_new_match": { + "description": "Button text to create a new match" + }, + "@data_successfully_deleted": { + "description": "Success message after deleting data" + }, + "@data_successfully_exported": { + "description": "Success message after exporting data" + }, + "@data_successfully_imported": { + "description": "Success message after importing data" + }, + "@days_ago": { + "description": "Date format for days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@delete": { + "description": "Delete button text" + }, + "@delete_all_data": { + "description": "Confirmation dialog for deleting all data" + }, + "@error_creating_group": { + "description": "Error message when group creation fails" + }, + "@error_reading_file": { + "description": "Error message when file cannot be read" + }, + "@export_canceled": { + "description": "Message when export is canceled" + }, + "@export_data": { + "description": "Export data menu item" + }, + "@format_exception": { + "description": "Error message for format exceptions" + }, + "@game": { + "description": "Game label" + }, + "@game_name": { + "description": "Placeholder for game name search" + }, + "@game_tracker": { + "description": "App Name" + }, + "@group": { + "description": "Group label" + }, + "@group_name": { + "description": "Placeholder for group name input" + }, + "@groups": { + "description": "Label for groups" + }, + "@home": { + "description": "Home tab label" + }, + "@import_canceled": { + "description": "Message when import is canceled" + }, + "@import_data": { + "description": "Import data menu item" + }, + "@info": { + "description": "Info label" + }, + "@invalid_schema": { + "description": "Error message for invalid schema" + }, + "@least_points": { + "description": "Title for least points ruleset" + }, + "@match_in_progress": { + "description": "Message when match is in progress" + }, + "@match_name": { + "description": "Placeholder for match name input" + }, + "@matches": { + "description": "Label for matches" + }, + "@menu": { + "description": "Menu label" + }, + "@most_points": { + "description": "Title for most points ruleset" + }, + "@no_data_available": { + "description": "Message when no data in the statistic tiles is given" + }, + "@no_groups_created_yet": { + "description": "Message when no groups exist" + }, + "@no_matches_created_yet": { + "description": "Message when no matches exist" + }, + "@no_players_created_yet": { + "description": "Message when no players exist" + }, + "@no_players_found_with_that_name": { + "description": "Message when search returns no results" + }, + "@no_players_selected": { + "description": "Message when no players are selected" + }, + "@no_recent_matches_available": { + "description": "Message when no recent matches exist" + }, + "@no_second_match_available": { + "description": "Message when no second match exists" + }, + "@no_statistics_available": { + "description": "Message when no statistics are available, because no matches were played yet" + }, + "@none": { + "description": "None option label" + }, + "@none_group": { + "description": "None group option label" + }, + "@not_available": { + "description": "Abbreviation for not available" + }, + "@player_name": { + "description": "Placeholder for player name input" + }, + "@players": { + "description": "Players label" + }, + "@players_count": { + "description": "Shows the number of players", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@quick_create": { + "description": "Title for quick create section" + }, + "@recent_matches": { + "description": "Title for recent matches section" + }, + "@ruleset": { + "description": "Ruleset label" + }, + "@ruleset_least_points": { + "description": "Description for least points ruleset" + }, + "@ruleset_most_points": { + "description": "Description for most points ruleset" + }, + "@ruleset_single_loser": { + "description": "Description for single loser ruleset" + }, + "@ruleset_single_winner": { + "description": "Description for single winner ruleset" + }, + "@search_for_groups": { + "description": "Hint text for group search input field" + }, + "@search_for_players": { + "description": "Hint text for player search input field" + }, + "@select_winner": { + "description": "Label to select the winner" + }, + "@selected_players": { + "description": "Shows the number of selected players", + "placeholders": { + "count": { + "type": "int", + "format": "compact" + } + } + }, + "@settings": { + "description": "Settings label" + }, + "@single_loser": { + "description": "Title for single loser ruleset" + }, + "@single_winner": { + "description": "Title for single winner ruleset" + }, + "@statistics": { + "description": "Statistics tab label" + }, + "@stats": { + "description": "Stats tab label (short)" + }, + "@successfully_added_player": { + "description": "Success message when adding a player", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, + "@there_is_no_group_matching_your_search": { + "description": "Message when search returns no groups" + }, + "@this_cannot_be_undone": { + "description": "Warning message for irreversible actions" + }, + "@today_at": { + "description": "Date format for today", + "placeholders": { + "time": { + "type": "String", + "example": "14:30" + } + } + }, + "@undo": { + "description": "Undo button text" + }, + "@unknown_exception": { + "description": "Error message for unknown exceptions" + }, + "@winner": { + "description": "Winner label" + }, + "@winrate": { + "description": "Label for winrate statistic" + }, + "@wins": { + "description": "Label for wins statistic" + }, + "@yesterday_at": { + "description": "Date format for yesterday", + "placeholders": { + "time": { + "type": "String", + "example": "14:30" + } + } + }, + "all_players": "All players:", + "all_players_selected": "All players selected", + "amount_of_matches": "Amount of Matches", + "cancel": "Cancel", + "choose_game": "Choose Game", + "choose_group": "Choose Group", + "choose_ruleset": "Choose Ruleset", + "could_not_add_player": "Could not add player {playerName}", + "create_group": "Create Group", + "create_match": "Create match", + "create_new_group": "Create new group", + "create_new_match": "Create new match", + "data_successfully_deleted": "Data successfully deleted", + "data_successfully_exported": "Data successfully exported", + "data_successfully_imported": "Data successfully imported", + "days_ago": "{count} days ago", + "delete": "Delete", + "delete_all_data": "Delete all data?", + "error_creating_group": "Error while creating group, please try again", + "error_reading_file": "Error reading file", + "export_canceled": "Export canceled", + "export_data": "Export data", + "format_exception": "Format Exception (see console)", + "game": "Game", + "game_name": "Game Name", + "game_tracker": "Game Tracker", + "group": "Group", + "group_name": "Group name", + "groups": "Groups", + "home": "Home", + "import_canceled": "Import canceled", + "import_data": "Import data", + "info": "Info", + "invalid_schema": "Invalid Schema", + "least_points": "Least Points", + "match_in_progress": "Match in progress...", + "match_name": "Match name", + "matches": "Matches", + "menu": "Menu", + "most_points": "Most Points", + "no_data_available": "No data available", + "no_groups_created_yet": "No groups created yet", + "no_matches_created_yet": "No matches created yet", + "no_players_created_yet": "No players created yet", + "no_players_found_with_that_name": "No players found with that name", + "no_players_selected": "No players selected", + "no_recent_matches_available": "No recent matches available", + "no_second_match_available": "No second match available", + "no_statistics_available": "No statistics available", + "none": "None", + "none_group": "None", + "not_available": "Not available", + "player_name": "Player name", + "players": "Players", + "players_count": "{count} Players", + "quick_create": "Quick Create", + "recent_matches": "Recent Matches", + "ruleset": "Ruleset", + "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", + "ruleset_most_points": "Traditional ruleset: the player with the most points wins.", + "ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.", + "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", + "search_for_groups": "Search for groups", + "search_for_players": "Search for players", + "select_winner": "Select Winner:", + "selected_players": "Selected players: {count}", + "settings": "Settings", + "single_loser": "Single Loser", + "single_winner": "Single Winner", + "statistics": "Statistics", + "stats": "Stats", + "successfully_added_player": "Successfully added player {playerName}", + "there_is_no_group_matching_your_search": "There is no group matching your search", + "this_cannot_be_undone": "This can't be undone", + "today_at": "Today at {time}", + "undo": "Undo", + "unknown_exception": "Unknown Exception (see console)", + "winner": "Winner", + "winrate": "Winrate", + "wins": "Wins", + "yesterday_at": "Yesterday at {time}" +} \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart new file mode 100644 index 0000000..951ff22 --- /dev/null +++ b/lib/l10n/generated/app_localizations.dart @@ -0,0 +1,620 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_de.dart'; +import 'app_localizations_en.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'generated/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('de'), + Locale('en'), + ]; + + /// Label for choosing a group + /// + /// In en, this message translates to: + /// **'Choose Group'** + String get choose_group; + + /// Button text to create a new match + /// + /// In en, this message translates to: + /// **'Create new match'** + String get create_new_match; + + /// Label for choosing a ruleset + /// + /// In en, this message translates to: + /// **'Choose Ruleset'** + String get choose_ruleset; + + /// Label for choosing a game + /// + /// In en, this message translates to: + /// **'Choose Game'** + String get choose_game; + + /// Label to select the winner + /// + /// In en, this message translates to: + /// **'Select Winner:'** + String get select_winner; + + /// App Name + /// + /// In en, this message translates to: + /// **'Game Tracker'** + String get game_tracker; + + /// Message when no recent matches exist + /// + /// In en, this message translates to: + /// **'No recent matches available'** + String get no_recent_matches_available; + + /// Message when no second match exists + /// + /// In en, this message translates to: + /// **'No second match available'** + String get no_second_match_available; + + /// Confirmation dialog for deleting all data + /// + /// In en, this message translates to: + /// **'Delete all data?'** + String get delete_all_data; + + /// Cancel button text + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// Delete button text + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// Button text to create a new group + /// + /// In en, this message translates to: + /// **'Create new group'** + String get create_new_group; + + /// Error message when group creation fails + /// + /// In en, this message translates to: + /// **'Error while creating group, please try again'** + String get error_creating_group; + + /// Shows the number of selected players + /// + /// In en, this message translates to: + /// **'Selected players: {count}'** + String selected_players(int count); + + /// Message when no players are selected + /// + /// In en, this message translates to: + /// **'No players selected'** + String get no_players_selected; + + /// Label for all players list + /// + /// In en, this message translates to: + /// **'All players:'** + String get all_players; + + /// Success message when adding a player + /// + /// In en, this message translates to: + /// **'Successfully added player {playerName}'** + String successfully_added_player(String playerName); + + /// Error message when adding a player fails + /// + /// In en, this message translates to: + /// **'Could not add player {playerName}'** + String could_not_add_player(String playerName); + + /// Shows the winner's name + /// + /// In en, this message translates to: + /// **'Winner: {winnerName}'** + String winner(String winnerName); + + /// Players label + /// + /// In en, this message translates to: + /// **'Players'** + String get players; + + /// Message when no statistics are available, because no matches were played yet + /// + /// In en, this message translates to: + /// **'No statistics available'** + String get no_statistics_available; + + /// Message when no data in the statistic tiles is given + /// + /// In en, this message translates to: + /// **'No data available'** + String get no_data_available; + + /// Label for matches + /// + /// In en, this message translates to: + /// **'Matches'** + String get matches; + + /// Label for groups + /// + /// In en, this message translates to: + /// **'Groups'** + String get groups; + + /// Title for recent matches section + /// + /// In en, this message translates to: + /// **'Recent Matches'** + String get recent_matches; + + /// Title for quick create section + /// + /// In en, this message translates to: + /// **'Quick Create'** + String get quick_create; + + /// Message when match is in progress + /// + /// In en, this message translates to: + /// **'Match in progress...'** + String get match_in_progress; + + /// Menu label + /// + /// In en, this message translates to: + /// **'Menu'** + String get menu; + + /// Settings label + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// Export data menu item + /// + /// In en, this message translates to: + /// **'Export data'** + String get export_data; + + /// Import data menu item + /// + /// In en, this message translates to: + /// **'Import data'** + String get import_data; + + /// Warning message for irreversible actions + /// + /// In en, this message translates to: + /// **'This can\'t be undone'** + String get this_cannot_be_undone; + + /// Success message after deleting data + /// + /// In en, this message translates to: + /// **'Data successfully deleted'** + String get data_successfully_deleted; + + /// Success message after importing data + /// + /// In en, this message translates to: + /// **'Data successfully imported'** + String get data_successfully_imported; + + /// Error message for invalid schema + /// + /// In en, this message translates to: + /// **'Invalid Schema'** + String get invalid_schema; + + /// Error message when file cannot be read + /// + /// In en, this message translates to: + /// **'Error reading file'** + String get error_reading_file; + + /// Message when import is canceled + /// + /// In en, this message translates to: + /// **'Import canceled'** + String get import_canceled; + + /// Error message for format exceptions + /// + /// In en, this message translates to: + /// **'Format Exception (see console)'** + String get format_exception; + + /// Error message for unknown exceptions + /// + /// In en, this message translates to: + /// **'Unknown Exception (see console)'** + String get unknown_exception; + + /// Success message after exporting data + /// + /// In en, this message translates to: + /// **'Data successfully exported'** + String get data_successfully_exported; + + /// Message when export is canceled + /// + /// In en, this message translates to: + /// **'Export canceled'** + String get export_canceled; + + /// Undo button text + /// + /// In en, this message translates to: + /// **'Undo'** + String get undo; + + /// Label for wins statistic + /// + /// In en, this message translates to: + /// **'Wins'** + String get wins; + + /// Label for winrate statistic + /// + /// In en, this message translates to: + /// **'Winrate'** + String get winrate; + + /// Label for amount of matches statistic + /// + /// In en, this message translates to: + /// **'Amount of Matches'** + String get amount_of_matches; + + /// Info label + /// + /// In en, this message translates to: + /// **'Info'** + String get info; + + /// Message when no groups exist + /// + /// In en, this message translates to: + /// **'No groups created yet'** + String get no_groups_created_yet; + + /// Message when no players exist + /// + /// In en, this message translates to: + /// **'No players created yet'** + String get no_players_created_yet; + + /// Button text to create a group + /// + /// In en, this message translates to: + /// **'Create Group'** + String get create_group; + + /// Placeholder for group name input + /// + /// In en, this message translates to: + /// **'Group name'** + String get group_name; + + /// Placeholder for player name input + /// + /// In en, this message translates to: + /// **'Player name'** + String get player_name; + + /// Message when no matches exist + /// + /// In en, this message translates to: + /// **'No matches created yet'** + String get no_matches_created_yet; + + /// Placeholder for match name input + /// + /// In en, this message translates to: + /// **'Match name'** + String get match_name; + + /// Game label + /// + /// In en, this message translates to: + /// **'Game'** + String get game; + + /// Ruleset label + /// + /// In en, this message translates to: + /// **'Ruleset'** + String get ruleset; + + /// Group label + /// + /// In en, this message translates to: + /// **'Group'** + String get group; + + /// None option label + /// + /// In en, this message translates to: + /// **'None'** + String get none; + + /// None group option label + /// + /// In en, this message translates to: + /// **'None'** + String get none_group; + + /// Button text to create a match + /// + /// In en, this message translates to: + /// **'Create match'** + String get create_match; + + /// Message when search returns no results + /// + /// In en, this message translates to: + /// **'No players found with that name'** + String get no_players_found_with_that_name; + + /// Message when all players are added to selection + /// + /// In en, this message translates to: + /// **'All players selected'** + String get all_players_selected; + + /// Date format for today + /// + /// In en, this message translates to: + /// **'Today at {time}'** + String today_at(String time); + + /// Date format for yesterday + /// + /// In en, this message translates to: + /// **'Yesterday at {time}'** + String yesterday_at(String time); + + /// Date format for days ago + /// + /// In en, this message translates to: + /// **'{count} days ago'** + String days_ago(int count); + + /// Home tab label + /// + /// In en, this message translates to: + /// **'Home'** + String get home; + + /// Statistics tab label + /// + /// In en, this message translates to: + /// **'Statistics'** + String get statistics; + + /// Stats tab label (short) + /// + /// In en, this message translates to: + /// **'Stats'** + String get stats; + + /// Shows the number of players + /// + /// In en, this message translates to: + /// **'{count} Players'** + String players_count(int count); + + /// Message when search returns no groups + /// + /// In en, this message translates to: + /// **'There is no group matching your search'** + String get there_is_no_group_matching_your_search; + + /// Placeholder for game name search + /// + /// In en, this message translates to: + /// **'Game Name'** + String get game_name; + + /// Description for single winner ruleset + /// + /// In en, this message translates to: + /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** + String get ruleset_single_winner; + + /// Description for single loser ruleset + /// + /// In en, this message translates to: + /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** + String get ruleset_single_loser; + + /// Description for most points ruleset + /// + /// In en, this message translates to: + /// **'Traditional ruleset: the player with the most points wins.'** + String get ruleset_most_points; + + /// Description for least points ruleset + /// + /// In en, this message translates to: + /// **'Inverse scoring: the player with the fewest points wins.'** + String get ruleset_least_points; + + /// Title for single winner ruleset + /// + /// In en, this message translates to: + /// **'Single Winner'** + String get single_winner; + + /// Title for single loser ruleset + /// + /// In en, this message translates to: + /// **'Single Loser'** + String get single_loser; + + /// Title for most points ruleset + /// + /// In en, this message translates to: + /// **'Most Points'** + String get most_points; + + /// Title for least points ruleset + /// + /// In en, this message translates to: + /// **'Least Points'** + String get least_points; + + /// Hint text for player search input field + /// + /// In en, this message translates to: + /// **'Search for players'** + String get search_for_players; + + /// Hint text for group search input field + /// + /// In en, this message translates to: + /// **'Search for groups'** + String get search_for_groups; + + /// Abbreviation for not available + /// + /// In en, this message translates to: + /// **'Not available'** + String get not_available; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['de', 'en'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart new file mode 100644 index 0000000..3f3e36e --- /dev/null +++ b/lib/l10n/generated/app_localizations_de.dart @@ -0,0 +1,282 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get choose_group => 'Gruppe wählen'; + + @override + String get create_new_match => 'Neues Match erstellen'; + + @override + String get choose_ruleset => 'Regelwerk wählen'; + + @override + String get choose_game => 'Spielvorlage wählen'; + + @override + String get select_winner => 'Gewinner:in wählen:'; + + @override + String get game_tracker => 'Game Tracker'; + + @override + String get no_recent_matches_available => 'Keine letzten Matches verfügbar'; + + @override + String get no_second_match_available => 'Kein zweites Match verfügbar'; + + @override + String get delete_all_data => 'Alle Daten löschen?'; + + @override + String get cancel => 'Abbrechen'; + + @override + String get delete => 'Löschen'; + + @override + String get create_new_group => 'Neue Gruppe erstellen'; + + @override + String get error_creating_group => + 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + + @override + String selected_players(int count) { + final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( + locale: localeName, + ); + final String countString = countNumberFormat.format(count); + + return 'Ausgewählte Spieler:in: $countString'; + } + + @override + String get no_players_selected => 'Keine Spieler:in ausgewählt'; + + @override + String get all_players => 'Alle Spieler:innen:'; + + @override + String successfully_added_player(String playerName) { + return 'Spieler:in $playerName erfolgreich hinzugefügt'; + } + + @override + String could_not_add_player(String playerName) { + return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; + } + + @override + String winner(String winnerName) { + return 'Gewinner:in: $winnerName'; + } + + @override + String get players => 'Spieler:in'; + + @override + String get no_statistics_available => 'Keine Statistiken verfügbar'; + + @override + String get no_data_available => 'Keine Daten verfügbar'; + + @override + String get matches => 'Matches'; + + @override + String get groups => 'Gruppen'; + + @override + String get recent_matches => 'Letzte Matches'; + + @override + String get quick_create => 'Schnellzugriff'; + + @override + String get match_in_progress => 'Match läuft...'; + + @override + String get menu => 'Menü'; + + @override + String get settings => 'Einstellungen'; + + @override + String get export_data => 'Daten exportieren'; + + @override + String get import_data => 'Daten importieren'; + + @override + String get this_cannot_be_undone => + 'Dies kann nicht rückgängig gemacht werden'; + + @override + String get data_successfully_deleted => 'Daten erfolgreich gelöscht'; + + @override + String get data_successfully_imported => 'Daten erfolgreich importiert'; + + @override + String get invalid_schema => 'Ungültiges Schema'; + + @override + String get error_reading_file => 'Fehler beim Lesen der Datei'; + + @override + String get import_canceled => 'Import abgebrochen'; + + @override + String get format_exception => 'Formatfehler (siehe Konsole)'; + + @override + String get unknown_exception => 'Unbekannter Fehler (siehe Konsole)'; + + @override + String get data_successfully_exported => 'Daten erfolgreich exportiert'; + + @override + String get export_canceled => 'Export abgebrochen'; + + @override + String get undo => 'Rückgängig'; + + @override + String get wins => 'Siege'; + + @override + String get winrate => 'Siegquote'; + + @override + String get amount_of_matches => 'Anzahl der Matches'; + + @override + String get info => 'Info'; + + @override + String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; + + @override + String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; + + @override + String get create_group => 'Gruppe erstellen'; + + @override + String get group_name => 'Gruppenname'; + + @override + String get player_name => 'Spieler:innenname'; + + @override + String get no_matches_created_yet => 'Noch keine Matches erstellt'; + + @override + String get match_name => 'Matchname'; + + @override + String get game => 'Spielvorlage'; + + @override + String get ruleset => 'Regelwerk'; + + @override + String get group => 'Gruppe'; + + @override + String get none => 'Kein'; + + @override + String get none_group => 'Keine'; + + @override + String get create_match => 'Match erstellen'; + + @override + String get no_players_found_with_that_name => + 'Keine Spieler:in mit diesem Namen gefunden'; + + @override + String get all_players_selected => 'Alle Spieler:innen ausgewählt'; + + @override + String today_at(String time) { + return 'Heute um $time'; + } + + @override + String yesterday_at(String time) { + return 'Gestern um $time'; + } + + @override + String days_ago(int count) { + return 'vor $count Tagen'; + } + + @override + String get home => 'Startseite'; + + @override + String get statistics => 'Statistiken'; + + @override + String get stats => 'Statistiken'; + + @override + String players_count(int count) { + return '$count Spieler'; + } + + @override + String get there_is_no_group_matching_your_search => + 'Es gibt keine Gruppe, die deiner Suche entspricht'; + + @override + String get game_name => 'Spielvorlagenname'; + + @override + String get ruleset_single_winner => + 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.'; + + @override + String get ruleset_single_loser => + 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; + + @override + String get ruleset_most_points => + 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; + + @override + String get ruleset_least_points => + 'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.'; + + @override + String get single_winner => 'Ein:e Gewinner:in'; + + @override + String get single_loser => 'Ein:e Verlierer:in'; + + @override + String get most_points => 'Höchste Punkte'; + + @override + String get least_points => 'Niedrigste Punkte'; + + @override + String get search_for_players => 'Nach Spieler:innen suchen'; + + @override + String get search_for_groups => 'Nach Gruppen suchen'; + + @override + String get not_available => 'Nicht verfügbar'; +} diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart new file mode 100644 index 0000000..263714c --- /dev/null +++ b/lib/l10n/generated/app_localizations_en.dart @@ -0,0 +1,281 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get choose_group => 'Choose Group'; + + @override + String get create_new_match => 'Create new match'; + + @override + String get choose_ruleset => 'Choose Ruleset'; + + @override + String get choose_game => 'Choose Game'; + + @override + String get select_winner => 'Select Winner:'; + + @override + String get game_tracker => 'Game Tracker'; + + @override + String get no_recent_matches_available => 'No recent matches available'; + + @override + String get no_second_match_available => 'No second match available'; + + @override + String get delete_all_data => 'Delete all data?'; + + @override + String get cancel => 'Cancel'; + + @override + String get delete => 'Delete'; + + @override + String get create_new_group => 'Create new group'; + + @override + String get error_creating_group => + 'Error while creating group, please try again'; + + @override + String selected_players(int count) { + final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( + locale: localeName, + ); + final String countString = countNumberFormat.format(count); + + return 'Selected players: $countString'; + } + + @override + String get no_players_selected => 'No players selected'; + + @override + String get all_players => 'All players:'; + + @override + String successfully_added_player(String playerName) { + return 'Successfully added player $playerName'; + } + + @override + String could_not_add_player(String playerName) { + return 'Could not add player $playerName'; + } + + @override + String winner(String winnerName) { + return 'Winner: $winnerName'; + } + + @override + String get players => 'Players'; + + @override + String get no_statistics_available => 'No statistics available'; + + @override + String get no_data_available => 'No data available'; + + @override + String get matches => 'Matches'; + + @override + String get groups => 'Groups'; + + @override + String get recent_matches => 'Recent Matches'; + + @override + String get quick_create => 'Quick Create'; + + @override + String get match_in_progress => 'Match in progress...'; + + @override + String get menu => 'Menu'; + + @override + String get settings => 'Settings'; + + @override + String get export_data => 'Export data'; + + @override + String get import_data => 'Import data'; + + @override + String get this_cannot_be_undone => 'This can\'t be undone'; + + @override + String get data_successfully_deleted => 'Data successfully deleted'; + + @override + String get data_successfully_imported => 'Data successfully imported'; + + @override + String get invalid_schema => 'Invalid Schema'; + + @override + String get error_reading_file => 'Error reading file'; + + @override + String get import_canceled => 'Import canceled'; + + @override + String get format_exception => 'Format Exception (see console)'; + + @override + String get unknown_exception => 'Unknown Exception (see console)'; + + @override + String get data_successfully_exported => 'Data successfully exported'; + + @override + String get export_canceled => 'Export canceled'; + + @override + String get undo => 'Undo'; + + @override + String get wins => 'Wins'; + + @override + String get winrate => 'Winrate'; + + @override + String get amount_of_matches => 'Amount of Matches'; + + @override + String get info => 'Info'; + + @override + String get no_groups_created_yet => 'No groups created yet'; + + @override + String get no_players_created_yet => 'No players created yet'; + + @override + String get create_group => 'Create Group'; + + @override + String get group_name => 'Group name'; + + @override + String get player_name => 'Player name'; + + @override + String get no_matches_created_yet => 'No matches created yet'; + + @override + String get match_name => 'Match name'; + + @override + String get game => 'Game'; + + @override + String get ruleset => 'Ruleset'; + + @override + String get group => 'Group'; + + @override + String get none => 'None'; + + @override + String get none_group => 'None'; + + @override + String get create_match => 'Create match'; + + @override + String get no_players_found_with_that_name => + 'No players found with that name'; + + @override + String get all_players_selected => 'All players selected'; + + @override + String today_at(String time) { + return 'Today at $time'; + } + + @override + String yesterday_at(String time) { + return 'Yesterday at $time'; + } + + @override + String days_ago(int count) { + return '$count days ago'; + } + + @override + String get home => 'Home'; + + @override + String get statistics => 'Statistics'; + + @override + String get stats => 'Stats'; + + @override + String players_count(int count) { + return '$count Players'; + } + + @override + String get there_is_no_group_matching_your_search => + 'There is no group matching your search'; + + @override + String get game_name => 'Game Name'; + + @override + String get ruleset_single_winner => + 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'; + + @override + String get ruleset_single_loser => + 'Exactly one loser is determined; last place receives the penalty or consequence.'; + + @override + String get ruleset_most_points => + 'Traditional ruleset: the player with the most points wins.'; + + @override + String get ruleset_least_points => + 'Inverse scoring: the player with the fewest points wins.'; + + @override + String get single_winner => 'Single Winner'; + + @override + String get single_loser => 'Single Loser'; + + @override + String get most_points => 'Most Points'; + + @override + String get least_points => 'Least Points'; + + @override + String get search_for_players => 'Search for players'; + + @override + String get search_for_groups => 'Search for groups'; + + @override + String get not_available => 'Not available'; +} diff --git a/lib/main.dart b/lib/main.dart index 98c40f8..c1ed977 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/custom_navigation_bar.dart'; import 'package:provider/provider.dart'; @@ -20,8 +21,20 @@ class GameTracker extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + localeResolutionCallback: (locale, supportedLocales) { + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale?.languageCode) { + return supportedLocale; + } + } + return supportedLocales.firstWhere( + (locale) => locale.languageCode == 'en', + ); + }, debugShowCheckedModeBanner: false, - title: 'Game Tracker', + onGenerateTitle: (context) => AppLocalizations.of(context).game_tracker, darkTheme: ThemeData.dark(), themeMode: ThemeMode.dark, // forces dark mode diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 2ec28fa..1e38808 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart'; import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart'; @@ -19,13 +20,9 @@ class _CustomNavigationBarState extends State int currentIndex = 0; int tabKeyCount = 0; - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); // Pretty ugly but works final List tabs = [ KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), @@ -46,7 +43,7 @@ class _CustomNavigationBarState extends State appBar: AppBar( centerTitle: true, title: Text( - _currentTabTitle(), + _currentTabTitle(context), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), backgroundColor: CustomTheme.backgroundColor, @@ -89,28 +86,28 @@ class _CustomNavigationBarState extends State index: 0, isSelected: currentIndex == 0, icon: Icons.home_rounded, - label: 'Home', + label: loc.home, onTabTapped: onTabTapped, ), NavbarItem( index: 1, isSelected: currentIndex == 1, icon: Icons.gamepad_rounded, - label: 'Matches', + label: loc.matches, onTabTapped: onTabTapped, ), NavbarItem( index: 2, isSelected: currentIndex == 2, icon: Icons.group_rounded, - label: 'Groups', + label: loc.groups, onTabTapped: onTabTapped, ), NavbarItem( index: 3, isSelected: currentIndex == 3, icon: Icons.bar_chart_rounded, - label: 'Stats', + label: loc.statistics, onTabTapped: onTabTapped, ), ], @@ -128,16 +125,17 @@ class _CustomNavigationBarState extends State }); } - String _currentTabTitle() { + String _currentTabTitle(context) { + final loc = AppLocalizations.of(context); switch (currentIndex) { case 0: - return 'Home'; + return loc.home; case 1: - return 'Matches'; + return loc.matches; case 2: - return 'Groups'; + return loc.groups; case 3: - return 'Statistics'; + return loc.statistics; default: return ''; } diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index f20fb4e..cba22ef 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -4,6 +4,7 @@ import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; import 'package:game_tracker/presentation/widgets/player_selection.dart'; import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; @@ -19,11 +20,13 @@ class CreateGroupView extends StatefulWidget { class _CreateGroupViewState extends State { final _groupNameController = TextEditingController(); late final AppDatabase db; + List selectedPlayers = []; @override void initState() { super.initState(); + db = Provider.of(context, listen: false); _groupNameController.addListener(() { setState(() {}); @@ -38,14 +41,15 @@ class _CreateGroupViewState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, - title: const Text( - 'Create new group', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + title: Text( + loc.create_new_group, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), centerTitle: true, ), @@ -57,7 +61,7 @@ class _CreateGroupViewState extends State { margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: TextInputField( controller: _groupNameController, - hintText: 'Group name', + hintText: loc.group_name, onChanged: (value) { setState(() {}); }, @@ -73,7 +77,7 @@ class _CreateGroupViewState extends State { ), ), CustomWidthButton( - text: 'Create group', + text: loc.create_group, sizeRelativeToWidth: 0.95, buttonType: ButtonType.primary, onPressed: @@ -94,10 +98,12 @@ class _CreateGroupViewState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: CustomTheme.boxColor, - content: const Center( + content: Center( child: Text( - 'Error while creating group, please try again', - style: TextStyle(color: Colors.white), + AppLocalizations.of( + context, + ).error_creating_group, + style: const TextStyle(color: Colors.white), ), ), ), diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index b2243bc..3505a3c 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -4,6 +4,7 @@ import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; @@ -34,12 +35,14 @@ class _GroupsViewState extends State { @override void initState() { super.initState(); + db = Provider.of(context, listen: false); loadGroups(); } @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, body: Stack( @@ -49,11 +52,11 @@ class _GroupsViewState extends State { enabled: isLoading, child: Visibility( visible: groups.isNotEmpty, - replacement: const Center( + replacement: Center( child: TopCenteredMessage( icon: Icons.info, - title: 'Info', - message: 'No groups created yet', + title: loc.info, + message: loc.no_groups_created_yet, ), ), child: ListView.builder( @@ -73,7 +76,7 @@ class _GroupsViewState extends State { Positioned( bottom: MediaQuery.paddingOf(context).bottom, child: CustomWidthButton( - text: 'Create Group', + text: loc.create_group, sizeRelativeToWidth: 0.90, onPressed: () async { await Navigator.push( diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 8f911cd..96280ce 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -4,6 +4,7 @@ import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; @@ -71,6 +72,7 @@ class _HomeViewState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return AppSkeleton( @@ -86,7 +88,7 @@ class _HomeViewState extends State { QuickInfoTile( width: constraints.maxWidth * 0.45, height: constraints.maxHeight * 0.15, - title: 'Matches', + title: loc.matches, icon: Icons.groups_rounded, value: matchCount, ), @@ -94,7 +96,7 @@ class _HomeViewState extends State { QuickInfoTile( width: constraints.maxWidth * 0.45, height: constraints.maxHeight * 0.15, - title: 'Groups', + title: loc.groups, icon: Icons.groups_rounded, value: groupCount, ), @@ -104,15 +106,19 @@ class _HomeViewState extends State { padding: const EdgeInsets.symmetric(vertical: 16.0), child: InfoTile( width: constraints.maxWidth * 0.95, - title: 'Recent Matches', + title: loc.recent_matches, icon: Icons.timer, content: Padding( padding: const EdgeInsets.symmetric(horizontal: 40.0), child: Visibility( visible: !isLoading && loadedRecentMatches.isNotEmpty, - replacement: const Center( + replacement: Center( heightFactor: 12, - child: Text('No recent matches available'), + child: Text( + AppLocalizations.of( + context, + ).no_recent_matches_available, + ), ), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -122,9 +128,14 @@ class _HomeViewState extends State { matchTitle: recentMatches[0].name, game: 'Winner', ruleset: 'Ruleset', - players: _getPlayerText(recentMatches[0]), + players: _getPlayerText( + recentMatches[0], + context, + ), winner: recentMatches[0].winner == null - ? 'Match in progress...' + ? AppLocalizations.of( + context, + ).match_in_progress : recentMatches[0].winner!.name, ), const Padding( @@ -136,16 +147,25 @@ class _HomeViewState extends State { matchTitle: recentMatches[1].name, game: 'Winner', ruleset: 'Ruleset', - players: _getPlayerText(recentMatches[1]), + players: _getPlayerText( + recentMatches[1], + context, + ), winner: recentMatches[1].winner == null - ? 'Match in progress...' + ? AppLocalizations.of( + context, + ).match_in_progress : recentMatches[1].winner!.name, ), const SizedBox(height: 8), ] else ...[ - const Center( + Center( heightFactor: 5.35, - child: Text('No second match available'), + child: Text( + AppLocalizations.of( + context, + ).no_second_match_available, + ), ), ], ], @@ -156,7 +176,7 @@ class _HomeViewState extends State { ), InfoTile( width: constraints.maxWidth * 0.95, - title: 'Quick Create', + title: loc.quick_create, icon: Icons.add_box_rounded, content: Column( children: [ @@ -210,10 +230,11 @@ class _HomeViewState extends State { ); } - String _getPlayerText(Match game) { + String _getPlayerText(Match game, context) { + final loc = AppLocalizations.of(context); if (game.group == null) { final playerCount = game.players?.length ?? 0; - return '$playerCount Players'; + return loc.players_count(playerCount); } if (game.players == null || game.players!.isEmpty) { return game.group!.name; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index e21e868..18e1e9d 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; @@ -20,6 +21,7 @@ class ChooseGameView extends StatefulWidget { class _ChooseGameViewState extends State { late int selectedGameIndex; + final TextEditingController searchBarController = TextEditingController(); @override @@ -30,6 +32,7 @@ class _ChooseGameViewState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( @@ -41,9 +44,9 @@ class _ChooseGameViewState extends State { Navigator.of(context).pop(selectedGameIndex); }, ), - title: const Text( - 'Choose Game', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + title: Text( + loc.choose_game, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), centerTitle: true, ), @@ -63,7 +66,7 @@ class _ChooseGameViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 10), child: CustomSearchBar( controller: searchBarController, - hintText: 'Game Name', + hintText: loc.game_name, ), ), const SizedBox(height: 5), @@ -74,7 +77,10 @@ class _ChooseGameViewState extends State { return TitleDescriptionListTile( title: widget.games[index].$1, description: widget.games[index].$2, - badgeText: translateRulesetToString(widget.games[index].$3), + badgeText: translateRulesetToString( + widget.games[index].$3, + context, + ), isHighlighted: selectedGameIndex == index, onPressed: () async { setState(() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index 92b66ad..5101db6 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; @@ -22,7 +23,6 @@ class ChooseGroupView extends StatefulWidget { class _ChooseGroupViewState extends State { late String selectedGroupId; final TextEditingController controller = TextEditingController(); - final String hintText = 'Group Name'; late final List filteredGroups; @override @@ -34,6 +34,7 @@ class _ChooseGroupViewState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( @@ -51,9 +52,9 @@ class _ChooseGroupViewState extends State { ); }, ), - title: const Text( - 'Choose Group', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + title: Text( + loc.choose_group, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), centerTitle: true, ), @@ -79,7 +80,7 @@ class _ChooseGroupViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 10), child: CustomSearchBar( controller: controller, - hintText: hintText, + hintText: loc.search_for_groups, onChanged: (value) { setState(() { filterGroups(value); @@ -92,15 +93,17 @@ class _ChooseGroupViewState extends State { visible: filteredGroups.isNotEmpty, replacement: Visibility( visible: widget.groups.isNotEmpty, - replacement: const TopCenteredMessage( + replacement: TopCenteredMessage( icon: Icons.info, - title: 'Info', - message: 'You have no groups created yet', + title: loc.info, + message: loc.no_groups_created_yet, ), - child: const TopCenteredMessage( + child: TopCenteredMessage( icon: Icons.info, - title: 'Info', - message: 'There is no group matching your search', + title: loc.info, + message: AppLocalizations.of( + context, + ).there_is_no_group_matching_your_search, ), ), child: ListView.builder( diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart index 479106c..7a41417 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; class ChooseRulesetView extends StatefulWidget { @@ -28,6 +29,7 @@ class _ChooseRulesetViewState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return DefaultTabController( length: 2, initialIndex: 0, @@ -46,9 +48,9 @@ class _ChooseRulesetViewState extends State { ); }, ), - title: const Text( - 'Choose Ruleset', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + title: Text( + loc.choose_ruleset, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), centerTitle: true, ), @@ -80,7 +82,10 @@ class _ChooseRulesetViewState extends State { } }); }, - title: translateRulesetToString(widget.rulesets[index].$1), + title: translateRulesetToString( + widget.rulesets[index].$1, + context, + ), description: widget.rulesets[index].$2, isHighlighted: selectedRulesetIndex == index, ); diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 9923d7a..d3a23ae 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -6,6 +6,7 @@ import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart'; @@ -67,34 +68,6 @@ class _CreateMatchViewState extends State { /// The currently selected players List? selectedPlayers; - /// List of available rulesets with their descriptions - /// as tuples of (Ruleset, String) - /// TODO: Replace when rulesets are implemented - List<(Ruleset, String)> rulesets = [ - ( - Ruleset.singleWinner, - 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.', - ), - ( - Ruleset.singleLoser, - 'Exactly one loser is determined; last place receives the penalty or consequence.', - ), - ( - Ruleset.mostPoints, - 'Traditional ruleset: the player with the most points wins.', - ), - ( - Ruleset.leastPoints, - 'Inverse scoring: the player with the fewest points wins.', - ), - ]; - - // TODO: Replace when games are implemented - List<(String, String, Ruleset)> games = [ - ('Example Game 1', 'This is a discription', Ruleset.leastPoints), - ('Example Game 2', '', Ruleset.singleWinner), - ]; - @override void initState() { super.initState(); @@ -116,16 +89,33 @@ class _CreateMatchViewState extends State { }); } + List<(Ruleset, String)> _getRulesets(BuildContext context) { + final loc = AppLocalizations.of(context); + return [ + (Ruleset.singleWinner, loc.ruleset_single_winner), + (Ruleset.singleLoser, loc.ruleset_single_loser), + (Ruleset.mostPoints, loc.ruleset_most_points), + (Ruleset.leastPoints, loc.ruleset_least_points), + ]; + } + + // TODO: Replace when games are implemented + List<(String, String, Ruleset)> games = [ + ('Example Game 1', 'This is a description', Ruleset.leastPoints), + ('Example Game 2', '', Ruleset.singleWinner), + ]; + @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, - title: const Text( - 'Create new match', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + title: Text( + loc.create_new_match, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), centerTitle: true, ), @@ -141,9 +131,9 @@ class _CreateMatchViewState extends State { ), ), ChooseTile( - title: 'Game', + title: loc.game, trailingText: selectedGameIndex == -1 - ? 'None' + ? loc.none : games[selectedGameIndex].$1, onPressed: () async { selectedGameIndex = await Navigator.of(context).push( @@ -158,9 +148,9 @@ class _CreateMatchViewState extends State { if (selectedGameIndex != -1) { hintText = games[selectedGameIndex].$1; selectedRuleset = games[selectedGameIndex].$3; - selectedRulesetIndex = rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); + selectedRulesetIndex = _getRulesets( + context, + ).indexWhere((r) => r.$1 == selectedRuleset); } else { hintText = 'Match Name'; selectedRuleset = null; @@ -169,11 +159,12 @@ class _CreateMatchViewState extends State { }, ), ChooseTile( - title: 'Ruleset', + title: loc.ruleset, trailingText: selectedRuleset == null - ? 'None' - : translateRulesetToString(selectedRuleset!), + ? loc.none + : translateRulesetToString(selectedRuleset!, context), onPressed: () async { + final rulesets = _getRulesets(context); selectedRuleset = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => ChooseRulesetView( @@ -182,6 +173,7 @@ class _CreateMatchViewState extends State { ), ), ); + if (!mounted) return; selectedRulesetIndex = rulesets.indexWhere( (r) => r.$1 == selectedRuleset, ); @@ -190,9 +182,9 @@ class _CreateMatchViewState extends State { }, ), ChooseTile( - title: 'Group', + title: loc.group, trailingText: selectedGroup == null - ? 'None' + ? loc.none_group : selectedGroup!.name, onPressed: () async { selectedGroup = await Navigator.of(context).push( @@ -229,7 +221,7 @@ class _CreateMatchViewState extends State { ), ), CustomWidthButton( - text: 'Create match', + text: loc.create_match, sizeRelativeToWidth: 0.95, buttonType: ButtonType.primary, onPressed: _enableCreateGameButton() diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index c2076cc..5c455f6 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -3,6 +3,7 @@ import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:provider/provider.dart'; @@ -35,6 +36,7 @@ class _MatchResultViewState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( @@ -79,9 +81,9 @@ class _MatchResultViewState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Select Winner:', - style: TextStyle( + Text( + loc.select_winner, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 5b12e11..73f596f 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -8,6 +8,7 @@ import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; @@ -43,12 +44,14 @@ class _MatchViewState extends State { @override void initState() { super.initState(); + db = Provider.of(context, listen: false); loadGames(); } @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, body: Stack( @@ -58,11 +61,11 @@ class _MatchViewState extends State { enabled: isLoading, child: Visibility( visible: matches.isNotEmpty, - replacement: const Center( + replacement: Center( child: TopCenteredMessage( icon: Icons.report, - title: 'Info', - message: 'No games created yet', + title: loc.info, + message: loc.no_matches_created_yet, ), ), child: ListView.builder( @@ -96,7 +99,7 @@ class _MatchViewState extends State { Positioned( bottom: MediaQuery.paddingOf(context).bottom, child: CustomWidthButton( - text: 'Create Match', + text: loc.create_match, sizeRelativeToWidth: 0.90, onPressed: () async { Navigator.push( diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index 6ebb7fb..8f1e68a 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:game_tracker/services/data_transfer_service.dart'; @@ -12,8 +13,14 @@ class SettingsView extends StatefulWidget { } class _SettingsViewState extends State { + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Scaffold( appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), backgroundColor: CustomTheme.backgroundColor, @@ -24,30 +31,33 @@ class _SettingsViewState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.fromLTRB(24, 0, 24, 10), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), child: Text( textAlign: TextAlign.start, - 'Menu', - style: TextStyle( + loc.menu, + style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 10, + ), child: Text( textAlign: TextAlign.start, - 'Settings', - style: TextStyle( + loc.settings, + style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, ), ), ), SettingsListTile( - title: 'Export data', + title: loc.export_data, icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), onPressed: () async { @@ -62,7 +72,7 @@ class _SettingsViewState extends State { }, ), SettingsListTile( - title: 'Import data', + title: loc.import_data, icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), onPressed: () async { @@ -74,23 +84,23 @@ class _SettingsViewState extends State { }, ), SettingsListTile( - title: 'Delete all data', + title: loc.delete_all_data, icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Delete all data?'), - content: const Text('This can\'t be undone'), + title: Text(loc.delete_all_data), + content: Text(loc.this_cannot_be_undone), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Abbrechen'), + child: Text(loc.cancel), ), TextButton( onPressed: () => Navigator.of(context).pop(true), - child: const Text('Löschen'), + child: Text(loc.delete), ), ], ), @@ -99,7 +109,9 @@ class _SettingsViewState extends State { DataTransferService.deleteAllData(context); showSnackbar( context: context, - message: 'Daten erfolgreich gelöscht', + message: AppLocalizations.of( + context, + ).data_successfully_deleted, ); } }); @@ -120,25 +132,20 @@ class _SettingsViewState extends State { required BuildContext context, required ImportResult result, }) { + final loc = AppLocalizations.of(context); switch (result) { case ImportResult.success: - showSnackbar(context: context, message: 'Data successfully imported'); + showSnackbar(context: context, message: loc.data_successfully_imported); case ImportResult.invalidSchema: - showSnackbar(context: context, message: 'Invalid Schema'); + showSnackbar(context: context, message: loc.invalid_schema); case ImportResult.fileReadError: - showSnackbar(context: context, message: 'Error reading file'); + showSnackbar(context: context, message: loc.error_reading_file); case ImportResult.canceled: - showSnackbar(context: context, message: 'Import canceled'); + showSnackbar(context: context, message: loc.import_canceled); case ImportResult.formatException: - showSnackbar( - context: context, - message: 'Format Exception (see console)', - ); + showSnackbar(context: context, message: loc.format_exception); case ImportResult.unknownException: - showSnackbar( - context: context, - message: 'Unknown Exception (see console)', - ); + showSnackbar(context: context, message: loc.unknown_exception); } } @@ -150,16 +157,14 @@ class _SettingsViewState extends State { required BuildContext context, required ExportResult result, }) { + final loc = AppLocalizations.of(context); switch (result) { case ExportResult.success: - showSnackbar(context: context, message: 'Data successfully exported'); + showSnackbar(context: context, message: loc.data_successfully_exported); case ExportResult.canceled: - showSnackbar(context: context, message: 'Export canceled'); + showSnackbar(context: context, message: loc.export_canceled); case ExportResult.unknownException: - showSnackbar( - context: context, - message: 'Unknown Exception (see console)', - ); + showSnackbar(context: context, message: loc.unknown_exception); } } @@ -175,6 +180,7 @@ class _SettingsViewState extends State { Duration duration = const Duration(seconds: 3), VoidCallback? action, }) { + final loc = AppLocalizations.of(context); final messenger = ScaffoldMessenger.of(context); messenger.hideCurrentSnackBar(); messenger.showSnackBar( @@ -183,7 +189,7 @@ class _SettingsViewState extends State { backgroundColor: CustomTheme.onBoxColor, duration: duration, action: action != null - ? SnackBarAction(label: 'Rückgängig', onPressed: action) + ? SnackBarAction(label: loc.undo, onPressed: action) : null, ), ); diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 0e95721..6c30483 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -3,6 +3,7 @@ import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; @@ -24,6 +25,7 @@ class _StatisticsViewState extends State { @override void initState() { super.initState(); + final db = Provider.of(context, listen: false); Future.wait([ @@ -31,21 +33,25 @@ class _StatisticsViewState extends State { db.playerDao.getAllPlayers(), Future.delayed(minimumSkeletonDuration), ]).then((results) async { + if (!mounted) return; final matches = results[0] as List; final players = results[1] as List; - winCounts = _calculateWinsForAllPlayers(matches, players); - matchCounts = _calculateMatchAmountsForAllPlayers(matches, players); + winCounts = _calculateWinsForAllPlayers(matches, players, context); + matchCounts = _calculateMatchAmountsForAllPlayers( + matches, + players, + context, + ); winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts); - if (mounted) { - setState(() { - isLoading = false; - }); - } + setState(() { + isLoading = false; + }); }); } @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return SingleChildScrollView( @@ -68,7 +74,7 @@ class _StatisticsViewState extends State { children: [ StatisticsTile( icon: Icons.sports_score, - title: 'Wins', + title: loc.wins, width: constraints.maxWidth * 0.95, values: winCounts, itemCount: 3, @@ -77,7 +83,7 @@ class _StatisticsViewState extends State { SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( icon: Icons.percent, - title: 'Winrate', + title: loc.winrate, width: constraints.maxWidth * 0.95, values: winRates, itemCount: 5, @@ -86,7 +92,7 @@ class _StatisticsViewState extends State { SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( icon: Icons.casino, - title: 'Amount of Matches', + title: loc.amount_of_matches, width: constraints.maxWidth * 0.95, values: matchCounts, itemCount: 10, @@ -94,10 +100,12 @@ class _StatisticsViewState extends State { ), ], ), - child: const TopCenteredMessage( + child: TopCenteredMessage( icon: Icons.info, - title: 'Info', - message: 'No statistics available', + title: loc.info, + message: AppLocalizations.of( + context, + ).no_statistics_available, ), ), SizedBox(height: MediaQuery.paddingOf(context).bottom), @@ -115,8 +123,10 @@ class _StatisticsViewState extends State { List<(String, int)> _calculateWinsForAllPlayers( List matches, List players, + BuildContext context, ) { List<(String, int)> winCounts = []; + final loc = AppLocalizations.of(context); // Getting the winners for (var match in matches) { @@ -147,7 +157,7 @@ class _StatisticsViewState extends State { final playerId = winCounts[i].$1; final player = players.firstWhere( (p) => p.id == playerId, - orElse: () => Player(id: playerId, name: 'N.a.'), + orElse: () => Player(id: playerId, name: loc.not_available), ); winCounts[i] = (player.name, winCounts[i].$2); } @@ -162,8 +172,10 @@ class _StatisticsViewState extends State { List<(String, int)> _calculateMatchAmountsForAllPlayers( List matches, List players, + BuildContext context, ) { List<(String, int)> matchCounts = []; + final loc = AppLocalizations.of(context); // Counting matches for each player for (var match in matches) { @@ -209,7 +221,7 @@ class _StatisticsViewState extends State { final playerId = matchCounts[i].$1; final player = players.firstWhere( (p) => p.id == playerId, - orElse: () => Player(id: playerId, name: 'N.a.'), + orElse: () => Player(id: playerId, name: loc.not_available), ); matchCounts[i] = (player.name, matchCounts[i].$2); } diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 6ca6373..eac4480 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -3,6 +3,7 @@ import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart'; @@ -86,6 +87,7 @@ class _PlayerSelectionState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); return Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), @@ -96,7 +98,7 @@ class _PlayerSelectionState extends State { CustomSearchBar( controller: _searchBarController, constraints: const BoxConstraints(maxHeight: 45, minHeight: 45), - hintText: 'Search for players', + hintText: loc.search_for_players, trailingButtonShown: true, trailingButtonicon: Icons.add_circle, trailingButtonEnabled: _searchBarController.text.trim().isNotEmpty, @@ -129,14 +131,16 @@ class _PlayerSelectionState extends State { ), const SizedBox(height: 10), Text( - 'Selected players: (${selectedPlayers.length})', + AppLocalizations.of( + context, + ).selected_players(selectedPlayers.length), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), SizedBox( height: 50, child: selectedPlayers.isEmpty - ? const Center(child: Text('No players selected')) + ? Center(child: Text(loc.no_players_selected)) : SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -177,9 +181,9 @@ class _PlayerSelectionState extends State { ), ), const SizedBox(height: 10), - const Text( - 'All players:', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + Text( + loc.all_players, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Expanded( @@ -189,8 +193,8 @@ class _PlayerSelectionState extends State { visible: suggestedPlayers.isNotEmpty, replacement: TopCenteredMessage( icon: Icons.info, - title: 'Info', - message: _getInfoText(), + title: loc.info, + message: _getInfoText(context), ), child: ListView.builder( itemCount: suggestedPlayers.length, @@ -227,6 +231,7 @@ class _PlayerSelectionState extends State { /// Shows a snackbar indicating success or failure. /// [context] - BuildContext to show the snackbar. void addNewPlayerFromSearch({required BuildContext context}) async { + final loc = AppLocalizations.of(context); String playerName = _searchBarController.text.trim(); Player createdPlayer = Player(name: playerName); bool success = await db.playerDao.addPlayer(player: createdPlayer); @@ -246,7 +251,9 @@ class _PlayerSelectionState extends State { backgroundColor: CustomTheme.boxColor, content: Center( child: Text( - 'Successfully added player $playerName.', + AppLocalizations.of( + context, + ).successfully_added_player(playerName), style: const TextStyle(color: Colors.white), ), ), @@ -258,7 +265,7 @@ class _PlayerSelectionState extends State { backgroundColor: CustomTheme.boxColor, content: Center( child: Text( - 'Could not add player $playerName.', + loc.could_not_add_player(playerName), style: const TextStyle(color: Colors.white), ), ), @@ -269,18 +276,19 @@ class _PlayerSelectionState extends State { /// Determines the appropriate info text to display when no players /// are available in the suggested players list. - String _getInfoText() { + String _getInfoText(BuildContext context) { + final loc = AppLocalizations.of(context); if (allPlayers.isEmpty) { // No players exist in the database - return 'No players created yet'; + return loc.no_players_created_yet; } else if (selectedPlayers.length == allPlayers.length || widget.availablePlayers?.isEmpty == true) { // All players have been selected or // available players list is provided but empty - return 'No more players to add'; + return loc.all_players_selected; } else { // No players match the search query - return 'No players found with that name'; + return loc.no_players_found_with_that_name; } } } diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index bfe1b9f..c455949 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:intl/intl.dart'; @@ -20,6 +21,7 @@ class _MatchTileState extends State { final group = widget.match.group; final winner = widget.match.winner; final allPlayers = _getAllPlayers(); + final loc = AppLocalizations.of(context); return GestureDetector( onTap: widget.onTap, @@ -48,7 +50,7 @@ class _MatchTileState extends State { ), ), Text( - _formatDate(widget.match.createdAt), + _formatDate(widget.match.createdAt, context), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], @@ -97,7 +99,7 @@ class _MatchTileState extends State { const SizedBox(width: 8), Expanded( child: Text( - 'Winner: ${winner.name}', + '${loc.winner}: ${winner.name}', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -113,9 +115,9 @@ class _MatchTileState extends State { ], if (allPlayers.isNotEmpty) ...[ - const Text( - 'Players', - style: TextStyle( + Text( + loc.players, + style: const TextStyle( fontSize: 13, color: Colors.grey, fontWeight: FontWeight.w500, @@ -136,16 +138,21 @@ class _MatchTileState extends State { ); } - String _formatDate(DateTime dateTime) { + String _formatDate(DateTime dateTime, BuildContext context) { final now = DateTime.now(); final difference = now.difference(dateTime); + final loc = AppLocalizations.of(context); if (difference.inDays == 0) { - return 'Today at ${DateFormat('HH:mm').format(dateTime)}'; + return AppLocalizations.of( + context, + ).today_at(DateFormat('HH:mm').format(dateTime)); } else if (difference.inDays == 1) { - return 'Yesterday at ${DateFormat('HH:mm').format(dateTime)}'; + return AppLocalizations.of( + context, + ).yesterday_at(DateFormat('HH:mm').format(dateTime)); } else if (difference.inDays < 7) { - return '${difference.inDays} days ago'; + return loc.days_ago(difference.inDays); } else { return DateFormat('MMM d, yyyy').format(dateTime); } diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 582cf66..598fad0 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; class StatisticsTile extends StatelessWidget { @@ -24,6 +25,7 @@ class StatisticsTile extends StatelessWidget { @override Widget build(BuildContext context) { final maxBarWidth = MediaQuery.of(context).size.width * 0.65; + final loc = AppLocalizations.of(context); return InfoTile( width: width, @@ -33,9 +35,9 @@ class StatisticsTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Visibility( visible: values.isNotEmpty, - replacement: const Center( + replacement: Center( heightFactor: 4, - child: Text('No data available.'), + child: Text(loc.no_data_available), ), child: Column( children: List.generate(min(values.length, itemCount), (index) { diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 7a138a0..465c94d 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -54,7 +54,7 @@ class TitleDescriptionListTile extends StatelessWidget { if (badgeText != null) ...[ const Spacer(), Container( - constraints: const BoxConstraints(maxWidth: 100), + constraints: const BoxConstraints(maxWidth: 115), margin: const EdgeInsets.only(top: 4), padding: const EdgeInsets.symmetric( vertical: 2, diff --git a/pubspec.yaml b/pubspec.yaml index 07e4df2..e79ca17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,9 @@ dependencies: json_schema: ^5.2.2 file_saver: ^0.3.1 clock: ^1.1.2 - intl: ^0.18.0 + intl: any + flutter_localizations: + sdk: flutter dev_dependencies: flutter_test: @@ -35,5 +37,6 @@ dev_dependencies: flutter: uses-material-design: true + generate: true assets: - assets/schema.json