Lokalisierung implementieren #112

Merged
sneeex merged 41 commits from feature/100-lokalisierung-hinzufügen into development 2026-01-07 11:30:11 +00:00
26 changed files with 1927 additions and 194 deletions

1
.gitignore vendored
View File

@@ -195,3 +195,4 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/devtools_options.yaml

5
l10n.yaml Normal file
View File

@@ -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

View File

@@ -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;
}
}

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

@@ -0,0 +1,83 @@
{
sneeex marked this conversation as resolved
Review

Datei alphabetisch sortieren

Datei alphabetisch sortieren
"@@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",
sneeex marked this conversation as resolved Outdated

Gendern? (auch generell)

Gendern? (auch generell)
"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}"
}

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

@@ -0,0 +1,367 @@
{
sneeex marked this conversation as resolved
Review

Datei alphabetisch sortierne

Datei alphabetisch sortierne
"@@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"
sneeex marked this conversation as resolved Outdated

Würde : und die Variable wegnehmen. Wäre die Variable innerhalb des Strings, dann sinnvoll, ansonsten nicht und einfach an der jeweiligen Stelle mit interpolation machen

Würde `:` und die Variable wegnehmen. Wäre die Variable innerhalb des Strings, dann sinnvoll, ansonsten nicht und einfach an der jeweiligen Stelle mit interpolation machen
},
"@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}"
}

View File

@@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['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.',
);
}

View File

@@ -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';
}

View File

@@ -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';
}

View File

@@ -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(
sneeex marked this conversation as resolved Outdated

Debug-Print entfernen

Debug-Print entfernen
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(),
sneeex marked this conversation as resolved Outdated

Lokalisierung fehlt

Lokalisierung fehlt
themeMode: ThemeMode.dark, // forces dark mode

View File

@@ -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<CustomNavigationBar>
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<Widget> tabs = [
KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()),
@@ -46,7 +43,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
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<CustomNavigationBar>
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<CustomNavigationBar>
});
}
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 '';
}

View File

@@ -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<CreateGroupView> {
final _groupNameController = TextEditingController();
late final AppDatabase db;
List<Player> selectedPlayers = [];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_groupNameController.addListener(() {
setState(() {});
@@ -38,14 +41,15 @@ class _CreateGroupViewState extends State<CreateGroupView> {
@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<CreateGroupView> {
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<CreateGroupView> {
),
),
CustomWidthButton(
text: 'Create group',
text: loc.create_group,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed:
@@ -94,10 +98,12 @@ class _CreateGroupViewState extends State<CreateGroupView> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: const Center(
content: Center(
child: Text(
sneeex marked this conversation as resolved Outdated

Abkürzen für etwas generelles, z.B. creating_group_error oder creating_group_error_message

Abkürzen für etwas generelles, z.B. `creating_group_error` oder `creating_group_error_message`
'Error while creating group, please try again',
style: TextStyle(color: Colors.white),
AppLocalizations.of(
context,
).error_creating_group,
style: const TextStyle(color: Colors.white),
),
),
),

View File

@@ -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<GroupsView> {
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(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<GroupsView> {
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<GroupsView> {
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Group',
text: loc.create_group,
sizeRelativeToWidth: 0.90,
onPressed: () async {
await Navigator.push(

View File

@@ -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<HomeView> {
@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<HomeView> {
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<HomeView> {
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<HomeView> {
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,
flixcoo marked this conversation as resolved Outdated

Idee: Generell alle Info-Messages auch als solche bennenen z.B. info_no_recent_matches

Idee: Generell alle Info-Messages auch als solche bennenen z.B. `info_no_recent_matches`

finde ich eigentlich unnötig

finde ich eigentlich unnötig

weil eigentlich kann man die messages ja auch anders benutzen und es ist ja eigentlich egal obs ne info ist

weil eigentlich kann man die messages ja auch anders benutzen und es ist ja eigentlich egal obs ne info ist
).no_recent_matches_available,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
@@ -122,9 +128,14 @@ class _HomeViewState extends State<HomeView> {
matchTitle: recentMatches[0].name,
game: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(recentMatches[0]),
players: _getPlayerText(
sneeex marked this conversation as resolved Outdated

Wenn winner_labelund ruleset_label nirgends anders verwendet werden, einfach die normalen Strings belassen, weil diese ja in Zukunft durch das Ruleset und das Game ersetzt werden

Wenn `winner_label`und `ruleset_label` nirgends anders verwendet werden, einfach die normalen Strings belassen, weil diese ja in Zukunft durch das Ruleset und das Game ersetzt werden
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<HomeView> {
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<HomeView> {
),
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<HomeView> {
);
}
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;

View File

@@ -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<ChooseGameView> {
late int selectedGameIndex;
final TextEditingController searchBarController = TextEditingController();
@override
@@ -30,6 +32,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
@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<ChooseGameView> {
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<ChooseGameView> {
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<ChooseGameView> {
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(() {

View File

@@ -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<ChooseGroupView> {
late String selectedGroupId;
final TextEditingController controller = TextEditingController();
final String hintText = 'Group Name';
late final List<Group> filteredGroups;
@override
@@ -34,6 +34,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
@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<ChooseGroupView> {
);
},
),
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<ChooseGroupView> {
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<ChooseGroupView> {
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(

View File

@@ -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<ChooseRulesetView> {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return DefaultTabController(
length: 2,
initialIndex: 0,
@@ -46,9 +48,9 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
);
},
),
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<ChooseRulesetView> {
}
});
},
title: translateRulesetToString(widget.rulesets[index].$1),
title: translateRulesetToString(
widget.rulesets[index].$1,
context,
),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);

View File

@@ -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<CreateMatchView> {
/// The currently selected players
List<Player>? 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<CreateMatchView> {
});
}
List<(Ruleset, String)> _getRulesets(BuildContext context) {
final loc = AppLocalizations.of(context);
return [
(Ruleset.singleWinner, loc.ruleset_single_winner),
(Ruleset.singleLoser, loc.ruleset_single_loser),
sneeex marked this conversation as resolved Outdated

Warum _desc?

Warum `_desc`?
(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<CreateMatchView> {
),
),
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<CreateMatchView> {
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<CreateMatchView> {
},
),
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<CreateMatchView> {
),
),
);
if (!mounted) return;
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
@@ -190,9 +182,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
},
),
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<CreateMatchView> {
),
),
CustomWidthButton(
text: 'Create match',
text: loc.create_match,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton()

View File

@@ -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<MatchResultView> {
@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<MatchResultView> {
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,
),

View File

@@ -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<MatchView> {
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(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<MatchView> {
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<MatchView> {
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton(
text: 'Create Match',
text: loc.create_match,
sizeRelativeToWidth: 0.90,
onPressed: () async {
Navigator.push(

View File

@@ -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<SettingsView> {
@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<SettingsView> {
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<SettingsView> {
},
),
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<SettingsView> {
},
),
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<bool>(
context: context,
flixcoo marked this conversation as resolved Outdated

Hier auch sowas wie popup oder so mit einbauen, dass klar wird, wozu dieser text ist

Hier auch sowas wie `popup` oder so mit einbauen, dass klar wird, wozu dieser text ist

finde nicht, das kann man für verschiedene sachen nutzen und reicht so m. M. n.

finde nicht, das kann man für verschiedene sachen nutzen und reicht so m. M. n.
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<SettingsView> {
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<SettingsView> {
required BuildContext context,
required ImportResult result,
}) {
final loc = AppLocalizations.of(context);
switch (result) {
case ImportResult.success:
flixcoo marked this conversation as resolved Outdated

hier sowas wie snackbar?

hier sowas wie `snackbar`?

auch nicht relevant m. M. n.

auch nicht relevant m. M. n.
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<SettingsView> {
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<SettingsView> {
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<SettingsView> {
backgroundColor: CustomTheme.onBoxColor,
duration: duration,
action: action != null
? SnackBarAction(label: 'Rückgängig', onPressed: action)
? SnackBarAction(label: loc.undo, onPressed: action)
: null,
),
);

View File

@@ -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<StatisticsView> {
@override
void initState() {
super.initState();
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
@@ -31,21 +33,25 @@ class _StatisticsViewState extends State<StatisticsView> {
db.playerDao.getAllPlayers(),
Future.delayed(minimumSkeletonDuration),
]).then((results) async {
if (!mounted) return;
final matches = results[0] as List<Match>;
final players = results[1] as List<Player>;
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<StatisticsView> {
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<StatisticsView> {
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<StatisticsView> {
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<StatisticsView> {
),
],
),
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<StatisticsView> {
List<(String, int)> _calculateWinsForAllPlayers(
List<Match> matches,
List<Player> 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<StatisticsView> {
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<StatisticsView> {
List<(String, int)> _calculateMatchAmountsForAllPlayers(
List<Match> matches,
List<Player> 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<StatisticsView> {
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);
}

View File

@@ -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<PlayerSelection> {
@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<PlayerSelection> {
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<PlayerSelection> {
),
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<PlayerSelection> {
),
),
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<PlayerSelection> {
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<PlayerSelection> {
/// 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<PlayerSelection> {
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<PlayerSelection> {
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<PlayerSelection> {
/// 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;
}
}
}

View File

@@ -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<MatchTile> {
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<MatchTile> {
),
),
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<MatchTile> {
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<MatchTile> {
],
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<MatchTile> {
);
}
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);
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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