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 <noreply@anthropic.com>
This commit is contained in:
Maxiwere45 2026-06-23 12:13:51 +02:00
parent 409ab9c1dd
commit a2a7ffd79f
26 changed files with 1012 additions and 70 deletions

4
l10n.yaml Normal file
View File

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

View File

@ -41,6 +41,9 @@ class AppConstants {
/// Clé SharedPreferences pour le filtre de générations. /// Clé SharedPreferences pour le filtre de générations.
static const String prefsGenFilter = 'gen_filter'; 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). /// Plages d'IDs Pokémon par génération [min, max] (inclusif).
static const List<(int, int)> genRanges = [ static const List<(int, int)> genRanges = [
(1, 151), // Gen 1 (1, 151), // Gen 1

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

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

50
lib/l10n/app_fr.arb Normal file
View File

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

View File

@ -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, 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('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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['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.');
}

View File

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

View File

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

View File

@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'l10n/app_localizations.dart';
import 'presentation/pages/main_page.dart'; import 'presentation/pages/main_page.dart';
import 'presentation/pages/pokemon_detail.dart'; import 'presentation/pages/pokemon_detail.dart';
import 'presentation/pages/game_over_page.dart'; import 'presentation/pages/game_over_page.dart';
import 'presentation/providers/theme_provider.dart'; import 'presentation/providers/theme_provider.dart';
import 'presentation/providers/locale_provider.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -25,9 +27,13 @@ class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final paletteIndex = ref.watch(themeProvider); final paletteIndex = ref.watch(themeProvider);
final palette = appPalettes[paletteIndex]; final palette = appPalettes[paletteIndex];
final locale = ref.watch(localeProvider);
return MaterialApp( return MaterialApp(
title: 'Pokéguess', onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: palette.primary, seedColor: palette.primary,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/config/app_constants.dart'; import '../../core/config/app_constants.dart';
import '../../l10n/app_localizations.dart';
import '../providers/repository_provider.dart'; import '../providers/repository_provider.dart';
import '../widgets/game_over/game_over_header.dart'; import '../widgets/game_over/game_over_header.dart';
import '../widgets/game_over/game_over_stats.dart'; import '../widgets/game_over/game_over_stats.dart';
@ -70,10 +71,10 @@ class _GameOverPageState extends ConsumerState<GameOverPage> {
width: double.infinity, width: double.infinity,
color: messageBoxBg, color: messageBoxBg,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: const Text( child: Text(
"\"Looks like your journey\nends here. You've run out\nof energy!\"", AppLocalizations.of(context)!.gameOverMessage,
textAlign: TextAlign.center, 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), const SizedBox(height: 16),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/game/game_state.dart'; import '../../domain/game/game_state.dart';
import '../../l10n/app_localizations.dart';
import '../providers/gen_filter_provider.dart'; import '../providers/gen_filter_provider.dart';
import '../providers/game_provider.dart'; import '../providers/game_provider.dart';
import '../providers/navigation_provider.dart'; import '../providers/navigation_provider.dart';
@ -44,19 +45,19 @@ class _GuessPageState extends ConsumerState<GuessPage> {
Future<void> _onGuess() async { Future<void> _onGuess() async {
final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text); final result = await ref.read(gameProvider.notifier).submitGuess(_guessController.text);
if (!mounted) return; if (!mounted) return;
final l = AppLocalizations.of(context)!;
final state = ref.read(gameProvider); final state = ref.read(gameProvider);
switch (result) { switch (result) {
case GuessResult.correct: case GuessResult.correct:
final name = state.currentPokemon!.formatedName;
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.isShiny content: Text(state.isShiny ? l.caughtShiny(name) : l.caughtNormal(name)),
? '✨ SHINY! You caught ${state.currentPokemon!.formatedName}! (+20 pts) ✨'
: 'Correct! You caught ${state.currentPokemon!.formatedName}!'),
backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green, backgroundColor: state.isShiny ? Colors.amber[800] : Colors.green,
)); ));
break; break;
case GuessResult.wrong: case GuessResult.wrong:
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange)); content: Text(l.wrongGuess), backgroundColor: Colors.orange));
break; break;
case GuessResult.gameOver: case GuessResult.gameOver:
await _showGameOver(); await _showGameOver();
@ -96,7 +97,7 @@ class _GuessPageState extends ConsumerState<GuessPage> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (state.currentPokemon == null || state.status == GameStatus.error) { 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!; final pokemon = state.currentPokemon!;

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/pokemon.dart'; import '../../domain/entities/pokemon.dart';
import '../../l10n/app_localizations.dart';
import '../providers/pokedex_provider.dart'; import '../providers/pokedex_provider.dart';
import '../widgets/pokemon_tile.dart'; import '../widgets/pokemon_tile.dart';
import '../widgets/scanline_overlay.dart'; import '../widgets/scanline_overlay.dart';
@ -32,6 +33,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final pokedexAsync = ref.watch(pokedexProvider); final pokedexAsync = ref.watch(pokedexProvider);
final caughtCount = ref.watch(caughtCountProvider); final caughtCount = ref.watch(caughtCountProvider);
@ -45,8 +47,8 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
height: 40, height: 40,
child: Row( child: Row(
children: [ children: [
_buildTab('ALL', _filter == 'ALL'), _buildTab('ALL', l.tabAll, _filter == 'ALL'),
_buildTab('CAUGHT', _filter == 'CAUGHT'), _buildTab('CAUGHT', l.tabCaught, _filter == 'CAUGHT'),
], ],
), ),
), ),
@ -58,7 +60,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
pokedexAsync.when( pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center( 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)), textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54)),
), ),
data: (all) { data: (all) {
@ -79,8 +81,8 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
height: 24, height: 24,
color: const Color(0xFF1B2333), color: const Color(0xFF1B2333),
alignment: Alignment.center, alignment: Alignment.center,
child: const Text('NATIONAL POKEDEX V2.0', child: Text(l.pokedexFooter,
style: TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)), style: const TextStyle(color: Colors.white70, fontSize: 12, letterSpacing: 1)),
), ),
], ],
), ),
@ -88,25 +90,27 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
} }
Widget _emptyState() { Widget _emptyState() {
final l = AppLocalizations.of(context)!;
final filterLabel = _filter == 'CAUGHT' ? l.tabCaught : l.tabAll;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.search_off, size: 64, color: Colors.black26), const Icon(Icons.search_off, size: 64, color: Colors.black26),
const SizedBox(height: 16), 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)), 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( return Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (_filter != title) { if (_filter != key) {
setState(() => _filter = title); setState(() => _filter = key);
if (_scrollController.hasClients) _scrollController.jumpTo(0); if (_scrollController.hasClients) _scrollController.jumpTo(0);
} }
}, },
@ -118,7 +122,7 @@ class _PokemonListPageState extends ConsumerState<PokemonListPage> {
: null, : null,
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text(title, child: Text(label,
style: TextStyle( style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)), fontSize: 18, fontWeight: FontWeight.bold, color: isSelected ? Colors.black : Colors.black54)),
), ),

View File

@ -2,14 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../core/config/app_constants.dart'; import '../../core/config/app_constants.dart';
import '../../l10n/app_localizations.dart';
import '../providers/pokedex_provider.dart'; import '../providers/pokedex_provider.dart';
import '../providers/theme_provider.dart'; import '../providers/theme_provider.dart';
import '../providers/locale_provider.dart';
import '../widgets/system/system_header.dart'; import '../widgets/system/system_header.dart';
import '../widgets/system/section_title.dart'; import '../widgets/system/section_title.dart';
import '../widgets/system/system_stats.dart'; import '../widgets/system/system_stats.dart';
import '../widgets/system/palette_picker.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 { class SystemPage extends ConsumerWidget {
const SystemPage({super.key}); const SystemPage({super.key});
@ -20,6 +23,7 @@ class SystemPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context)!;
final paletteIndex = ref.watch(themeProvider); final paletteIndex = ref.watch(themeProvider);
final palette = appPalettes[paletteIndex]; final palette = appPalettes[paletteIndex];
final pokedexAsync = ref.watch(pokedexProvider); final pokedexAsync = ref.watch(pokedexProvider);
@ -28,18 +32,18 @@ class SystemPage extends ConsumerWidget {
color: const Color(0xFFC8D1D8), color: const Color(0xFFC8D1D8),
child: Column( child: Column(
children: [ children: [
SystemHeader(primaryColor: palette.primary), SystemHeader(primaryColor: palette.primary, title: l.system),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SectionTitle(text: 'STATISTIQUES', color: palette.primary), SectionTitle(text: l.statistics, color: palette.primary),
const SizedBox(height: 8), const SizedBox(height: 8),
pokedexAsync.when( pokedexAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Text('Erreur de chargement'), error: (_, __) => Text(l.loadingError),
data: (pokemons) { data: (pokemons) {
final total = pokemons.length; final total = pokemons.length;
final caught = pokemons.where((p) => p.isCaught).length; final caught = pokemons.where((p) => p.isCaught).length;
@ -52,10 +56,10 @@ class SystemPage extends ConsumerWidget {
return StatsGrid( return StatsGrid(
primaryColor: palette.primary, primaryColor: palette.primary,
items: [ items: [
StatItem(label: 'Meilleur score', value: '$best', icon: Icons.emoji_events), StatItem(label: l.statBestScore, value: '$best', icon: Icons.emoji_events),
StatItem(label: 'Attrapés', value: '$caught / $total', icon: Icons.catching_pokemon), StatItem(label: l.statCaught, value: '$caught / $total', icon: Icons.catching_pokemon),
StatItem(label: 'Vus', value: '$seen / $total', icon: Icons.visibility), StatItem(label: l.statSeenLabel, value: '$seen / $total', icon: Icons.visibility),
StatItem(label: 'Complétion', value: '$pct%', icon: Icons.pie_chart), StatItem(label: l.statCompletion, value: '$pct%', icon: Icons.pie_chart),
], ],
); );
}, },
@ -63,7 +67,15 @@ class SystemPage extends ConsumerWidget {
}, },
), ),
const SizedBox(height: 24), 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), const SizedBox(height: 8),
PalettePicker( PalettePicker(
selectedIndex: paletteIndex, selectedIndex: paletteIndex,

View File

@ -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<Locale> supportedLocales = [Locale('en'), Locale('fr')];
/// Gère la langue sélectionnée, persistée dans les préférences.
class LocaleNotifier extends Notifier<Locale> {
@override
Locale build() {
_loadSaved();
return const Locale('en');
}
Future<void> _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<void> 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, Locale>(LocaleNotifier.new);

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.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. /// Écran inférieur du détail : stats de base, description et éléments décoratifs.
class PokemonStatsPanel extends StatelessWidget { class PokemonStatsPanel extends StatelessWidget {
@ -8,6 +9,7 @@ class PokemonStatsPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@ -21,7 +23,7 @@ class PokemonStatsPanel extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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)), 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( child: Text(
pokemon.description != null && pokemon.description!.isNotEmpty pokemon.description != null && pokemon.description!.isNotEmpty
? '"${pokemon.description!}"' ? '"${pokemon.description!}"'
: '"No description available for this Pokémon."', : '"${l.noDescription}"',
style: const TextStyle(fontSize: 16, height: 1.5), style: const TextStyle(fontSize: 16, height: 1.5),
), ),
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex. /// Boutons d'action de l'écran game over : rejouer ou retourner au Pokédex.
class GameOverActions extends StatelessWidget { class GameOverActions extends StatelessWidget {
@ -9,11 +10,12 @@ class GameOverActions extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Column( return Column(
children: [ 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), 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),
], ],
); );
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import '../pokemon_image.dart'; import '../pokemon_image.dart';
/// Bloc supérieur de l'écran game over : bannière "GAME OVER", image et nom du Pokémon. /// 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Container( return Container(
decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)), decoration: BoxDecoration(color: _darkRed, border: Border.all(color: _darkRed, width: 4)),
child: Container( child: Container(
@ -24,9 +26,9 @@ class GameOverHeader extends StatelessWidget {
Container( Container(
color: _darkRed, color: _darkRed,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: const Text( child: Text(
"GAME OVER", l.gameOver,
style: TextStyle( style: const TextStyle(
fontSize: 26, fontSize: 26,
color: Colors.yellow, color: Colors.yellow,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -40,7 +42,7 @@ class GameOverHeader extends StatelessWidget {
SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)), SizedBox(height: 140, child: PokemonImage(imageUrl: pokemonImage, fit: BoxFit.contain)),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
"It was $pokemonName!", l.itWas(pokemonName),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 22, fontSize: 22,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE). /// Rangée de statistiques de fin de partie (STREAK / SEEN / SCORE).
class GameOverStats extends StatelessWidget { class GameOverStats extends StatelessWidget {
@ -10,13 +11,14 @@ class GameOverStats extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Row( return Row(
children: [ children: [
Expanded(child: _StatBox(label: "STREAK", value: streak)), Expanded(child: _StatBox(label: l.statStreak, value: streak)),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SEEN", value: seen)), Expanded(child: _StatBox(label: l.statSeen, value: seen)),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded(child: _StatBox(label: "SCORE", value: score)), Expanded(child: _StatBox(label: l.statScore, value: score)),
], ],
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/config/app_constants.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. /// Bouton "GEN FILTER" et son panneau dépliable de sélection des générations.
class GenFilterSection extends StatelessWidget { class GenFilterSection extends StatelessWidget {
@ -30,9 +31,9 @@ class GenFilterSection extends StatelessWidget {
isOpen ? Icons.expand_less : Icons.filter_list, isOpen ? Icons.expand_less : Icons.filter_list,
color: const Color(0xFF1B2333), color: const Color(0xFF1B2333),
), ),
label: const Text( label: Text(
'GEN FILTER', AppLocalizations.of(context)!.genFilter,
style: TextStyle( style: const TextStyle(
color: Color(0xFF1B2333), color: Color(0xFF1B2333),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Section de saisie : indice optionnel, champ de réponse et boutons d'action /// 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. /// (Guess / Continue / Hint / Skip). Purement présentationnelle : tout passe par les callbacks.
@ -36,14 +37,15 @@ class GuessInputSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
"IDENTIFICATION INPUT", l.identificationInput,
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold), style: const TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
if (isHintUsed) if (isHintUsed)
@ -57,7 +59,7 @@ class GuessInputSection extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
"HINT: ${_maskedName(pokemonName)}", "${l.hintPrefix}: ${_maskedName(pokemonName)}",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4), style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.amber[900], letterSpacing: 4),
), ),
@ -67,28 +69,28 @@ class GuessInputSection extends StatelessWidget {
child: TextField( child: TextField(
controller: controller, controller: controller,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5), style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none, border: InputBorder.none,
hintText: 'Enter Pokémon name...', hintText: l.enterPokemonName,
), ),
onSubmitted: (_) => onGuess(), onSubmitted: (_) => onGuess(),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (isGuessed) if (isGuessed)
_bigButton(label: "CONTINUE", color: Colors.green, onPressed: onContinue) _bigButton(label: l.continueButton, color: Colors.green, onPressed: onContinue)
else ...[ else ...[
_bigButton(label: "GUESS!", color: const Color(0xFF3B6EE3), onPressed: onGuess), _bigButton(label: l.guessButton, color: const Color(0xFF3B6EE3), onPressed: onGuess),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
children: [ children: [
Expanded( 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), const SizedBox(width: 8),
Expanded( 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),
), ),
], ],
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../domain/entities/pokemon.dart'; import '../../../domain/entities/pokemon.dart';
import '../../../l10n/app_localizations.dart';
import '../pokemon_image.dart'; import '../pokemon_image.dart';
/// Écran bleu affichant la silhouette (jeu en cours) ou l'image révélée (manche gagnée). /// É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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl; final imageUrl = isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl;
return Container( return Container(
height: 250, height: 250,
@ -53,7 +55,7 @@ class GuessSilhouette extends StatelessWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Text( child: Text(
isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?", isShiny ? l.shinyDetected : l.whosThatPokemon,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: isShiny ? Colors.yellow[400] : Colors.white, color: isShiny ? Colors.yellow[400] : Colors.white,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Encart affichant le score courant et le meilleur score personnel. /// Encart affichant le score courant et le meilleur score personnel.
class ScoreBoard extends StatelessWidget { class ScoreBoard extends StatelessWidget {
@ -9,6 +10,7 @@ class ScoreBoard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -19,9 +21,9 @@ class ScoreBoard extends StatelessWidget {
), ),
child: Column( child: Column(
children: [ children: [
const Text( Text(
"CURRENT SCORE", l.currentScore,
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
), ),
Text( Text(
"$currentScore", "$currentScore",
@ -29,7 +31,7 @@ class ScoreBoard extends StatelessWidget {
), ),
const Divider(height: 24), const Divider(height: 24),
Text( Text(
"PERSONAL BEST: $bestScore", l.personalBest(bestScore),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
), ),
], ],

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Bandeau affichant le nombre de Pokémon découverts sur le total. /// Bandeau affichant le nombre de Pokémon découverts sur le total.
class PokedexCountBar extends StatelessWidget { class PokedexCountBar extends StatelessWidget {
@ -20,8 +21,8 @@ class PokedexCountBar extends StatelessWidget {
'${caught.toString().padLeft(3, '0')} / $total', '${caught.toString().padLeft(3, '0')} / $total',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
), ),
const Text('POKEMON DISCOVERED', Text(AppLocalizations.of(context)!.pokemonDiscovered,
style: TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)), style: const TextStyle(fontSize: 14, color: Colors.black54, letterSpacing: 1)),
], ],
), ),
); );

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
/// Barre de titre de la liste du Pokédex. /// Barre de titre de la liste du Pokédex.
class PokedexListHeader extends StatelessWidget { class PokedexListHeader extends StatelessWidget {
@ -9,13 +10,13 @@ class PokedexListHeader extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
color: const Color(0xFF90A4AE), color: const Color(0xFF90A4AE),
child: const Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Icon(Icons.menu, color: Colors.black87), const Icon(Icons.menu, color: Colors.black87),
Text('LIST - NATIONAL', Text(AppLocalizations.of(context)!.listNational,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2)),
Icon(Icons.search, color: Colors.black87), const Icon(Icons.search, color: Colors.black87),
], ],
), ),
); );

View File

@ -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(),
);
}
}

View File

@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
/// En-tête de la page Système. /// En-tête de la page Système.
class SystemHeader extends StatelessWidget { class SystemHeader extends StatelessWidget {
final Color primaryColor; final Color primaryColor;
const SystemHeader({super.key, required this.primaryColor}); final String title;
const SystemHeader({super.key, required this.primaryColor, required this.title});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -14,10 +15,10 @@ class SystemHeader extends StatelessWidget {
color: primaryColor, color: primaryColor,
border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)), border: Border(bottom: BorderSide(color: primaryColor.withAlpha(180), width: 3)),
), ),
child: const Text( child: Text(
'SYSTÈME', title,
textAlign: TextAlign.center, 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),
), ),
); );
} }

View File

@ -11,6 +11,11 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# Internationalisation (FR / EN)
flutter_localizations:
sdk: flutter
intl: any
# Shared API # Shared API
sqflite_common: ^2.5.0 sqflite_common: ^2.5.0
@ -35,3 +40,4 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
generate: true # active la génération des localisations (gen-l10n)