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:
2026-06-23 12:13:51 +02:00
co-authored by Claude Opus 4.8
parent 409ab9c1dd
commit a2a7ffd79f
26 changed files with 1012 additions and 70 deletions
+57
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
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"
}
+385
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.');
}
+151
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';
}
+151
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';
}