feat(l10n): 국제화(L10n) 시스템 도입 및 하드코딩 텍스트 변환

- flutter_localizations 및 intl 패키지 추가
- l10n.yaml 설정 파일 및 app_ko.arb 메시지 파일 생성
- 모든 화면(app, front, game_play, new_character, save_picker)의 하드코딩 텍스트를 L10n 키로 변환
- 테스트 파일에 localizationsDelegates 추가하여 L10n 지원
This commit is contained in:
JiWoong Sul
2025-12-11 17:50:34 +09:00
parent 2b10deba5d
commit 35e3d92316
20 changed files with 2155 additions and 113 deletions

View File

@@ -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<GamePlayScreen>
/// 뒤로가기 시 저장 확인 다이얼로그
Future<bool> _onPopInvoked() async {
final l10n = L10n.of(context);
final shouldPop = await showDialog<bool>(
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<GamePlayScreen>
Navigator.of(context).pop(true);
}
},
child: const Text('Save and Exit'),
child: Text(l10n.saveAndExit),
),
],
),
@@ -189,23 +191,23 @@ class _GamePlayScreenState extends State<GamePlayScreen>
},
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<GamePlayScreen>
/// 좌측 패널: 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<GamePlayScreen>
/// 중앙 패널: 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<GamePlayScreen>
/// 우측 패널: 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<GamePlayScreen>
: null,
),
_buildPanelHeader('Quests'),
_buildPanelHeader(l10n.quests),
// Quest 목록
Expanded(child: _buildQuestList(state)),
@@ -345,7 +350,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
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<GamePlayScreen>
}
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<GamePlayScreen>
}
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<GamePlayScreen>
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<GamePlayScreen>
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<GamePlayScreen>
}
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<GamePlayScreen>
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<GamePlayScreen>
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<GamePlayScreen>
),
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<GamePlayScreen>
}
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<GamePlayScreen>
child: Text(
currentTask.caption.isNotEmpty
? currentTask.caption
: 'Quest #$questCount',
: l10n.questNumber(questCount),
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),