From a2a7ffd79f2a034924b758f40d79854e30be872d Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Tue, 23 Jun 2026 12:13:51 +0200 Subject: [PATCH] feat(i18n): add FR/EN internationalization with in-app language selector - gen-l10n setup (flutter_localizations, intl, l10n.yaml, ARB en/fr) - localeProvider (persisted) wired into MaterialApp - language selector in the System page (alongside the color palette) - all user-facing strings across screens moved to AppLocalizations Co-Authored-By: Claude Opus 4.8 --- l10n.yaml | 4 + lib/core/config/app_constants.dart | 3 + lib/l10n/app_en.arb | 57 +++ lib/l10n/app_fr.arb | 50 +++ lib/l10n/app_localizations.dart | 385 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 151 +++++++ lib/l10n/app_localizations_fr.dart | 151 +++++++ lib/main.dart | 8 +- lib/presentation/pages/game_over_page.dart | 7 +- lib/presentation/pages/guess_page.dart | 13 +- lib/presentation/pages/pokemon_list.dart | 24 +- lib/presentation/pages/system_page.dart | 30 +- .../providers/locale_provider.dart | 33 ++ .../widgets/detail/pokemon_stats_panel.dart | 6 +- .../widgets/game_over/game_over_actions.dart | 6 +- .../widgets/game_over/game_over_header.dart | 10 +- .../widgets/game_over/game_over_stats.dart | 8 +- .../widgets/guess/gen_filter_section.dart | 7 +- .../widgets/guess/guess_input_section.dart | 24 +- .../widgets/guess/guess_silhouette.dart | 4 +- .../widgets/guess/score_board.dart | 10 +- .../widgets/list/pokedex_count_bar.dart | 5 +- .../widgets/list/pokedex_list_header.dart | 11 +- .../widgets/system/language_picker.dart | 60 +++ .../widgets/system/system_header.dart | 9 +- pubspec.yaml | 6 + 26 files changed, 1012 insertions(+), 70 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_fr.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_fr.dart create mode 100644 lib/presentation/providers/locale_provider.dart create mode 100644 lib/presentation/widgets/system/language_picker.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..af4d3c1 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,4 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-dir: lib/l10n diff --git a/lib/core/config/app_constants.dart b/lib/core/config/app_constants.dart index 85fd53d..9b25a67 100644 --- a/lib/core/config/app_constants.dart +++ b/lib/core/config/app_constants.dart @@ -41,6 +41,9 @@ class AppConstants { /// Clé SharedPreferences pour le filtre de générations. static const String prefsGenFilter = 'gen_filter'; + /// Clé SharedPreferences pour la langue de l'application. + static const String prefsLocale = 'locale_code'; + /// Plages d'IDs Pokémon par génération [min, max] (inclusif). static const List<(int, int)> genRanges = [ (1, 151), // Gen 1 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..f79663d --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,57 @@ +{ + "@@locale": "en", + "appTitle": "Pokéguess", + + "whosThatPokemon": "WHO'S THAT POKÉMON?", + "shinyDetected": "✨ SHINY POKÉMON DETECTED! ✨", + "genFilter": "GEN FILTER", + "identificationInput": "IDENTIFICATION INPUT", + "hintPrefix": "HINT", + "enterPokemonName": "Enter Pokémon name...", + "continueButton": "CONTINUE", + "guessButton": "GUESS!", + "hintButton": "HINT ({count})", + "@hintButton": { "placeholders": { "count": { "type": "int" } } }, + "skipButton": "SKIP ({count})", + "@skipButton": { "placeholders": { "count": { "type": "int" } } }, + "currentScore": "CURRENT SCORE", + "personalBest": "PERSONAL BEST: {score}", + "@personalBest": { "placeholders": { "score": { "type": "int" } } }, + "caughtShiny": "✨ SHINY! You caught {name}! (+20 pts) ✨", + "@caughtShiny": { "placeholders": { "name": { "type": "String" } } }, + "caughtNormal": "Correct! You caught {name}!", + "@caughtNormal": { "placeholders": { "name": { "type": "String" } } }, + "wrongGuess": "Wrong guess! Try again.", + "errorLoadingPokemon": "Error loading Pokémon", + + "listNational": "LIST - NATIONAL", + "tabAll": "ALL", + "tabCaught": "CAUGHT", + "pokemonDiscovered": "POKEMON DISCOVERED", + "noPokemonFound": "NO POKEMON FOUND IN {filter}", + "@noPokemonFound": { "placeholders": { "filter": { "type": "String" } } }, + "pokedexFooter": "NATIONAL POKEDEX V2.0", + "loadingError": "Loading error", + + "baseStats": "BASE STATS", + "noDescription": "No description available for this Pokémon.", + + "gameOver": "GAME OVER", + "itWas": "It was {name}!", + "@itWas": { "placeholders": { "name": { "type": "String" } } }, + "gameOverMessage": "\"Looks like your journey ends here. You've run out of energy!\"", + "statStreak": "STREAK", + "statSeen": "SEEN", + "statScore": "SCORE", + "tryAgain": "TRY AGAIN", + "backToPokedex": "BACK TO POKEDEX", + + "system": "SYSTEM", + "statistics": "STATISTICS", + "colorPalette": "COLOR PALETTE", + "language": "LANGUAGE", + "statBestScore": "Best score", + "statCaught": "Caught", + "statSeenLabel": "Seen", + "statCompletion": "Completion" +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 0000000..34277a2 --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,50 @@ +{ + "@@locale": "fr", + "appTitle": "Pokéguess", + + "whosThatPokemon": "QUI EST CE POKÉMON ?", + "shinyDetected": "✨ POKÉMON SHINY DÉTECTÉ ! ✨", + "genFilter": "FILTRE GÉN.", + "identificationInput": "SAISIE D'IDENTIFICATION", + "hintPrefix": "INDICE", + "enterPokemonName": "Entrez le nom du Pokémon...", + "continueButton": "CONTINUER", + "guessButton": "DEVINER !", + "hintButton": "INDICE ({count})", + "skipButton": "PASSER ({count})", + "currentScore": "SCORE ACTUEL", + "personalBest": "MEILLEUR SCORE : {score}", + "caughtShiny": "✨ SHINY ! Tu as attrapé {name} ! (+20 pts) ✨", + "caughtNormal": "Correct ! Tu as attrapé {name} !", + "wrongGuess": "Mauvaise réponse ! Réessaie.", + "errorLoadingPokemon": "Erreur de chargement du Pokémon", + + "listNational": "LISTE - NATIONALE", + "tabAll": "TOUS", + "tabCaught": "ATTRAPÉS", + "pokemonDiscovered": "POKÉMON DÉCOUVERTS", + "noPokemonFound": "AUCUN POKÉMON DANS {filter}", + "pokedexFooter": "POKÉDEX NATIONAL V2.0", + "loadingError": "Erreur de chargement", + + "baseStats": "STATS DE BASE", + "noDescription": "Aucune description disponible pour ce Pokémon.", + + "gameOver": "PARTIE TERMINÉE", + "itWas": "C'était {name} !", + "gameOverMessage": "« On dirait que ton aventure s'arrête ici. Tu n'as plus d'énergie ! »", + "statStreak": "SÉRIE", + "statSeen": "VUS", + "statScore": "SCORE", + "tryAgain": "REJOUER", + "backToPokedex": "RETOUR AU POKÉDEX", + + "system": "SYSTÈME", + "statistics": "STATISTIQUES", + "colorPalette": "PALETTE DE COULEURS", + "language": "LANGUE", + "statBestScore": "Meilleur score", + "statCaught": "Attrapés", + "statSeenLabel": "Vus", + "statCompletion": "Complétion" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..a4ec2e9 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,385 @@ +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_en.dart'; +import 'app_localizations_fr.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 'l10n/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('en'), + Locale('fr') + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'Pokéguess'** + String get appTitle; + + /// No description provided for @whosThatPokemon. + /// + /// In en, this message translates to: + /// **'WHO\'S THAT POKÉMON?'** + String get whosThatPokemon; + + /// No description provided for @shinyDetected. + /// + /// In en, this message translates to: + /// **'✨ SHINY POKÉMON DETECTED! ✨'** + String get shinyDetected; + + /// No description provided for @genFilter. + /// + /// In en, this message translates to: + /// **'GEN FILTER'** + String get genFilter; + + /// No description provided for @identificationInput. + /// + /// In en, this message translates to: + /// **'IDENTIFICATION INPUT'** + String get identificationInput; + + /// No description provided for @hintPrefix. + /// + /// In en, this message translates to: + /// **'HINT'** + String get hintPrefix; + + /// No description provided for @enterPokemonName. + /// + /// In en, this message translates to: + /// **'Enter Pokémon name...'** + String get enterPokemonName; + + /// No description provided for @continueButton. + /// + /// In en, this message translates to: + /// **'CONTINUE'** + String get continueButton; + + /// No description provided for @guessButton. + /// + /// In en, this message translates to: + /// **'GUESS!'** + String get guessButton; + + /// No description provided for @hintButton. + /// + /// In en, this message translates to: + /// **'HINT ({count})'** + String hintButton(int count); + + /// No description provided for @skipButton. + /// + /// In en, this message translates to: + /// **'SKIP ({count})'** + String skipButton(int count); + + /// No description provided for @currentScore. + /// + /// In en, this message translates to: + /// **'CURRENT SCORE'** + String get currentScore; + + /// No description provided for @personalBest. + /// + /// In en, this message translates to: + /// **'PERSONAL BEST: {score}'** + String personalBest(int score); + + /// No description provided for @caughtShiny. + /// + /// In en, this message translates to: + /// **'✨ SHINY! You caught {name}! (+20 pts) ✨'** + String caughtShiny(String name); + + /// No description provided for @caughtNormal. + /// + /// In en, this message translates to: + /// **'Correct! You caught {name}!'** + String caughtNormal(String name); + + /// No description provided for @wrongGuess. + /// + /// In en, this message translates to: + /// **'Wrong guess! Try again.'** + String get wrongGuess; + + /// No description provided for @errorLoadingPokemon. + /// + /// In en, this message translates to: + /// **'Error loading Pokémon'** + String get errorLoadingPokemon; + + /// No description provided for @listNational. + /// + /// In en, this message translates to: + /// **'LIST - NATIONAL'** + String get listNational; + + /// No description provided for @tabAll. + /// + /// In en, this message translates to: + /// **'ALL'** + String get tabAll; + + /// No description provided for @tabCaught. + /// + /// In en, this message translates to: + /// **'CAUGHT'** + String get tabCaught; + + /// No description provided for @pokemonDiscovered. + /// + /// In en, this message translates to: + /// **'POKEMON DISCOVERED'** + String get pokemonDiscovered; + + /// No description provided for @noPokemonFound. + /// + /// In en, this message translates to: + /// **'NO POKEMON FOUND IN {filter}'** + String noPokemonFound(String filter); + + /// No description provided for @pokedexFooter. + /// + /// In en, this message translates to: + /// **'NATIONAL POKEDEX V2.0'** + String get pokedexFooter; + + /// No description provided for @loadingError. + /// + /// In en, this message translates to: + /// **'Loading error'** + String get loadingError; + + /// No description provided for @baseStats. + /// + /// In en, this message translates to: + /// **'BASE STATS'** + String get baseStats; + + /// No description provided for @noDescription. + /// + /// In en, this message translates to: + /// **'No description available for this Pokémon.'** + String get noDescription; + + /// No description provided for @gameOver. + /// + /// In en, this message translates to: + /// **'GAME OVER'** + String get gameOver; + + /// No description provided for @itWas. + /// + /// In en, this message translates to: + /// **'It was {name}!'** + String itWas(String name); + + /// No description provided for @gameOverMessage. + /// + /// In en, this message translates to: + /// **'\"Looks like your journey ends here. You\'ve run out of energy!\"'** + String get gameOverMessage; + + /// No description provided for @statStreak. + /// + /// In en, this message translates to: + /// **'STREAK'** + String get statStreak; + + /// No description provided for @statSeen. + /// + /// In en, this message translates to: + /// **'SEEN'** + String get statSeen; + + /// No description provided for @statScore. + /// + /// In en, this message translates to: + /// **'SCORE'** + String get statScore; + + /// No description provided for @tryAgain. + /// + /// In en, this message translates to: + /// **'TRY AGAIN'** + String get tryAgain; + + /// No description provided for @backToPokedex. + /// + /// In en, this message translates to: + /// **'BACK TO POKEDEX'** + String get backToPokedex; + + /// No description provided for @system. + /// + /// In en, this message translates to: + /// **'SYSTEM'** + String get system; + + /// No description provided for @statistics. + /// + /// In en, this message translates to: + /// **'STATISTICS'** + String get statistics; + + /// No description provided for @colorPalette. + /// + /// In en, this message translates to: + /// **'COLOR PALETTE'** + String get colorPalette; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'LANGUAGE'** + String get language; + + /// No description provided for @statBestScore. + /// + /// In en, this message translates to: + /// **'Best score'** + String get statBestScore; + + /// No description provided for @statCaught. + /// + /// In en, this message translates to: + /// **'Caught'** + String get statCaught; + + /// No description provided for @statSeenLabel. + /// + /// In en, this message translates to: + /// **'Seen'** + String get statSeenLabel; + + /// No description provided for @statCompletion. + /// + /// In en, this message translates to: + /// **'Completion'** + String get statCompletion; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'fr'].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 'en': + return AppLocalizationsEn(); + case 'fr': + return AppLocalizationsFr(); + } + + 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/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..6863505 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,151 @@ +// 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 appTitle => 'Pokéguess'; + + @override + String get whosThatPokemon => 'WHO\'S THAT POKÉMON?'; + + @override + String get shinyDetected => '✨ SHINY POKÉMON DETECTED! ✨'; + + @override + String get genFilter => 'GEN FILTER'; + + @override + String get identificationInput => 'IDENTIFICATION INPUT'; + + @override + String get hintPrefix => 'HINT'; + + @override + String get enterPokemonName => 'Enter Pokémon name...'; + + @override + String get continueButton => 'CONTINUE'; + + @override + String get guessButton => 'GUESS!'; + + @override + String hintButton(int count) { + return 'HINT ($count)'; + } + + @override + String skipButton(int count) { + return 'SKIP ($count)'; + } + + @override + String get currentScore => 'CURRENT SCORE'; + + @override + String personalBest(int score) { + return 'PERSONAL BEST: $score'; + } + + @override + String caughtShiny(String name) { + return '✨ SHINY! You caught $name! (+20 pts) ✨'; + } + + @override + String caughtNormal(String name) { + return 'Correct! You caught $name!'; + } + + @override + String get wrongGuess => 'Wrong guess! Try again.'; + + @override + String get errorLoadingPokemon => 'Error loading Pokémon'; + + @override + String get listNational => 'LIST - NATIONAL'; + + @override + String get tabAll => 'ALL'; + + @override + String get tabCaught => 'CAUGHT'; + + @override + String get pokemonDiscovered => 'POKEMON DISCOVERED'; + + @override + String noPokemonFound(String filter) { + return 'NO POKEMON FOUND IN $filter'; + } + + @override + String get pokedexFooter => 'NATIONAL POKEDEX V2.0'; + + @override + String get loadingError => 'Loading error'; + + @override + String get baseStats => 'BASE STATS'; + + @override + String get noDescription => 'No description available for this Pokémon.'; + + @override + String get gameOver => 'GAME OVER'; + + @override + String itWas(String name) { + return 'It was $name!'; + } + + @override + String get gameOverMessage => + '\"Looks like your journey ends here. You\'ve run out of energy!\"'; + + @override + String get statStreak => 'STREAK'; + + @override + String get statSeen => 'SEEN'; + + @override + String get statScore => 'SCORE'; + + @override + String get tryAgain => 'TRY AGAIN'; + + @override + String get backToPokedex => 'BACK TO POKEDEX'; + + @override + String get system => 'SYSTEM'; + + @override + String get statistics => 'STATISTICS'; + + @override + String get colorPalette => 'COLOR PALETTE'; + + @override + String get language => 'LANGUAGE'; + + @override + String get statBestScore => 'Best score'; + + @override + String get statCaught => 'Caught'; + + @override + String get statSeenLabel => 'Seen'; + + @override + String get statCompletion => 'Completion'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..a82f5ba --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,151 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appTitle => 'Pokéguess'; + + @override + String get whosThatPokemon => 'QUI EST CE POKÉMON ?'; + + @override + String get shinyDetected => '✨ POKÉMON SHINY DÉTECTÉ ! ✨'; + + @override + String get genFilter => 'FILTRE GÉN.'; + + @override + String get identificationInput => 'SAISIE D\'IDENTIFICATION'; + + @override + String get hintPrefix => 'INDICE'; + + @override + String get enterPokemonName => 'Entrez le nom du Pokémon...'; + + @override + String get continueButton => 'CONTINUER'; + + @override + String get guessButton => 'DEVINER !'; + + @override + String hintButton(int count) { + return 'INDICE ($count)'; + } + + @override + String skipButton(int count) { + return 'PASSER ($count)'; + } + + @override + String get currentScore => 'SCORE ACTUEL'; + + @override + String personalBest(int score) { + return 'MEILLEUR SCORE : $score'; + } + + @override + String caughtShiny(String name) { + return '✨ SHINY ! Tu as attrapé $name ! (+20 pts) ✨'; + } + + @override + String caughtNormal(String name) { + return 'Correct ! Tu as attrapé $name !'; + } + + @override + String get wrongGuess => 'Mauvaise réponse ! Réessaie.'; + + @override + String get errorLoadingPokemon => 'Erreur de chargement du Pokémon'; + + @override + String get listNational => 'LISTE - NATIONALE'; + + @override + String get tabAll => 'TOUS'; + + @override + String get tabCaught => 'ATTRAPÉS'; + + @override + String get pokemonDiscovered => 'POKÉMON DÉCOUVERTS'; + + @override + String noPokemonFound(String filter) { + return 'AUCUN POKÉMON DANS $filter'; + } + + @override + String get pokedexFooter => 'POKÉDEX NATIONAL V2.0'; + + @override + String get loadingError => 'Erreur de chargement'; + + @override + String get baseStats => 'STATS DE BASE'; + + @override + String get noDescription => 'Aucune description disponible pour ce Pokémon.'; + + @override + String get gameOver => 'PARTIE TERMINÉE'; + + @override + String itWas(String name) { + return 'C\'était $name !'; + } + + @override + String get gameOverMessage => + '« On dirait que ton aventure s\'arrête ici. Tu n\'as plus d\'énergie ! »'; + + @override + String get statStreak => 'SÉRIE'; + + @override + String get statSeen => 'VUS'; + + @override + String get statScore => 'SCORE'; + + @override + String get tryAgain => 'REJOUER'; + + @override + String get backToPokedex => 'RETOUR AU POKÉDEX'; + + @override + String get system => 'SYSTÈME'; + + @override + String get statistics => 'STATISTIQUES'; + + @override + String get colorPalette => 'PALETTE DE COULEURS'; + + @override + String get language => 'LANGUE'; + + @override + String get statBestScore => 'Meilleur score'; + + @override + String get statCaught => 'Attrapés'; + + @override + String get statSeenLabel => 'Vus'; + + @override + String get statCompletion => 'Complétion'; +} diff --git a/lib/main.dart b/lib/main.dart index 0bdce7c..3c8158a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,10 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'l10n/app_localizations.dart'; import 'presentation/pages/main_page.dart'; import 'presentation/pages/pokemon_detail.dart'; import 'presentation/pages/game_over_page.dart'; import 'presentation/providers/theme_provider.dart'; +import 'presentation/providers/locale_provider.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -25,9 +27,13 @@ class MyApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final paletteIndex = ref.watch(themeProvider); final palette = appPalettes[paletteIndex]; + final locale = ref.watch(localeProvider); return MaterialApp( - title: 'Pokéguess', + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, + locale: locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: palette.primary, diff --git a/lib/presentation/pages/game_over_page.dart b/lib/presentation/pages/game_over_page.dart index 3e8088f..70d98f1 100644 --- a/lib/presentation/pages/game_over_page.dart +++ b/lib/presentation/pages/game_over_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/config/app_constants.dart'; +import '../../l10n/app_localizations.dart'; import '../providers/repository_provider.dart'; import '../widgets/game_over/game_over_header.dart'; import '../widgets/game_over/game_over_stats.dart'; @@ -70,10 +71,10 @@ class _GameOverPageState extends ConsumerState { width: double.infinity, color: messageBoxBg, padding: const EdgeInsets.all(24), - child: const Text( - "\"Looks like your journey\nends here. You've run out\nof energy!\"", + child: Text( + AppLocalizations.of(context)!.gameOverMessage, textAlign: TextAlign.center, - style: TextStyle(color: Colors.white, fontSize: 18, height: 1.5), + style: const TextStyle(color: Colors.white, fontSize: 18, height: 1.5), ), ), const SizedBox(height: 16), diff --git a/lib/presentation/pages/guess_page.dart b/lib/presentation/pages/guess_page.dart index a5d1c97..be04e76 100644 --- a/lib/presentation/pages/guess_page.dart +++ b/lib/presentation/pages/guess_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/game/game_state.dart'; +import '../../l10n/app_localizations.dart'; import '../providers/gen_filter_provider.dart'; import '../providers/game_provider.dart'; import '../providers/navigation_provider.dart'; @@ -44,19 +45,19 @@ class _GuessPageState extends ConsumerState { Future _onGuess() async { final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text); if (!mounted) return; + final l = AppLocalizations.of(context)!; final state = ref.read(gameProvider); switch (result) { case GuessResult.correct: + final name = state.currentPokemon!.formatedName; ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(state.isShiny - ? '✨ SHINY! You caught ${state.currentPokemon!.formatedName}! (+20 pts) ✨' - : 'Correct! You caught ${state.currentPokemon!.formatedName}!'), + content: Text(state.isShiny ? l.caughtShiny(name) : l.caughtNormal(name)), backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green, )); break; case GuessResult.wrong: - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange)); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(l.wrongGuess), backgroundColor: Colors.orange)); break; case GuessResult.gameOver: await _showGameOver(); @@ -96,7 +97,7 @@ class _GuessPageState extends ConsumerState { return const Center(child: CircularProgressIndicator()); } if (state.currentPokemon == null || state.status == GameStatus.error) { - return const Center(child: Text("Error loading Pokémon")); + return Center(child: Text(AppLocalizations.of(context)!.errorLoadingPokemon)); } final pokemon = state.currentPokemon!; diff --git a/lib/presentation/pages/pokemon_list.dart b/lib/presentation/pages/pokemon_list.dart index a78b9e9..4732538 100644 --- a/lib/presentation/pages/pokemon_list.dart +++ b/lib/presentation/pages/pokemon_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/pokemon.dart'; +import '../../l10n/app_localizations.dart'; import '../providers/pokedex_provider.dart'; import '../widgets/pokemon_tile.dart'; import '../widgets/scanline_overlay.dart'; @@ -32,6 +33,7 @@ class _PokemonListPageState extends ConsumerState { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; final pokedexAsync = ref.watch(pokedexProvider); final caughtCount = ref.watch(caughtCountProvider); @@ -45,8 +47,8 @@ class _PokemonListPageState extends ConsumerState { height: 40, child: Row( children: [ - _buildTab('ALL', _filter == 'ALL'), - _buildTab('CAUGHT', _filter == 'CAUGHT'), + _buildTab('ALL', l.tabAll, _filter == 'ALL'), + _buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT'), ], ), ), @@ -58,7 +60,7 @@ class _PokemonListPageState extends ConsumerState { pokedexAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( - child: Text('Erreur de chargement\n$e', + child: Text('${l.loadingError}\n$e', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)), ), data: (all) { @@ -79,8 +81,8 @@ class _PokemonListPageState extends ConsumerState { height: 24, color: const Color(0xFF1B2333), alignment: Alignment.center, - child: const Text('NATIONAL POKEDEX V2.0', - style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)), + child: Text(l.pokedexFooter, + style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)), ), ], ), @@ -88,25 +90,27 @@ class _PokemonListPageState extends ConsumerState { } Widget _emptyState() { + final l = AppLocalizations.of(context)!; + final filterLabel = _filter == 'CAUGHT' ? l.tabCaught : l.tabAll; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.search_off, size: 64, color: Colors.black26), const SizedBox(height: 16), - Text('NO POKEMON FOUND IN $_filter', + Text(l.noPokemonFound(filterLabel), style: const TextStyle(color: Colors.black45, fontSize: 18, fontWeight: FontWeight.bold)), ], ), ); } - Widget _buildTab(String title, bool isSelected) { + Widget _buildTab(String key, String label, bool isSelected) { return Expanded( child: GestureDetector( onTap: () { - if (_filter != title) { - setState(() => _filter = title); + if (_filter != key) { + setState(() => _filter = key); if (_scrollController.hasClients) _scrollController.jumpTo(0); } }, @@ -118,7 +122,7 @@ class _PokemonListPageState extends ConsumerState { : null, ), alignment: Alignment.center, - child: Text(title, + child: Text(label, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)), ), diff --git a/lib/presentation/pages/system_page.dart b/lib/presentation/pages/system_page.dart index 50985ac..a7d5ab0 100644 --- a/lib/presentation/pages/system_page.dart +++ b/lib/presentation/pages/system_page.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../core/config/app_constants.dart'; +import '../../l10n/app_localizations.dart'; import '../providers/pokedex_provider.dart'; import '../providers/theme_provider.dart'; +import '../providers/locale_provider.dart'; import '../widgets/system/system_header.dart'; import '../widgets/system/section_title.dart'; import '../widgets/system/system_stats.dart'; import '../widgets/system/palette_picker.dart'; +import '../widgets/system/language_picker.dart'; -/// Page "Système" : statistiques de jeu et sélection de la palette de couleurs. +/// Page "Système" : statistiques de jeu, langue et palette de couleurs. class SystemPage extends ConsumerWidget { const SystemPage({super.key}); @@ -20,6 +23,7 @@ class SystemPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l = AppLocalizations.of(context)!; final paletteIndex = ref.watch(themeProvider); final palette = appPalettes[paletteIndex]; final pokedexAsync = ref.watch(pokedexProvider); @@ -28,18 +32,18 @@ class SystemPage extends ConsumerWidget { color: const Color(0xFFC8D1D8), child: Column( children: [ - SystemHeader(primaryColor: palette.primary), + SystemHeader(primaryColor: palette.primary, title: l.system), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SectionTitle(text: 'STATISTIQUES', color: palette.primary), + SectionTitle(text: l.statistics, color: palette.primary), const SizedBox(height: 8), pokedexAsync.when( loading: () => const Center(child: CircularProgressIndicator()), - error: (_, __) => const Text('Erreur de chargement'), + error: (_, __) => Text(l.loadingError), data: (pokemons) { final total = pokemons.length; final caught = pokemons.where((p) => p.isCaught).length; @@ -52,10 +56,10 @@ class SystemPage extends ConsumerWidget { return StatsGrid( primaryColor: palette.primary, items: [ - StatItem(label: 'Meilleur score', value: '$best', icon: Icons.emoji_events), - StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon), - StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility), - StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart), + StatItem(label: l.statBestScore, value: '$best', icon: Icons.emoji_events), + StatItem(label: l.statCaught, value: '$caught / $total', icon: Icons.catching_pokemon), + StatItem(label: l.statSeenLabel, value: '$seen / $total', icon: Icons.visibility), + StatItem(label: l.statCompletion, value: '$pct%', icon: Icons.pie_chart), ], ); }, @@ -63,7 +67,15 @@ class SystemPage extends ConsumerWidget { }, ), const SizedBox(height: 24), - SectionTitle(text: 'PALETTE DE COULEURS', color: palette.primary), + SectionTitle(text: l.language, color: palette.primary), + const SizedBox(height: 8), + LanguagePicker( + selected: ref.watch(localeProvider), + primaryColor: palette.primary, + onSelect: (loc) => ref.read(localeProvider.notifier).setLocale(loc), + ), + const SizedBox(height: 24), + SectionTitle(text: l.colorPalette, color: palette.primary), const SizedBox(height: 8), PalettePicker( selectedIndex: paletteIndex, diff --git a/lib/presentation/providers/locale_provider.dart b/lib/presentation/providers/locale_provider.dart new file mode 100644 index 0000000..ef4e92a --- /dev/null +++ b/lib/presentation/providers/locale_provider.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../core/config/app_constants.dart'; + +/// Langues prises en charge par l'application. +const List supportedLocales = [Locale('en'), Locale('fr')]; + +/// Gère la langue sélectionnée, persistée dans les préférences. +class LocaleNotifier extends Notifier { + @override + Locale build() { + _loadSaved(); + return const Locale('en'); + } + + Future _loadSaved() async { + final prefs = await SharedPreferences.getInstance(); + final code = prefs.getString(AppConstants.prefsLocale); + if (code != null && supportedLocales.any((l) => l.languageCode == code)) { + state = Locale(code); + } + } + + Future setLocale(Locale locale) async { + state = locale; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(AppConstants.prefsLocale, locale.languageCode); + } +} + +/// Langue active de l'application. +final localeProvider = NotifierProvider(LocaleNotifier.new); diff --git a/lib/presentation/widgets/detail/pokemon_stats_panel.dart b/lib/presentation/widgets/detail/pokemon_stats_panel.dart index 7b5bbbc..fa8573c 100644 --- a/lib/presentation/widgets/detail/pokemon_stats_panel.dart +++ b/lib/presentation/widgets/detail/pokemon_stats_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../domain/entities/pokemon.dart'; +import '../../../l10n/app_localizations.dart'; /// Écran inférieur du détail : stats de base, description et éléments décoratifs. class PokemonStatsPanel extends StatelessWidget { @@ -8,6 +9,7 @@ class PokemonStatsPanel extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(8), @@ -21,7 +23,7 @@ class PokemonStatsPanel extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("BASE STATS", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), + Text(l.baseStats, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), Text("MODEL: DS-01", style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.bold)), ], ), @@ -38,7 +40,7 @@ class PokemonStatsPanel extends StatelessWidget { child: Text( pokemon.description != null && pokemon.description!.isNotEmpty ? '"${pokemon.description!}"' - : '"No description available for this Pokémon."', + : '"${l.noDescription}"', style: const TextStyle(fontSize: 16, height: 1.5), ), ), diff --git a/lib/presentation/widgets/game_over/game_over_actions.dart b/lib/presentation/widgets/game_over/game_over_actions.dart index a847a11..f1db5dc 100644 --- a/lib/presentation/widgets/game_over/game_over_actions.dart +++ b/lib/presentation/widgets/game_over/game_over_actions.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex. class GameOverActions extends StatelessWidget { @@ -9,11 +10,12 @@ class GameOverActions extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Column( children: [ - _button(label: "TRY AGAIN", icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain), + _button(label: l.tryAgain, icon: Icons.refresh, color: const Color(0xFF2962FF), onPressed: onTryAgain), const SizedBox(height: 16), - _button(label: "BACK TO POKEDEX", icon: Icons.menu_book, color: const Color(0xFFA66A00), onPressed: onBack), + _button(label: l.backToPokedex, icon: Icons.menu_book, color: const Color(0xFFA66A00), onPressed: onBack), ], ); } diff --git a/lib/presentation/widgets/game_over/game_over_header.dart b/lib/presentation/widgets/game_over/game_over_header.dart index a7a31cc..2174b0d 100644 --- a/lib/presentation/widgets/game_over/game_over_header.dart +++ b/lib/presentation/widgets/game_over/game_over_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; import '../pokemon_image.dart'; /// Bloc supérieur de l'écran game over : bannière "GAME OVER", image et nom du Pokémon. @@ -13,6 +14,7 @@ class GameOverHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Container( decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)), child: Container( @@ -24,9 +26,9 @@ class GameOverHeader extends StatelessWidget { Container( color: _darkRed, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: const Text( - "GAME OVER", - style: TextStyle( + child: Text( + l.gameOver, + style: const TextStyle( fontSize: 26, color: Colors.yellow, fontWeight: FontWeight.bold, @@ -40,7 +42,7 @@ class GameOverHeader extends StatelessWidget { SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)), const SizedBox(height: 12), Text( - "It was $pokemonName!", + l.itWas(pokemonName), textAlign: TextAlign.center, style: const TextStyle( fontSize: 22, diff --git a/lib/presentation/widgets/game_over/game_over_stats.dart b/lib/presentation/widgets/game_over/game_over_stats.dart index c6818a4..f0ce29a 100644 --- a/lib/presentation/widgets/game_over/game_over_stats.dart +++ b/lib/presentation/widgets/game_over/game_over_stats.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE). class GameOverStats extends StatelessWidget { @@ -10,13 +11,14 @@ class GameOverStats extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Row( children: [ - Expanded(child: _StatBox(label: "STREAK", value: streak)), + Expanded(child: _StatBox(label: l.statStreak, value: streak)), const SizedBox(width: 16), - Expanded(child: _StatBox(label: "SEEN", value: seen)), + Expanded(child: _StatBox(label: l.statSeen, value: seen)), const SizedBox(width: 16), - Expanded(child: _StatBox(label: "SCORE", value: score)), + Expanded(child: _StatBox(label: l.statScore, value: score)), ], ); } diff --git a/lib/presentation/widgets/guess/gen_filter_section.dart b/lib/presentation/widgets/guess/gen_filter_section.dart index 4863515..caa0122 100644 --- a/lib/presentation/widgets/guess/gen_filter_section.dart +++ b/lib/presentation/widgets/guess/gen_filter_section.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../core/config/app_constants.dart'; +import '../../../l10n/app_localizations.dart'; /// Bouton "GEN FILTER" et son panneau dépliable de sélection des générations. class GenFilterSection extends StatelessWidget { @@ -30,9 +31,9 @@ class GenFilterSection extends StatelessWidget { isOpen ? Icons.expand_less : Icons.filter_list, color: const Color(0xFF1B2333), ), - label: const Text( - 'GEN FILTER', - style: TextStyle( + label: Text( + AppLocalizations.of(context)!.genFilter, + style: const TextStyle( color: Color(0xFF1B2333), fontWeight: FontWeight.bold, fontSize: 16, diff --git a/lib/presentation/widgets/guess/guess_input_section.dart b/lib/presentation/widgets/guess/guess_input_section.dart index 6fd5494..944b68d 100644 --- a/lib/presentation/widgets/guess/guess_input_section.dart +++ b/lib/presentation/widgets/guess/guess_input_section.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Section de saisie : indice optionnel, champ de réponse et boutons d'action /// (Guess / Continue / Hint / Skip). Purement présentationnelle : tout passe par les callbacks. @@ -36,14 +37,15 @@ class GuessInputSection extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "IDENTIFICATION INPUT", - style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold), + Text( + l.identificationInput, + style: const TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), if (isHintUsed) @@ -57,7 +59,7 @@ class GuessInputSection extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - "HINT: ${_maskedName(pokemonName)}", + "${l.hintPrefix}: ${_maskedName(pokemonName)}", textAlign: TextAlign.center, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4), ), @@ -67,28 +69,28 @@ class GuessInputSection extends StatelessWidget { child: TextField( controller: controller, style: const TextStyle(fontSize: 24, letterSpacing: 1.5), - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), border: InputBorder.none, - hintText: 'Enter Pokémon name...', + hintText: l.enterPokemonName, ), onSubmitted: (_) => onGuess(), ), ), const SizedBox(height: 16), if (isGuessed) - _bigButton(label: "CONTINUE", color: Colors.green, onPressed: onContinue) + _bigButton(label: l.continueButton, color: Colors.green, onPressed: onContinue) else ...[ - _bigButton(label: "GUESS!", color: const Color(0xFF3B6EE3), onPressed: onGuess), + _bigButton(label: l.guessButton, color: const Color(0xFF3B6EE3), onPressed: onGuess), const SizedBox(height: 16), Row( children: [ Expanded( - child: _actionButton(icon: Icons.lightbulb, label: "HINT ($hints)", color: Colors.amber, onPressed: onHint), + child: _actionButton(icon: Icons.lightbulb, label: l.hintButton(hints), color: Colors.amber, onPressed: onHint), ), const SizedBox(width: 8), Expanded( - child: _actionButton(icon: Icons.skip_next, label: "SKIP ($skips)", color: Colors.grey[400]!, onPressed: onSkip), + child: _actionButton(icon: Icons.skip_next, label: l.skipButton(skips), color: Colors.grey[400]!, onPressed: onSkip), ), ], ), diff --git a/lib/presentation/widgets/guess/guess_silhouette.dart b/lib/presentation/widgets/guess/guess_silhouette.dart index adb941e..3c7845e 100644 --- a/lib/presentation/widgets/guess/guess_silhouette.dart +++ b/lib/presentation/widgets/guess/guess_silhouette.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../domain/entities/pokemon.dart'; +import '../../../l10n/app_localizations.dart'; import '../pokemon_image.dart'; /// Écran bleu affichant la silhouette (jeu en cours) ou l'image révélée (manche gagnée). @@ -17,6 +18,7 @@ class GuessSilhouette extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl; return Container( height: 250, @@ -53,7 +55,7 @@ class GuessSilhouette extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8), child: Text( - isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?", + isShiny ? l.shinyDetected : l.whosThatPokemon, textAlign: TextAlign.center, style: TextStyle( color: isShiny ? Colors.yellow[400] : Colors.white, diff --git a/lib/presentation/widgets/guess/score_board.dart b/lib/presentation/widgets/guess/score_board.dart index dcc9b92..948813c 100644 --- a/lib/presentation/widgets/guess/score_board.dart +++ b/lib/presentation/widgets/guess/score_board.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Encart affichant le score courant et le meilleur score personnel. class ScoreBoard extends StatelessWidget { @@ -9,6 +10,7 @@ class ScoreBoard extends StatelessWidget { @override Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; return Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -19,9 +21,9 @@ class ScoreBoard extends StatelessWidget { ), child: Column( children: [ - const Text( - "CURRENT SCORE", - style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold), + Text( + l.currentScore, + style: const TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold), ), Text( "$currentScore", @@ -29,7 +31,7 @@ class ScoreBoard extends StatelessWidget { ), const Divider(height: 24), Text( - "PERSONAL BEST: $bestScore", + l.personalBest(bestScore), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87), ), ], diff --git a/lib/presentation/widgets/list/pokedex_count_bar.dart b/lib/presentation/widgets/list/pokedex_count_bar.dart index 6b50e04..d7947ea 100644 --- a/lib/presentation/widgets/list/pokedex_count_bar.dart +++ b/lib/presentation/widgets/list/pokedex_count_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Bandeau affichant le nombre de Pokémon découverts sur le total. class PokedexCountBar extends StatelessWidget { @@ -20,8 +21,8 @@ class PokedexCountBar extends StatelessWidget { '${caught.toString().padLeft(3, '0')} / $total', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), ), - const Text('POKEMON DISCOVERED', - style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)), + Text(AppLocalizations.of(context)!.pokemonDiscovered, + style: const TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)), ], ), ); diff --git a/lib/presentation/widgets/list/pokedex_list_header.dart b/lib/presentation/widgets/list/pokedex_list_header.dart index 6a7ccde..cfe82b6 100644 --- a/lib/presentation/widgets/list/pokedex_list_header.dart +++ b/lib/presentation/widgets/list/pokedex_list_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../l10n/app_localizations.dart'; /// Barre de titre de la liste du Pokédex. class PokedexListHeader extends StatelessWidget { @@ -9,13 +10,13 @@ class PokedexListHeader extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), color: const Color(0xFF90A4AE), - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon(Icons.menu, color: Colors.black87), - Text('LIST - NATIONAL', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), - Icon(Icons.search, color: Colors.black87), + const Icon(Icons.menu, color: Colors.black87), + Text(AppLocalizations.of(context)!.listNational, + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), + const Icon(Icons.search, color: Colors.black87), ], ), ); diff --git a/lib/presentation/widgets/system/language_picker.dart b/lib/presentation/widgets/system/language_picker.dart new file mode 100644 index 0000000..27fc205 --- /dev/null +++ b/lib/presentation/widgets/system/language_picker.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import '../../providers/locale_provider.dart'; + +/// Sélecteur de langue de l'application (parmi [supportedLocales]). +class LanguagePicker extends StatelessWidget { + final Locale selected; + final Color primaryColor; + final void Function(Locale) onSelect; + + const LanguagePicker({ + super.key, + required this.selected, + required this.primaryColor, + required this.onSelect, + }); + + static const _names = {'en': 'English', 'fr': 'Français'}; + static const _flags = {'en': '🇬🇧', 'fr': '🇫🇷'}; + + @override + Widget build(BuildContext context) { + return Column( + children: supportedLocales.map((locale) { + final isSelected = locale.languageCode == selected.languageCode; + return GestureDetector( + onTap: () => onSelect(locale), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: isSelected ? primaryColor : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? primaryColor : Colors.grey.shade300, + width: isSelected ? 3 : 1.5, + ), + ), + child: Row( + children: [ + Text(_flags[locale.languageCode] ?? '', style: const TextStyle(fontSize: 22)), + const SizedBox(width: 16), + Text( + _names[locale.languageCode] ?? locale.languageCode, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isSelected ? Colors.white : Colors.black87, + ), + ), + const Spacer(), + if (isSelected) const Icon(Icons.check_circle, color: Colors.white, size: 22), + ], + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/presentation/widgets/system/system_header.dart b/lib/presentation/widgets/system/system_header.dart index 6adc2bd..39455e1 100644 --- a/lib/presentation/widgets/system/system_header.dart +++ b/lib/presentation/widgets/system/system_header.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; /// En-tête de la page Système. class SystemHeader extends StatelessWidget { final Color primaryColor; - const SystemHeader({super.key, required this.primaryColor}); + final String title; + const SystemHeader({super.key, required this.primaryColor, required this.title}); @override Widget build(BuildContext context) { @@ -14,10 +15,10 @@ class SystemHeader extends StatelessWidget { color: primaryColor, border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)), ), - child: const Text( - 'SYSTÈME', + child: Text( + title, textAlign: TextAlign.center, - style: TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4), + style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4), ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 5d3eee0..f65a3d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,11 @@ dependencies: flutter: sdk: flutter + # Internationalisation (FR / EN) + flutter_localizations: + sdk: flutter + intl: any + # Shared API sqflite_common: ^2.5.0 @@ -35,3 +40,4 @@ dev_dependencies: flutter: uses-material-design: true + generate: true # active la génération des localisations (gen-l10n)