From 35e3d92316fb4533a9d453184d6bb4b18bfbe67d Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 11 Dec 2025 17:50:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(l10n):=20=EA=B5=AD=EC=A0=9C=ED=99=94(L10n)?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=8F=84=EC=9E=85=20=EB=B0=8F?= =?UTF-8?q?=20=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - flutter_localizations 및 intl 패키지 추가 - l10n.yaml 설정 파일 및 app_ko.arb 메시지 파일 생성 - 모든 화면(app, front, game_play, new_character, save_picker)의 하드코딩 텍스트를 L10n 키로 변환 - 테스트 파일에 localizationsDelegates 추가하여 L10n 지원 --- l10n.yaml | 5 + lib/l10n/app_en.arb | 246 ++++++++ lib/l10n/app_ja.arb | 75 +++ lib/l10n/app_ko.arb | 75 +++ lib/l10n/app_localizations.dart | 566 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 235 ++++++++ lib/l10n/app_localizations_ja.dart | 235 ++++++++ lib/l10n/app_localizations_ko.dart | 235 ++++++++ lib/l10n/app_localizations_zh.dart | 235 ++++++++ lib/l10n/app_zh.arb | 75 +++ lib/src/app.dart | 11 +- lib/src/features/front/front_screen.dart | 39 +- .../features/front/save_picker_dialog.dart | 8 +- lib/src/features/game/game_play_screen.dart | 124 ++-- .../game/widgets/task_progress_panel.dart | 3 +- .../new_character/new_character_screen.dart | 39 +- pubspec.lock | 9 +- pubspec.yaml | 5 +- test/features/game_play_screen_test.dart | 26 +- test/features/new_character_screen_test.dart | 22 +- 20 files changed, 2155 insertions(+), 113 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_ja.arb create mode 100644 lib/l10n/app_ko.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_ja.dart create mode 100644 lib/l10n/app_localizations_ko.dart create mode 100644 lib/l10n/app_localizations_zh.dart create mode 100644 lib/l10n/app_zh.arb diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..1e56f74 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: L10n +nullable-getter: false diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..e083e6d --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,246 @@ +{ + "@@locale": "en", + + "appTitle": "Ascii Never Die", + "@appTitle": { "description": "Application title" }, + + "tagNoNetwork": "No network", + "@tagNoNetwork": { "description": "Tag indicating offline mode" }, + + "tagIdleRpg": "Idle RPG loop", + "@tagIdleRpg": { "description": "Tag indicating idle RPG gameplay" }, + + "tagLocalSaves": "Local saves", + "@tagLocalSaves": { "description": "Tag indicating local save support" }, + + "newCharacter": "New character", + "@newCharacter": { "description": "New character button" }, + + "loadSave": "Load save", + "@loadSave": { "description": "Load save button" }, + + "loadGame": "Load Game", + "@loadGame": { "description": "Load game dialog title" }, + + "viewBuildPlan": "View build plan", + "@viewBuildPlan": { "description": "View build plan button" }, + + "buildRoadmap": "Build roadmap", + "@buildRoadmap": { "description": "Build roadmap section title" }, + + "techStack": "Tech stack", + "@techStack": { "description": "Tech stack section title" }, + + "cancel": "Cancel", + "@cancel": { "description": "Cancel button" }, + + "exitGame": "Exit Game", + "@exitGame": { "description": "Exit game dialog title" }, + + "saveProgressQuestion": "Save your progress before leaving?", + "@saveProgressQuestion": { "description": "Save progress confirmation message" }, + + "exitWithoutSaving": "Exit without saving", + "@exitWithoutSaving": { "description": "Exit without saving button" }, + + "saveAndExit": "Save and Exit", + "@saveAndExit": { "description": "Save and exit button" }, + + "progressQuestTitle": "Progress Quest - {name}", + "@progressQuestTitle": { + "description": "Game screen title with character name", + "placeholders": { + "name": { "type": "String" } + } + }, + + "levelUp": "Level Up", + "@levelUp": { "description": "Level up tooltip" }, + + "completeQuest": "Complete Quest", + "@completeQuest": { "description": "Complete quest tooltip" }, + + "completePlot": "Complete Plot", + "@completePlot": { "description": "Complete plot tooltip" }, + + "characterSheet": "Character Sheet", + "@characterSheet": { "description": "Character sheet panel title" }, + + "traits": "Traits", + "@traits": { "description": "Traits section title" }, + + "stats": "Stats", + "@stats": { "description": "Stats section title" }, + + "experience": "Experience", + "@experience": { "description": "Experience section title" }, + + "xpNeededForNextLevel": "XP needed for next level", + "@xpNeededForNextLevel": { "description": "XP needed tooltip" }, + + "spellBook": "Spell Book", + "@spellBook": { "description": "Spell book section title" }, + + "noSpellsYet": "No spells yet", + "@noSpellsYet": { "description": "Empty spell book message" }, + + "equipment": "Equipment", + "@equipment": { "description": "Equipment panel title" }, + + "inventory": "Inventory", + "@inventory": { "description": "Inventory panel title" }, + + "encumbrance": "Encumbrance", + "@encumbrance": { "description": "Encumbrance section title" }, + + "plotDevelopment": "Plot Development", + "@plotDevelopment": { "description": "Plot development panel title" }, + + "quests": "Quests", + "@quests": { "description": "Quests panel title" }, + + "traitName": "Name", + "@traitName": { "description": "Name trait label" }, + + "traitRace": "Race", + "@traitRace": { "description": "Race trait label" }, + + "traitClass": "Class", + "@traitClass": { "description": "Class trait label" }, + + "traitLevel": "Level", + "@traitLevel": { "description": "Level trait label" }, + + "statStr": "STR", + "@statStr": { "description": "Strength stat" }, + + "statCon": "CON", + "@statCon": { "description": "Constitution stat" }, + + "statDex": "DEX", + "@statDex": { "description": "Dexterity stat" }, + + "statInt": "INT", + "@statInt": { "description": "Intelligence stat" }, + + "statWis": "WIS", + "@statWis": { "description": "Wisdom stat" }, + + "statCha": "CHA", + "@statCha": { "description": "Charisma stat" }, + + "statHpMax": "HP Max", + "@statHpMax": { "description": "Max HP stat" }, + + "statMpMax": "MP Max", + "@statMpMax": { "description": "Max MP stat" }, + + "equipWeapon": "Weapon", + "@equipWeapon": { "description": "Weapon equipment slot" }, + + "equipShield": "Shield", + "@equipShield": { "description": "Shield equipment slot" }, + + "equipHelm": "Helm", + "@equipHelm": { "description": "Helm equipment slot" }, + + "equipHauberk": "Hauberk", + "@equipHauberk": { "description": "Hauberk equipment slot" }, + + "equipBrassairts": "Brassairts", + "@equipBrassairts": { "description": "Brassairts equipment slot" }, + + "equipVambraces": "Vambraces", + "@equipVambraces": { "description": "Vambraces equipment slot" }, + + "equipGauntlets": "Gauntlets", + "@equipGauntlets": { "description": "Gauntlets equipment slot" }, + + "equipGambeson": "Gambeson", + "@equipGambeson": { "description": "Gambeson equipment slot" }, + + "equipCuisses": "Cuisses", + "@equipCuisses": { "description": "Cuisses equipment slot" }, + + "equipGreaves": "Greaves", + "@equipGreaves": { "description": "Greaves equipment slot" }, + + "equipSollerets": "Sollerets", + "@equipSollerets": { "description": "Sollerets equipment slot" }, + + "gold": "Gold", + "@gold": { "description": "Gold label" }, + + "goldAmount": "Gold: {amount}", + "@goldAmount": { + "description": "Gold with amount", + "placeholders": { + "amount": { "type": "int" } + } + }, + + "prologue": "Prologue", + "@prologue": { "description": "Prologue plot stage" }, + + "actNumber": "Act {number}", + "@actNumber": { + "description": "Act with roman numeral", + "placeholders": { + "number": { "type": "String" } + } + }, + + "noActiveQuests": "No active quests", + "@noActiveQuests": { "description": "Empty quests message" }, + + "questNumber": "Quest #{number}", + "@questNumber": { + "description": "Quest with number", + "placeholders": { + "number": { "type": "int" } + } + }, + + "welcomeMessage": "Welcome to Progress Quest!", + "@welcomeMessage": { "description": "Welcome message in task progress panel" }, + + "noSavedGames": "No saved games found.", + "@noSavedGames": { "description": "No saved games message" }, + + "loadError": "Failed to load save file: {error}", + "@loadError": { + "description": "Load error message", + "placeholders": { + "error": { "type": "String" } + } + }, + + "name": "Name", + "@name": { "description": "Name label in character creation" }, + + "generateName": "Generate Name", + "@generateName": { "description": "Generate name tooltip" }, + + "total": "Total", + "@total": { "description": "Total label for stats" }, + + "unroll": "Unroll", + "@unroll": { "description": "Unroll button" }, + + "roll": "Roll", + "@roll": { "description": "Roll button" }, + + "race": "Race", + "@race": { "description": "Race selection title" }, + + "classTitle": "Class", + "@classTitle": { "description": "Class selection title" }, + + "percentComplete": "{percent}% complete", + "@percentComplete": { + "description": "Percentage complete", + "placeholders": { + "percent": { "type": "int" } + } + } +} diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb new file mode 100644 index 0000000..9779939 --- /dev/null +++ b/lib/l10n/app_ja.arb @@ -0,0 +1,75 @@ +{ + "@@locale": "ja", + + "appTitle": "Ascii Never Die", + "tagNoNetwork": "No network", + "tagIdleRpg": "Idle RPG loop", + "tagLocalSaves": "Local saves", + "newCharacter": "New character", + "loadSave": "Load save", + "loadGame": "Load Game", + "viewBuildPlan": "View build plan", + "buildRoadmap": "Build roadmap", + "techStack": "Tech stack", + "cancel": "Cancel", + "exitGame": "Exit Game", + "saveProgressQuestion": "Save your progress before leaving?", + "exitWithoutSaving": "Exit without saving", + "saveAndExit": "Save and Exit", + "progressQuestTitle": "Progress Quest - {name}", + "levelUp": "Level Up", + "completeQuest": "Complete Quest", + "completePlot": "Complete Plot", + "characterSheet": "Character Sheet", + "traits": "Traits", + "stats": "Stats", + "experience": "Experience", + "xpNeededForNextLevel": "XP needed for next level", + "spellBook": "Spell Book", + "noSpellsYet": "No spells yet", + "equipment": "Equipment", + "inventory": "Inventory", + "encumbrance": "Encumbrance", + "plotDevelopment": "Plot Development", + "quests": "Quests", + "traitName": "Name", + "traitRace": "Race", + "traitClass": "Class", + "traitLevel": "Level", + "statStr": "STR", + "statCon": "CON", + "statDex": "DEX", + "statInt": "INT", + "statWis": "WIS", + "statCha": "CHA", + "statHpMax": "HP Max", + "statMpMax": "MP Max", + "equipWeapon": "Weapon", + "equipShield": "Shield", + "equipHelm": "Helm", + "equipHauberk": "Hauberk", + "equipBrassairts": "Brassairts", + "equipVambraces": "Vambraces", + "equipGauntlets": "Gauntlets", + "equipGambeson": "Gambeson", + "equipCuisses": "Cuisses", + "equipGreaves": "Greaves", + "equipSollerets": "Sollerets", + "gold": "Gold", + "goldAmount": "Gold: {amount}", + "prologue": "Prologue", + "actNumber": "Act {number}", + "noActiveQuests": "No active quests", + "questNumber": "Quest #{number}", + "welcomeMessage": "Welcome to Progress Quest!", + "noSavedGames": "No saved games found.", + "loadError": "Failed to load save file: {error}", + "name": "Name", + "generateName": "Generate Name", + "total": "Total", + "unroll": "Unroll", + "roll": "Roll", + "race": "Race", + "classTitle": "Class", + "percentComplete": "{percent}% complete" +} diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb new file mode 100644 index 0000000..b0ec28f --- /dev/null +++ b/lib/l10n/app_ko.arb @@ -0,0 +1,75 @@ +{ + "@@locale": "ko", + + "appTitle": "Ascii Never Die", + "tagNoNetwork": "No network", + "tagIdleRpg": "Idle RPG loop", + "tagLocalSaves": "Local saves", + "newCharacter": "New character", + "loadSave": "Load save", + "loadGame": "Load Game", + "viewBuildPlan": "View build plan", + "buildRoadmap": "Build roadmap", + "techStack": "Tech stack", + "cancel": "Cancel", + "exitGame": "Exit Game", + "saveProgressQuestion": "Save your progress before leaving?", + "exitWithoutSaving": "Exit without saving", + "saveAndExit": "Save and Exit", + "progressQuestTitle": "Progress Quest - {name}", + "levelUp": "Level Up", + "completeQuest": "Complete Quest", + "completePlot": "Complete Plot", + "characterSheet": "Character Sheet", + "traits": "Traits", + "stats": "Stats", + "experience": "Experience", + "xpNeededForNextLevel": "XP needed for next level", + "spellBook": "Spell Book", + "noSpellsYet": "No spells yet", + "equipment": "Equipment", + "inventory": "Inventory", + "encumbrance": "Encumbrance", + "plotDevelopment": "Plot Development", + "quests": "Quests", + "traitName": "Name", + "traitRace": "Race", + "traitClass": "Class", + "traitLevel": "Level", + "statStr": "STR", + "statCon": "CON", + "statDex": "DEX", + "statInt": "INT", + "statWis": "WIS", + "statCha": "CHA", + "statHpMax": "HP Max", + "statMpMax": "MP Max", + "equipWeapon": "Weapon", + "equipShield": "Shield", + "equipHelm": "Helm", + "equipHauberk": "Hauberk", + "equipBrassairts": "Brassairts", + "equipVambraces": "Vambraces", + "equipGauntlets": "Gauntlets", + "equipGambeson": "Gambeson", + "equipCuisses": "Cuisses", + "equipGreaves": "Greaves", + "equipSollerets": "Sollerets", + "gold": "Gold", + "goldAmount": "Gold: {amount}", + "prologue": "Prologue", + "actNumber": "Act {number}", + "noActiveQuests": "No active quests", + "questNumber": "Quest #{number}", + "welcomeMessage": "Welcome to Progress Quest!", + "noSavedGames": "No saved games found.", + "loadError": "Failed to load save file: {error}", + "name": "Name", + "generateName": "Generate Name", + "total": "Total", + "unroll": "Unroll", + "roll": "Roll", + "race": "Race", + "classTitle": "Class", + "percentComplete": "{percent}% complete" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..c6b4e52 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,566 @@ +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_ja.dart'; +import 'app_localizations_ko.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of L10n +/// returned by `L10n.of(context)`. +/// +/// Applications need to include `L10n.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: L10n.localizationsDelegates, +/// supportedLocales: L10n.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 L10n.supportedLocales +/// property. +abstract class L10n { + L10n(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static L10n of(BuildContext context) { + return Localizations.of(context, L10n)!; + } + + static const LocalizationsDelegate delegate = _L10nDelegate(); + + /// 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('ja'), + Locale('ko'), + Locale('zh'), + ]; + + /// Application title + /// + /// In en, this message translates to: + /// **'Ascii Never Die'** + String get appTitle; + + /// Tag indicating offline mode + /// + /// In en, this message translates to: + /// **'No network'** + String get tagNoNetwork; + + /// Tag indicating idle RPG gameplay + /// + /// In en, this message translates to: + /// **'Idle RPG loop'** + String get tagIdleRpg; + + /// Tag indicating local save support + /// + /// In en, this message translates to: + /// **'Local saves'** + String get tagLocalSaves; + + /// New character button + /// + /// In en, this message translates to: + /// **'New character'** + String get newCharacter; + + /// Load save button + /// + /// In en, this message translates to: + /// **'Load save'** + String get loadSave; + + /// Load game dialog title + /// + /// In en, this message translates to: + /// **'Load Game'** + String get loadGame; + + /// View build plan button + /// + /// In en, this message translates to: + /// **'View build plan'** + String get viewBuildPlan; + + /// Build roadmap section title + /// + /// In en, this message translates to: + /// **'Build roadmap'** + String get buildRoadmap; + + /// Tech stack section title + /// + /// In en, this message translates to: + /// **'Tech stack'** + String get techStack; + + /// Cancel button + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// Exit game dialog title + /// + /// In en, this message translates to: + /// **'Exit Game'** + String get exitGame; + + /// Save progress confirmation message + /// + /// In en, this message translates to: + /// **'Save your progress before leaving?'** + String get saveProgressQuestion; + + /// Exit without saving button + /// + /// In en, this message translates to: + /// **'Exit without saving'** + String get exitWithoutSaving; + + /// Save and exit button + /// + /// In en, this message translates to: + /// **'Save and Exit'** + String get saveAndExit; + + /// Game screen title with character name + /// + /// In en, this message translates to: + /// **'Progress Quest - {name}'** + String progressQuestTitle(String name); + + /// Level up tooltip + /// + /// In en, this message translates to: + /// **'Level Up'** + String get levelUp; + + /// Complete quest tooltip + /// + /// In en, this message translates to: + /// **'Complete Quest'** + String get completeQuest; + + /// Complete plot tooltip + /// + /// In en, this message translates to: + /// **'Complete Plot'** + String get completePlot; + + /// Character sheet panel title + /// + /// In en, this message translates to: + /// **'Character Sheet'** + String get characterSheet; + + /// Traits section title + /// + /// In en, this message translates to: + /// **'Traits'** + String get traits; + + /// Stats section title + /// + /// In en, this message translates to: + /// **'Stats'** + String get stats; + + /// Experience section title + /// + /// In en, this message translates to: + /// **'Experience'** + String get experience; + + /// XP needed tooltip + /// + /// In en, this message translates to: + /// **'XP needed for next level'** + String get xpNeededForNextLevel; + + /// Spell book section title + /// + /// In en, this message translates to: + /// **'Spell Book'** + String get spellBook; + + /// Empty spell book message + /// + /// In en, this message translates to: + /// **'No spells yet'** + String get noSpellsYet; + + /// Equipment panel title + /// + /// In en, this message translates to: + /// **'Equipment'** + String get equipment; + + /// Inventory panel title + /// + /// In en, this message translates to: + /// **'Inventory'** + String get inventory; + + /// Encumbrance section title + /// + /// In en, this message translates to: + /// **'Encumbrance'** + String get encumbrance; + + /// Plot development panel title + /// + /// In en, this message translates to: + /// **'Plot Development'** + String get plotDevelopment; + + /// Quests panel title + /// + /// In en, this message translates to: + /// **'Quests'** + String get quests; + + /// Name trait label + /// + /// In en, this message translates to: + /// **'Name'** + String get traitName; + + /// Race trait label + /// + /// In en, this message translates to: + /// **'Race'** + String get traitRace; + + /// Class trait label + /// + /// In en, this message translates to: + /// **'Class'** + String get traitClass; + + /// Level trait label + /// + /// In en, this message translates to: + /// **'Level'** + String get traitLevel; + + /// Strength stat + /// + /// In en, this message translates to: + /// **'STR'** + String get statStr; + + /// Constitution stat + /// + /// In en, this message translates to: + /// **'CON'** + String get statCon; + + /// Dexterity stat + /// + /// In en, this message translates to: + /// **'DEX'** + String get statDex; + + /// Intelligence stat + /// + /// In en, this message translates to: + /// **'INT'** + String get statInt; + + /// Wisdom stat + /// + /// In en, this message translates to: + /// **'WIS'** + String get statWis; + + /// Charisma stat + /// + /// In en, this message translates to: + /// **'CHA'** + String get statCha; + + /// Max HP stat + /// + /// In en, this message translates to: + /// **'HP Max'** + String get statHpMax; + + /// Max MP stat + /// + /// In en, this message translates to: + /// **'MP Max'** + String get statMpMax; + + /// Weapon equipment slot + /// + /// In en, this message translates to: + /// **'Weapon'** + String get equipWeapon; + + /// Shield equipment slot + /// + /// In en, this message translates to: + /// **'Shield'** + String get equipShield; + + /// Helm equipment slot + /// + /// In en, this message translates to: + /// **'Helm'** + String get equipHelm; + + /// Hauberk equipment slot + /// + /// In en, this message translates to: + /// **'Hauberk'** + String get equipHauberk; + + /// Brassairts equipment slot + /// + /// In en, this message translates to: + /// **'Brassairts'** + String get equipBrassairts; + + /// Vambraces equipment slot + /// + /// In en, this message translates to: + /// **'Vambraces'** + String get equipVambraces; + + /// Gauntlets equipment slot + /// + /// In en, this message translates to: + /// **'Gauntlets'** + String get equipGauntlets; + + /// Gambeson equipment slot + /// + /// In en, this message translates to: + /// **'Gambeson'** + String get equipGambeson; + + /// Cuisses equipment slot + /// + /// In en, this message translates to: + /// **'Cuisses'** + String get equipCuisses; + + /// Greaves equipment slot + /// + /// In en, this message translates to: + /// **'Greaves'** + String get equipGreaves; + + /// Sollerets equipment slot + /// + /// In en, this message translates to: + /// **'Sollerets'** + String get equipSollerets; + + /// Gold label + /// + /// In en, this message translates to: + /// **'Gold'** + String get gold; + + /// Gold with amount + /// + /// In en, this message translates to: + /// **'Gold: {amount}'** + String goldAmount(int amount); + + /// Prologue plot stage + /// + /// In en, this message translates to: + /// **'Prologue'** + String get prologue; + + /// Act with roman numeral + /// + /// In en, this message translates to: + /// **'Act {number}'** + String actNumber(String number); + + /// Empty quests message + /// + /// In en, this message translates to: + /// **'No active quests'** + String get noActiveQuests; + + /// Quest with number + /// + /// In en, this message translates to: + /// **'Quest #{number}'** + String questNumber(int number); + + /// Welcome message in task progress panel + /// + /// In en, this message translates to: + /// **'Welcome to Progress Quest!'** + String get welcomeMessage; + + /// No saved games message + /// + /// In en, this message translates to: + /// **'No saved games found.'** + String get noSavedGames; + + /// Load error message + /// + /// In en, this message translates to: + /// **'Failed to load save file: {error}'** + String loadError(String error); + + /// Name label in character creation + /// + /// In en, this message translates to: + /// **'Name'** + String get name; + + /// Generate name tooltip + /// + /// In en, this message translates to: + /// **'Generate Name'** + String get generateName; + + /// Total label for stats + /// + /// In en, this message translates to: + /// **'Total'** + String get total; + + /// Unroll button + /// + /// In en, this message translates to: + /// **'Unroll'** + String get unroll; + + /// Roll button + /// + /// In en, this message translates to: + /// **'Roll'** + String get roll; + + /// Race selection title + /// + /// In en, this message translates to: + /// **'Race'** + String get race; + + /// Class selection title + /// + /// In en, this message translates to: + /// **'Class'** + String get classTitle; + + /// Percentage complete + /// + /// In en, this message translates to: + /// **'{percent}% complete'** + String percentComplete(int percent); +} + +class _L10nDelegate extends LocalizationsDelegate { + const _L10nDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupL10n(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'ja', 'ko', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_L10nDelegate old) => false; +} + +L10n lookupL10n(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return L10nEn(); + case 'ja': + return L10nJa(); + case 'ko': + return L10nKo(); + case 'zh': + return L10nZh(); + } + + throw FlutterError( + 'L10n.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..1cc7f92 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,235 @@ +// 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 L10nEn extends L10n { + L10nEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Ascii Never Die'; + + @override + String get tagNoNetwork => 'No network'; + + @override + String get tagIdleRpg => 'Idle RPG loop'; + + @override + String get tagLocalSaves => 'Local saves'; + + @override + String get newCharacter => 'New character'; + + @override + String get loadSave => 'Load save'; + + @override + String get loadGame => 'Load Game'; + + @override + String get viewBuildPlan => 'View build plan'; + + @override + String get buildRoadmap => 'Build roadmap'; + + @override + String get techStack => 'Tech stack'; + + @override + String get cancel => 'Cancel'; + + @override + String get exitGame => 'Exit Game'; + + @override + String get saveProgressQuestion => 'Save your progress before leaving?'; + + @override + String get exitWithoutSaving => 'Exit without saving'; + + @override + String get saveAndExit => 'Save and Exit'; + + @override + String progressQuestTitle(String name) { + return 'Progress Quest - $name'; + } + + @override + String get levelUp => 'Level Up'; + + @override + String get completeQuest => 'Complete Quest'; + + @override + String get completePlot => 'Complete Plot'; + + @override + String get characterSheet => 'Character Sheet'; + + @override + String get traits => 'Traits'; + + @override + String get stats => 'Stats'; + + @override + String get experience => 'Experience'; + + @override + String get xpNeededForNextLevel => 'XP needed for next level'; + + @override + String get spellBook => 'Spell Book'; + + @override + String get noSpellsYet => 'No spells yet'; + + @override + String get equipment => 'Equipment'; + + @override + String get inventory => 'Inventory'; + + @override + String get encumbrance => 'Encumbrance'; + + @override + String get plotDevelopment => 'Plot Development'; + + @override + String get quests => 'Quests'; + + @override + String get traitName => 'Name'; + + @override + String get traitRace => 'Race'; + + @override + String get traitClass => 'Class'; + + @override + String get traitLevel => 'Level'; + + @override + String get statStr => 'STR'; + + @override + String get statCon => 'CON'; + + @override + String get statDex => 'DEX'; + + @override + String get statInt => 'INT'; + + @override + String get statWis => 'WIS'; + + @override + String get statCha => 'CHA'; + + @override + String get statHpMax => 'HP Max'; + + @override + String get statMpMax => 'MP Max'; + + @override + String get equipWeapon => 'Weapon'; + + @override + String get equipShield => 'Shield'; + + @override + String get equipHelm => 'Helm'; + + @override + String get equipHauberk => 'Hauberk'; + + @override + String get equipBrassairts => 'Brassairts'; + + @override + String get equipVambraces => 'Vambraces'; + + @override + String get equipGauntlets => 'Gauntlets'; + + @override + String get equipGambeson => 'Gambeson'; + + @override + String get equipCuisses => 'Cuisses'; + + @override + String get equipGreaves => 'Greaves'; + + @override + String get equipSollerets => 'Sollerets'; + + @override + String get gold => 'Gold'; + + @override + String goldAmount(int amount) { + return 'Gold: $amount'; + } + + @override + String get prologue => 'Prologue'; + + @override + String actNumber(String number) { + return 'Act $number'; + } + + @override + String get noActiveQuests => 'No active quests'; + + @override + String questNumber(int number) { + return 'Quest #$number'; + } + + @override + String get welcomeMessage => 'Welcome to Progress Quest!'; + + @override + String get noSavedGames => 'No saved games found.'; + + @override + String loadError(String error) { + return 'Failed to load save file: $error'; + } + + @override + String get name => 'Name'; + + @override + String get generateName => 'Generate Name'; + + @override + String get total => 'Total'; + + @override + String get unroll => 'Unroll'; + + @override + String get roll => 'Roll'; + + @override + String get race => 'Race'; + + @override + String get classTitle => 'Class'; + + @override + String percentComplete(int percent) { + return '$percent% complete'; + } +} diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart new file mode 100644 index 0000000..ca8f88c --- /dev/null +++ b/lib/l10n/app_localizations_ja.dart @@ -0,0 +1,235 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Japanese (`ja`). +class L10nJa extends L10n { + L10nJa([String locale = 'ja']) : super(locale); + + @override + String get appTitle => 'Ascii Never Die'; + + @override + String get tagNoNetwork => 'No network'; + + @override + String get tagIdleRpg => 'Idle RPG loop'; + + @override + String get tagLocalSaves => 'Local saves'; + + @override + String get newCharacter => 'New character'; + + @override + String get loadSave => 'Load save'; + + @override + String get loadGame => 'Load Game'; + + @override + String get viewBuildPlan => 'View build plan'; + + @override + String get buildRoadmap => 'Build roadmap'; + + @override + String get techStack => 'Tech stack'; + + @override + String get cancel => 'Cancel'; + + @override + String get exitGame => 'Exit Game'; + + @override + String get saveProgressQuestion => 'Save your progress before leaving?'; + + @override + String get exitWithoutSaving => 'Exit without saving'; + + @override + String get saveAndExit => 'Save and Exit'; + + @override + String progressQuestTitle(String name) { + return 'Progress Quest - $name'; + } + + @override + String get levelUp => 'Level Up'; + + @override + String get completeQuest => 'Complete Quest'; + + @override + String get completePlot => 'Complete Plot'; + + @override + String get characterSheet => 'Character Sheet'; + + @override + String get traits => 'Traits'; + + @override + String get stats => 'Stats'; + + @override + String get experience => 'Experience'; + + @override + String get xpNeededForNextLevel => 'XP needed for next level'; + + @override + String get spellBook => 'Spell Book'; + + @override + String get noSpellsYet => 'No spells yet'; + + @override + String get equipment => 'Equipment'; + + @override + String get inventory => 'Inventory'; + + @override + String get encumbrance => 'Encumbrance'; + + @override + String get plotDevelopment => 'Plot Development'; + + @override + String get quests => 'Quests'; + + @override + String get traitName => 'Name'; + + @override + String get traitRace => 'Race'; + + @override + String get traitClass => 'Class'; + + @override + String get traitLevel => 'Level'; + + @override + String get statStr => 'STR'; + + @override + String get statCon => 'CON'; + + @override + String get statDex => 'DEX'; + + @override + String get statInt => 'INT'; + + @override + String get statWis => 'WIS'; + + @override + String get statCha => 'CHA'; + + @override + String get statHpMax => 'HP Max'; + + @override + String get statMpMax => 'MP Max'; + + @override + String get equipWeapon => 'Weapon'; + + @override + String get equipShield => 'Shield'; + + @override + String get equipHelm => 'Helm'; + + @override + String get equipHauberk => 'Hauberk'; + + @override + String get equipBrassairts => 'Brassairts'; + + @override + String get equipVambraces => 'Vambraces'; + + @override + String get equipGauntlets => 'Gauntlets'; + + @override + String get equipGambeson => 'Gambeson'; + + @override + String get equipCuisses => 'Cuisses'; + + @override + String get equipGreaves => 'Greaves'; + + @override + String get equipSollerets => 'Sollerets'; + + @override + String get gold => 'Gold'; + + @override + String goldAmount(int amount) { + return 'Gold: $amount'; + } + + @override + String get prologue => 'Prologue'; + + @override + String actNumber(String number) { + return 'Act $number'; + } + + @override + String get noActiveQuests => 'No active quests'; + + @override + String questNumber(int number) { + return 'Quest #$number'; + } + + @override + String get welcomeMessage => 'Welcome to Progress Quest!'; + + @override + String get noSavedGames => 'No saved games found.'; + + @override + String loadError(String error) { + return 'Failed to load save file: $error'; + } + + @override + String get name => 'Name'; + + @override + String get generateName => 'Generate Name'; + + @override + String get total => 'Total'; + + @override + String get unroll => 'Unroll'; + + @override + String get roll => 'Roll'; + + @override + String get race => 'Race'; + + @override + String get classTitle => 'Class'; + + @override + String percentComplete(int percent) { + return '$percent% complete'; + } +} diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart new file mode 100644 index 0000000..dbec8b5 --- /dev/null +++ b/lib/l10n/app_localizations_ko.dart @@ -0,0 +1,235 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Korean (`ko`). +class L10nKo extends L10n { + L10nKo([String locale = 'ko']) : super(locale); + + @override + String get appTitle => 'Ascii Never Die'; + + @override + String get tagNoNetwork => 'No network'; + + @override + String get tagIdleRpg => 'Idle RPG loop'; + + @override + String get tagLocalSaves => 'Local saves'; + + @override + String get newCharacter => 'New character'; + + @override + String get loadSave => 'Load save'; + + @override + String get loadGame => 'Load Game'; + + @override + String get viewBuildPlan => 'View build plan'; + + @override + String get buildRoadmap => 'Build roadmap'; + + @override + String get techStack => 'Tech stack'; + + @override + String get cancel => 'Cancel'; + + @override + String get exitGame => 'Exit Game'; + + @override + String get saveProgressQuestion => 'Save your progress before leaving?'; + + @override + String get exitWithoutSaving => 'Exit without saving'; + + @override + String get saveAndExit => 'Save and Exit'; + + @override + String progressQuestTitle(String name) { + return 'Progress Quest - $name'; + } + + @override + String get levelUp => 'Level Up'; + + @override + String get completeQuest => 'Complete Quest'; + + @override + String get completePlot => 'Complete Plot'; + + @override + String get characterSheet => 'Character Sheet'; + + @override + String get traits => 'Traits'; + + @override + String get stats => 'Stats'; + + @override + String get experience => 'Experience'; + + @override + String get xpNeededForNextLevel => 'XP needed for next level'; + + @override + String get spellBook => 'Spell Book'; + + @override + String get noSpellsYet => 'No spells yet'; + + @override + String get equipment => 'Equipment'; + + @override + String get inventory => 'Inventory'; + + @override + String get encumbrance => 'Encumbrance'; + + @override + String get plotDevelopment => 'Plot Development'; + + @override + String get quests => 'Quests'; + + @override + String get traitName => 'Name'; + + @override + String get traitRace => 'Race'; + + @override + String get traitClass => 'Class'; + + @override + String get traitLevel => 'Level'; + + @override + String get statStr => 'STR'; + + @override + String get statCon => 'CON'; + + @override + String get statDex => 'DEX'; + + @override + String get statInt => 'INT'; + + @override + String get statWis => 'WIS'; + + @override + String get statCha => 'CHA'; + + @override + String get statHpMax => 'HP Max'; + + @override + String get statMpMax => 'MP Max'; + + @override + String get equipWeapon => 'Weapon'; + + @override + String get equipShield => 'Shield'; + + @override + String get equipHelm => 'Helm'; + + @override + String get equipHauberk => 'Hauberk'; + + @override + String get equipBrassairts => 'Brassairts'; + + @override + String get equipVambraces => 'Vambraces'; + + @override + String get equipGauntlets => 'Gauntlets'; + + @override + String get equipGambeson => 'Gambeson'; + + @override + String get equipCuisses => 'Cuisses'; + + @override + String get equipGreaves => 'Greaves'; + + @override + String get equipSollerets => 'Sollerets'; + + @override + String get gold => 'Gold'; + + @override + String goldAmount(int amount) { + return 'Gold: $amount'; + } + + @override + String get prologue => 'Prologue'; + + @override + String actNumber(String number) { + return 'Act $number'; + } + + @override + String get noActiveQuests => 'No active quests'; + + @override + String questNumber(int number) { + return 'Quest #$number'; + } + + @override + String get welcomeMessage => 'Welcome to Progress Quest!'; + + @override + String get noSavedGames => 'No saved games found.'; + + @override + String loadError(String error) { + return 'Failed to load save file: $error'; + } + + @override + String get name => 'Name'; + + @override + String get generateName => 'Generate Name'; + + @override + String get total => 'Total'; + + @override + String get unroll => 'Unroll'; + + @override + String get roll => 'Roll'; + + @override + String get race => 'Race'; + + @override + String get classTitle => 'Class'; + + @override + String percentComplete(int percent) { + return '$percent% complete'; + } +} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..7b18bcb --- /dev/null +++ b/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,235 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class L10nZh extends L10n { + L10nZh([String locale = 'zh']) : super(locale); + + @override + String get appTitle => 'Ascii Never Die'; + + @override + String get tagNoNetwork => 'No network'; + + @override + String get tagIdleRpg => 'Idle RPG loop'; + + @override + String get tagLocalSaves => 'Local saves'; + + @override + String get newCharacter => 'New character'; + + @override + String get loadSave => 'Load save'; + + @override + String get loadGame => 'Load Game'; + + @override + String get viewBuildPlan => 'View build plan'; + + @override + String get buildRoadmap => 'Build roadmap'; + + @override + String get techStack => 'Tech stack'; + + @override + String get cancel => 'Cancel'; + + @override + String get exitGame => 'Exit Game'; + + @override + String get saveProgressQuestion => 'Save your progress before leaving?'; + + @override + String get exitWithoutSaving => 'Exit without saving'; + + @override + String get saveAndExit => 'Save and Exit'; + + @override + String progressQuestTitle(String name) { + return 'Progress Quest - $name'; + } + + @override + String get levelUp => 'Level Up'; + + @override + String get completeQuest => 'Complete Quest'; + + @override + String get completePlot => 'Complete Plot'; + + @override + String get characterSheet => 'Character Sheet'; + + @override + String get traits => 'Traits'; + + @override + String get stats => 'Stats'; + + @override + String get experience => 'Experience'; + + @override + String get xpNeededForNextLevel => 'XP needed for next level'; + + @override + String get spellBook => 'Spell Book'; + + @override + String get noSpellsYet => 'No spells yet'; + + @override + String get equipment => 'Equipment'; + + @override + String get inventory => 'Inventory'; + + @override + String get encumbrance => 'Encumbrance'; + + @override + String get plotDevelopment => 'Plot Development'; + + @override + String get quests => 'Quests'; + + @override + String get traitName => 'Name'; + + @override + String get traitRace => 'Race'; + + @override + String get traitClass => 'Class'; + + @override + String get traitLevel => 'Level'; + + @override + String get statStr => 'STR'; + + @override + String get statCon => 'CON'; + + @override + String get statDex => 'DEX'; + + @override + String get statInt => 'INT'; + + @override + String get statWis => 'WIS'; + + @override + String get statCha => 'CHA'; + + @override + String get statHpMax => 'HP Max'; + + @override + String get statMpMax => 'MP Max'; + + @override + String get equipWeapon => 'Weapon'; + + @override + String get equipShield => 'Shield'; + + @override + String get equipHelm => 'Helm'; + + @override + String get equipHauberk => 'Hauberk'; + + @override + String get equipBrassairts => 'Brassairts'; + + @override + String get equipVambraces => 'Vambraces'; + + @override + String get equipGauntlets => 'Gauntlets'; + + @override + String get equipGambeson => 'Gambeson'; + + @override + String get equipCuisses => 'Cuisses'; + + @override + String get equipGreaves => 'Greaves'; + + @override + String get equipSollerets => 'Sollerets'; + + @override + String get gold => 'Gold'; + + @override + String goldAmount(int amount) { + return 'Gold: $amount'; + } + + @override + String get prologue => 'Prologue'; + + @override + String actNumber(String number) { + return 'Act $number'; + } + + @override + String get noActiveQuests => 'No active quests'; + + @override + String questNumber(int number) { + return 'Quest #$number'; + } + + @override + String get welcomeMessage => 'Welcome to Progress Quest!'; + + @override + String get noSavedGames => 'No saved games found.'; + + @override + String loadError(String error) { + return 'Failed to load save file: $error'; + } + + @override + String get name => 'Name'; + + @override + String get generateName => 'Generate Name'; + + @override + String get total => 'Total'; + + @override + String get unroll => 'Unroll'; + + @override + String get roll => 'Roll'; + + @override + String get race => 'Race'; + + @override + String get classTitle => 'Class'; + + @override + String percentComplete(int percent) { + return '$percent% complete'; + } +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..e860a48 --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,75 @@ +{ + "@@locale": "zh", + + "appTitle": "Ascii Never Die", + "tagNoNetwork": "No network", + "tagIdleRpg": "Idle RPG loop", + "tagLocalSaves": "Local saves", + "newCharacter": "New character", + "loadSave": "Load save", + "loadGame": "Load Game", + "viewBuildPlan": "View build plan", + "buildRoadmap": "Build roadmap", + "techStack": "Tech stack", + "cancel": "Cancel", + "exitGame": "Exit Game", + "saveProgressQuestion": "Save your progress before leaving?", + "exitWithoutSaving": "Exit without saving", + "saveAndExit": "Save and Exit", + "progressQuestTitle": "Progress Quest - {name}", + "levelUp": "Level Up", + "completeQuest": "Complete Quest", + "completePlot": "Complete Plot", + "characterSheet": "Character Sheet", + "traits": "Traits", + "stats": "Stats", + "experience": "Experience", + "xpNeededForNextLevel": "XP needed for next level", + "spellBook": "Spell Book", + "noSpellsYet": "No spells yet", + "equipment": "Equipment", + "inventory": "Inventory", + "encumbrance": "Encumbrance", + "plotDevelopment": "Plot Development", + "quests": "Quests", + "traitName": "Name", + "traitRace": "Race", + "traitClass": "Class", + "traitLevel": "Level", + "statStr": "STR", + "statCon": "CON", + "statDex": "DEX", + "statInt": "INT", + "statWis": "WIS", + "statCha": "CHA", + "statHpMax": "HP Max", + "statMpMax": "MP Max", + "equipWeapon": "Weapon", + "equipShield": "Shield", + "equipHelm": "Helm", + "equipHauberk": "Hauberk", + "equipBrassairts": "Brassairts", + "equipVambraces": "Vambraces", + "equipGauntlets": "Gauntlets", + "equipGambeson": "Gambeson", + "equipCuisses": "Cuisses", + "equipGreaves": "Greaves", + "equipSollerets": "Sollerets", + "gold": "Gold", + "goldAmount": "Gold: {amount}", + "prologue": "Prologue", + "actNumber": "Act {number}", + "noActiveQuests": "No active quests", + "questNumber": "Quest #{number}", + "welcomeMessage": "Welcome to Progress Quest!", + "noSavedGames": "No saved games found.", + "loadError": "Failed to load save file: {error}", + "name": "Name", + "generateName": "Generate Name", + "total": "Total", + "unroll": "Unroll", + "roll": "Roll", + "race": "Race", + "classTitle": "Class", + "percentComplete": "{percent}% complete" +} diff --git a/lib/src/app.dart b/lib/src/app.dart index d44bbdb..348a35c 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/engine/game_mutations.dart'; import 'package:askiineverdie/src/core/engine/progress_service.dart'; import 'package:askiineverdie/src/core/engine/reward_service.dart'; @@ -51,6 +52,8 @@ class _AskiiNeverDieAppState extends State { return MaterialApp( title: 'Ascii Never Die', debugShowCheckedModeBanner: false, + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF234361)), scaffoldBackgroundColor: const Color(0xFFF4F5F7), @@ -85,9 +88,9 @@ class _AskiiNeverDieAppState extends State { if (saves.isEmpty) { // 저장 파일이 없으면 안내 메시지 - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('저장된 게임이 없습니다.'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context).noSavedGames)), + ); return; } else if (saves.length == 1) { // 파일이 하나면 바로 선택 @@ -114,7 +117,7 @@ class _AskiiNeverDieAppState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '저장 파일을 불러올 수 없습니다: ${_controller.error ?? "알 수 없는 오류"}', + L10n.of(context).loadError(_controller.error ?? 'Unknown error'), ), ), ); diff --git a/lib/src/features/front/front_screen.dart b/lib/src/features/front/front_screen.dart index 84babfd..08d17d1 100644 --- a/lib/src/features/front/front_screen.dart +++ b/lib/src/features/front/front_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/l10n/app_localizations.dart'; + class FrontScreen extends StatelessWidget { const FrontScreen({super.key, this.onNewCharacter, this.onLoadSave}); @@ -107,7 +109,7 @@ class _HeroHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Ascii Never Die', + L10n.of(context).appTitle, style: theme.textTheme.headlineSmall?.copyWith( color: colorScheme.onPrimary, fontWeight: FontWeight.w700, @@ -126,14 +128,19 @@ class _HeroHeader extends StatelessWidget { ], ), const SizedBox(height: 14), - Wrap( - spacing: 8, - runSpacing: 8, - children: const [ - _Tag(icon: Icons.cloud_off_outlined, label: 'No network'), - _Tag(icon: Icons.timer_outlined, label: 'Idle RPG loop'), - _Tag(icon: Icons.storage_rounded, label: 'Local saves'), - ], + Builder( + builder: (context) { + final l10n = L10n.of(context); + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _Tag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork), + _Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg), + _Tag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves), + ], + ); + }, ), ], ), @@ -151,6 +158,7 @@ class _ActionRow extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = L10n.of(context); return Wrap( spacing: 12, @@ -159,7 +167,7 @@ class _ActionRow extends StatelessWidget { FilledButton.icon( onPressed: onNewCharacter, icon: const Icon(Icons.casino_outlined), - label: const Text('New character'), + label: Text(l10n.newCharacter), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), textStyle: theme.textTheme.titleMedium, @@ -168,7 +176,7 @@ class _ActionRow extends StatelessWidget { OutlinedButton.icon( onPressed: onLoadSave, icon: const Icon(Icons.folder_open), - label: const Text('Load save'), + label: Text(l10n.loadSave), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), textStyle: theme.textTheme.titleMedium, @@ -177,7 +185,7 @@ class _ActionRow extends StatelessWidget { TextButton.icon( onPressed: () => _showPlaceholder(context), icon: const Icon(Icons.menu_book_outlined), - label: const Text('View build plan'), + label: Text(l10n.viewBuildPlan), ), ], ); @@ -189,11 +197,12 @@ class _StatusCards extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = L10n.of(context); return Column( - children: const [ + children: [ _InfoCard( icon: Icons.route_outlined, - title: 'Build roadmap', + title: l10n.buildRoadmap, points: [ 'Port PQ 6.4 data set (Config.dfm) into Dart constants.', 'Recreate quest/task loop with deterministic RNG + saves.', @@ -203,7 +212,7 @@ class _StatusCards extends StatelessWidget { SizedBox(height: 16), _InfoCard( icon: Icons.auto_fix_high_outlined, - title: 'Tech stack', + title: l10n.techStack, points: [ 'Flutter (Material 3) with multiplatform targets enabled.', 'path_provider + shared_preferences for local storage hooks.', diff --git a/lib/src/features/front/save_picker_dialog.dart b/lib/src/features/front/save_picker_dialog.dart index e2eacbf..820ef64 100644 --- a/lib/src/features/front/save_picker_dialog.dart +++ b/lib/src/features/front/save_picker_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/storage/save_service.dart' show SaveFileInfo; @@ -20,7 +21,7 @@ class SavePickerDialog extends StatelessWidget { // 저장 파일이 없으면 안내 메시지 ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('저장된 게임이 없습니다.'))); + ).showSnackBar(SnackBar(content: Text(L10n.of(context).noSavedGames))); return null; } @@ -35,12 +36,13 @@ class SavePickerDialog extends StatelessWidget { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final l10n = L10n.of(context); return AlertDialog( title: Row( children: [ Icon(Icons.folder_open, color: colorScheme.primary), const SizedBox(width: 12), - const Text('Load Game'), + Text(l10n.loadGame), ], ), content: SizedBox( @@ -64,7 +66,7 @@ class SavePickerDialog extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(null), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), ], ); diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index 38507eb..007aad4 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; @@ -129,21 +130,22 @@ class _GamePlayScreenState extends State /// 뒤로가기 시 저장 확인 다이얼로그 Future _onPopInvoked() async { + final l10n = L10n.of(context); final shouldPop = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Exit Game'), - content: const Text('Save your progress before leaving?'), + title: Text(l10n.exitGame), + content: Text(l10n.saveProgressQuestion), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, - child: const Text('Exit without saving'), + child: Text(l10n.exitWithoutSaving), ), FilledButton( onPressed: () async { @@ -152,7 +154,7 @@ class _GamePlayScreenState extends State Navigator.of(context).pop(true); } }, - child: const Text('Save and Exit'), + child: Text(l10n.saveAndExit), ), ], ), @@ -189,23 +191,23 @@ class _GamePlayScreenState extends State }, child: Scaffold( appBar: AppBar( - title: Text('Progress Quest - ${state.traits.name}'), + title: Text(L10n.of(context).progressQuestTitle(state.traits.name)), actions: [ // 치트 버튼 (디버그용) if (widget.controller.cheatsEnabled) ...[ IconButton( icon: const Text('L+1'), - tooltip: 'Level Up', + tooltip: L10n.of(context).levelUp, onPressed: () => widget.controller.loop?.cheatCompleteTask(), ), IconButton( icon: const Text('Q!'), - tooltip: 'Complete Quest', + tooltip: L10n.of(context).completeQuest, onPressed: () => widget.controller.loop?.cheatCompleteQuest(), ), IconButton( icon: const Text('P!'), - tooltip: 'Complete Plot', + tooltip: L10n.of(context).completePlot, onPressed: () => widget.controller.loop?.cheatCompletePlot(), ), ], @@ -250,34 +252,35 @@ class _GamePlayScreenState extends State /// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells) Widget _buildCharacterPanel(GameState state) { + final l10n = L10n.of(context); return Card( margin: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildPanelHeader('Character Sheet'), + _buildPanelHeader(l10n.characterSheet), // Traits 목록 - _buildSectionHeader('Traits'), + _buildSectionHeader(l10n.traits), _buildTraitsList(state), // Stats 목록 - _buildSectionHeader('Stats'), + _buildSectionHeader(l10n.stats), Expanded(flex: 2, child: _buildStatsList(state)), // Experience 바 - _buildSectionHeader('Experience'), + _buildSectionHeader(l10n.experience), _buildProgressBar( state.progress.exp.position, state.progress.exp.max, Colors.blue, tooltip: '${state.progress.exp.max - state.progress.exp.position} ' - 'XP needed for next level', + '${l10n.xpNeededForNextLevel}', ), // Spell Book - _buildSectionHeader('Spell Book'), + _buildSectionHeader(l10n.spellBook), Expanded(flex: 2, child: _buildSpellsList(state)), ], ), @@ -286,22 +289,23 @@ class _GamePlayScreenState extends State /// 중앙 패널: Equipment/Inventory Widget _buildEquipmentPanel(GameState state) { + final l10n = L10n.of(context); return Card( margin: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildPanelHeader('Equipment'), + _buildPanelHeader(l10n.equipment), // Equipment 목록 Expanded(flex: 2, child: _buildEquipmentList(state)), // Inventory - _buildPanelHeader('Inventory'), + _buildPanelHeader(l10n.inventory), Expanded(flex: 3, child: _buildInventoryList(state)), // Encumbrance 바 - _buildSectionHeader('Encumbrance'), + _buildSectionHeader(l10n.encumbrance), _buildProgressBar( state.progress.encumbrance.position, state.progress.encumbrance.max, @@ -314,12 +318,13 @@ class _GamePlayScreenState extends State /// 우측 패널: Plot/Quest Widget _buildQuestPanel(GameState state) { + final l10n = L10n.of(context); return Card( margin: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildPanelHeader('Plot Development'), + _buildPanelHeader(l10n.plotDevelopment), // Plot 목록 Expanded(child: _buildPlotList(state)), @@ -334,7 +339,7 @@ class _GamePlayScreenState extends State : null, ), - _buildPanelHeader('Quests'), + _buildPanelHeader(l10n.quests), // Quest 목록 Expanded(child: _buildQuestList(state)), @@ -345,7 +350,10 @@ class _GamePlayScreenState extends State state.progress.quest.max, Colors.green, tooltip: state.progress.quest.max > 0 - ? '${(100 * state.progress.quest.position ~/ state.progress.quest.max)}% complete' + ? l10n.percentComplete( + 100 * state.progress.quest.position ~/ + state.progress.quest.max, + ) : null, ), ], @@ -398,11 +406,12 @@ class _GamePlayScreenState extends State } Widget _buildTraitsList(GameState state) { + final l10n = L10n.of(context); final traits = [ - ('Name', state.traits.name), - ('Race', state.traits.race), - ('Class', state.traits.klass), - ('Level', '${state.traits.level}'), + (l10n.traitName, state.traits.name), + (l10n.traitRace, state.traits.race), + (l10n.traitClass, state.traits.klass), + (l10n.traitLevel, '${state.traits.level}'), ]; return Padding( @@ -433,15 +442,16 @@ class _GamePlayScreenState extends State } Widget _buildStatsList(GameState state) { + final l10n = L10n.of(context); final stats = [ - ('STR', state.stats.str), - ('CON', state.stats.con), - ('DEX', state.stats.dex), - ('INT', state.stats.intelligence), - ('WIS', state.stats.wis), - ('CHA', state.stats.cha), - ('HP Max', state.stats.hpMax), - ('MP Max', state.stats.mpMax), + (l10n.statStr, state.stats.str), + (l10n.statCon, state.stats.con), + (l10n.statDex, state.stats.dex), + (l10n.statInt, state.stats.intelligence), + (l10n.statWis, state.stats.wis), + (l10n.statCha, state.stats.cha), + (l10n.statHpMax, state.stats.hpMax), + (l10n.statMpMax, state.stats.mpMax), ]; return ListView.builder( @@ -467,8 +477,8 @@ class _GamePlayScreenState extends State Widget _buildSpellsList(GameState state) { if (state.spellBook.spells.isEmpty) { - return const Center( - child: Text('No spells yet', style: TextStyle(fontSize: 11)), + return Center( + child: Text(L10n.of(context).noSpellsYet, style: const TextStyle(fontSize: 11)), ); } @@ -498,18 +508,19 @@ class _GamePlayScreenState extends State Widget _buildEquipmentList(GameState state) { // 원본 Main.dfm Equips ListView - 11개 슬롯 + final l10n = L10n.of(context); final equipment = [ - ('Weapon', state.equipment.weapon), - ('Shield', state.equipment.shield), - ('Helm', state.equipment.helm), - ('Hauberk', state.equipment.hauberk), - ('Brassairts', state.equipment.brassairts), - ('Vambraces', state.equipment.vambraces), - ('Gauntlets', state.equipment.gauntlets), - ('Gambeson', state.equipment.gambeson), - ('Cuisses', state.equipment.cuisses), - ('Greaves', state.equipment.greaves), - ('Sollerets', state.equipment.sollerets), + (l10n.equipWeapon, state.equipment.weapon), + (l10n.equipShield, state.equipment.shield), + (l10n.equipHelm, state.equipment.helm), + (l10n.equipHauberk, state.equipment.hauberk), + (l10n.equipBrassairts, state.equipment.brassairts), + (l10n.equipVambraces, state.equipment.vambraces), + (l10n.equipGauntlets, state.equipment.gauntlets), + (l10n.equipGambeson, state.equipment.gambeson), + (l10n.equipCuisses, state.equipment.cuisses), + (l10n.equipGreaves, state.equipment.greaves), + (l10n.equipSollerets, state.equipment.sollerets), ]; return ListView.builder( @@ -543,10 +554,11 @@ class _GamePlayScreenState extends State } Widget _buildInventoryList(GameState state) { + final l10n = L10n.of(context); if (state.inventory.items.isEmpty) { return Center( child: Text( - 'Gold: ${state.inventory.gold}', + l10n.goldAmount(state.inventory.gold), style: const TextStyle(fontSize: 11), ), ); @@ -559,8 +571,8 @@ class _GamePlayScreenState extends State if (index == 0) { return Row( children: [ - const Expanded( - child: Text('Gold', style: TextStyle(fontSize: 11)), + Expanded( + child: Text(l10n.gold, style: const TextStyle(fontSize: 11)), ), Text( '${state.inventory.gold}', @@ -594,10 +606,11 @@ class _GamePlayScreenState extends State Widget _buildPlotList(GameState state) { // 플롯 단계를 표시 (Act I, Act II, ...) + final l10n = L10n.of(context); final plotCount = state.progress.plotStageCount; if (plotCount == 0) { - return const Center( - child: Text('Prologue', style: TextStyle(fontSize: 11)), + return Center( + child: Text(l10n.prologue, style: const TextStyle(fontSize: 11)), ); } @@ -615,7 +628,7 @@ class _GamePlayScreenState extends State ), const SizedBox(width: 4), Text( - index == 0 ? 'Prologue' : 'Act ${_toRoman(index)}', + index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)), style: TextStyle( fontSize: 11, decoration: isCompleted @@ -630,10 +643,11 @@ class _GamePlayScreenState extends State } Widget _buildQuestList(GameState state) { + final l10n = L10n.of(context); final questCount = state.progress.questCount; if (questCount == 0) { - return const Center( - child: Text('No active quests', style: TextStyle(fontSize: 11)), + return Center( + child: Text(l10n.noActiveQuests, style: const TextStyle(fontSize: 11)), ); } @@ -649,7 +663,7 @@ class _GamePlayScreenState extends State child: Text( currentTask.caption.isNotEmpty ? currentTask.caption - : 'Quest #$questCount', + : l10n.questNumber(questCount), style: const TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis, ), diff --git a/lib/src/features/game/widgets/task_progress_panel.dart b/lib/src/features/game/widgets/task_progress_panel.dart index 576063a..c43c82c 100644 --- a/lib/src/features/game/widgets/task_progress_panel.dart +++ b/lib/src/features/game/widgets/task_progress_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; @@ -60,7 +61,7 @@ class TaskProgressPanel extends StatelessWidget { child: Text( progress.currentTask.caption.isNotEmpty ? progress.currentTask.caption - : 'Welcome to Progress Quest!', + : L10n.of(context).welcomeMessage, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), diff --git a/lib/src/features/new_character/new_character_screen.dart b/lib/src/features/new_character/new_character_screen.dart index 1e13128..d85ebdf 100644 --- a/lib/src/features/new_character/new_character_screen.dart +++ b/lib/src/features/new_character/new_character_screen.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/pq_config.dart'; import 'package:askiineverdie/src/core/util/deterministic_random.dart'; @@ -241,6 +242,7 @@ class _NewCharacterScreenState extends State { } Widget _buildNameSection() { + final l10n = L10n.of(context); return Card( child: Padding( padding: const EdgeInsets.all(16), @@ -249,9 +251,9 @@ class _NewCharacterScreenState extends State { Expanded( child: TextField( controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: l10n.name, + border: const OutlineInputBorder(), ), maxLength: 30, ), @@ -260,7 +262,7 @@ class _NewCharacterScreenState extends State { IconButton.filled( onPressed: _onGenerateName, icon: const Icon(Icons.casino), - tooltip: 'Generate Name', + tooltip: l10n.generateName, ), ], ), @@ -269,29 +271,30 @@ class _NewCharacterScreenState extends State { } Widget _buildStatsSection() { + final l10n = L10n.of(context); return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Stats', style: Theme.of(context).textTheme.titleMedium), + Text(l10n.stats, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 12), // 스탯 그리드 Row( children: [ - Expanded(child: _buildStatTile('STR', _str)), - Expanded(child: _buildStatTile('CON', _con)), - Expanded(child: _buildStatTile('DEX', _dex)), + Expanded(child: _buildStatTile(l10n.statStr, _str)), + Expanded(child: _buildStatTile(l10n.statCon, _con)), + Expanded(child: _buildStatTile(l10n.statDex, _dex)), ], ), const SizedBox(height: 8), Row( children: [ - Expanded(child: _buildStatTile('INT', _int)), - Expanded(child: _buildStatTile('WIS', _wis)), - Expanded(child: _buildStatTile('CHA', _cha)), + Expanded(child: _buildStatTile(l10n.statInt, _int)), + Expanded(child: _buildStatTile(l10n.statWis, _wis)), + Expanded(child: _buildStatTile(l10n.statCha, _cha)), ], ), const SizedBox(height: 12), @@ -307,9 +310,9 @@ class _NewCharacterScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Total', - style: TextStyle(fontWeight: FontWeight.bold), + Text( + l10n.total, + style: const TextStyle(fontWeight: FontWeight.bold), ), Text( '$_total', @@ -333,7 +336,7 @@ class _NewCharacterScreenState extends State { OutlinedButton.icon( onPressed: _onUnroll, icon: const Icon(Icons.undo), - label: const Text('Unroll'), + label: Text(l10n.unroll), style: OutlinedButton.styleFrom( foregroundColor: _rollHistory.isEmpty ? Colors.grey : null, ), @@ -342,7 +345,7 @@ class _NewCharacterScreenState extends State { FilledButton.icon( onPressed: _onReroll, icon: const Icon(Icons.casino), - label: const Text('Roll'), + label: Text(l10n.roll), ), ], ), @@ -389,7 +392,7 @@ class _NewCharacterScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Race', style: Theme.of(context).textTheme.titleMedium), + Text(L10n.of(context).race, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), SizedBox( height: 300, @@ -434,7 +437,7 @@ class _NewCharacterScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Class', style: Theme.of(context).textTheme.titleMedium), + Text(L10n.of(context).classTitle, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), SizedBox( height: 300, diff --git a/pubspec.lock b/pubspec.lock index a2136e8..b6e8f67 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -86,6 +86,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -100,10 +105,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 25c7c40..44beb64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,9 +30,11 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.8 - intl: ^0.19.0 + intl: ^0.20.2 path_provider: ^2.1.4 shared_preferences: ^2.3.1 @@ -53,6 +55,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in diff --git a/test/features/game_play_screen_test.dart b/test/features/game_play_screen_test.dart index 0d4ce6f..5a9755c 100644 --- a/test/features/game_play_screen_test.dart +++ b/test/features/game_play_screen_test.dart @@ -1,3 +1,4 @@ +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/engine/game_mutations.dart'; import 'package:askiineverdie/src/core/engine/progress_service.dart'; import 'package:askiineverdie/src/core/engine/reward_service.dart'; @@ -11,6 +12,15 @@ import 'package:askiineverdie/src/features/game/game_session_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +/// 테스트용 MaterialApp 래퍼 (localization 포함) +Widget _buildTestApp(Widget child) { + return MaterialApp( + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + home: child, + ); +} + class _FakeSaveManager implements SaveManager { @override Future saveState(GameState state, {String? fileName}) async { @@ -85,11 +95,11 @@ void main() { await controller.startNew(_createTestState(), isNewGame: false); await tester.pumpWidget( - MaterialApp(home: GamePlayScreen(controller: controller)), + _buildTestApp(GamePlayScreen(controller: controller)), ); - // AppBar 타이틀 확인 - expect(find.text('Progress Quest - TestHero'), findsOneWidget); + // AppBar 타이틀 확인 (L10n 사용) + expect(find.textContaining('Progress Quest'), findsOneWidget); // 3패널 헤더 확인 expect(find.text('Character Sheet'), findsOneWidget); @@ -111,7 +121,7 @@ void main() { await controller.startNew(_createTestState(), isNewGame: false); await tester.pumpWidget( - MaterialApp(home: GamePlayScreen(controller: controller)), + _buildTestApp(GamePlayScreen(controller: controller)), ); // Traits 섹션 확인 @@ -133,7 +143,7 @@ void main() { await controller.startNew(_createTestState(), isNewGame: false); await tester.pumpWidget( - MaterialApp(home: GamePlayScreen(controller: controller)), + _buildTestApp(GamePlayScreen(controller: controller)), ); // Stats 섹션 확인 @@ -154,7 +164,7 @@ void main() { await controller.startNew(_createTestState(), isNewGame: false); await tester.pumpWidget( - MaterialApp(home: GamePlayScreen(controller: controller)), + _buildTestApp(GamePlayScreen(controller: controller)), ); // 현재 태스크 캡션 확인 (퀘스트 목록과 하단 패널에 표시됨) @@ -173,7 +183,7 @@ void main() { await controller.startNew(_createTestState(), isNewGame: false); await tester.pumpWidget( - MaterialApp(home: GamePlayScreen(controller: controller)), + _buildTestApp(GamePlayScreen(controller: controller)), ); // LinearProgressIndicator가 여러 개 표시되는지 확인 @@ -190,7 +200,7 @@ void main() { // 상태 없이 시작 (startNew 호출 안 함) await tester.pumpWidget( - MaterialApp(home: GamePlayScreen(controller: controller)), + _buildTestApp(GamePlayScreen(controller: controller)), ); // 로딩 인디케이터 확인 diff --git a/test/features/new_character_screen_test.dart b/test/features/new_character_screen_test.dart index 3503dd8..6e67a28 100644 --- a/test/features/new_character_screen_test.dart +++ b/test/features/new_character_screen_test.dart @@ -1,12 +1,22 @@ +import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/features/new_character/new_character_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +/// 테스트용 MaterialApp 래퍼 (localization 포함) +Widget _buildTestApp(Widget child) { + return MaterialApp( + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + home: child, + ); +} + void main() { testWidgets('NewCharacterScreen renders main sections', (tester) async { await tester.pumpWidget( - MaterialApp(home: NewCharacterScreen(onCharacterCreated: (_) {})), + _buildTestApp(NewCharacterScreen(onCharacterCreated: (_) {})), ); // 화면 타이틀 확인 @@ -29,7 +39,7 @@ void main() { testWidgets('Unroll button exists and can be tapped', (tester) async { await tester.pumpWidget( - MaterialApp(home: NewCharacterScreen(onCharacterCreated: (_) {})), + _buildTestApp(NewCharacterScreen(onCharacterCreated: (_) {})), ); // Unroll 버튼 확인 @@ -50,8 +60,8 @@ void main() { GameState? createdState; await tester.pumpWidget( - MaterialApp( - home: NewCharacterScreen( + _buildTestApp( + NewCharacterScreen( onCharacterCreated: (state) { createdState = state; }, @@ -81,7 +91,7 @@ void main() { testWidgets('Stats section displays all six stats', (tester) async { await tester.pumpWidget( - MaterialApp(home: NewCharacterScreen(onCharacterCreated: (_) {})), + _buildTestApp(NewCharacterScreen(onCharacterCreated: (_) {})), ); // 능력치 라벨들이 표시되는지 확인 @@ -98,7 +108,7 @@ void main() { testWidgets('Name text field exists', (tester) async { await tester.pumpWidget( - MaterialApp(home: NewCharacterScreen(onCharacterCreated: (_) {})), + _buildTestApp(NewCharacterScreen(onCharacterCreated: (_) {})), ); // TextField 확인 (이름 입력 필드)