feat(i18n): 다국어 번역 확장 및 UI 개선

- 영어/일본어/한국어/중국어 번역 추가
- VictoryOverlay 레이아웃 개선
- NewCharacterScreen 기능 추가
This commit is contained in:
JiWoong Sul
2026-01-05 15:44:54 +09:00
parent afbd4e6853
commit 9ecf9d1692
11 changed files with 437 additions and 25 deletions

View File

@@ -251,5 +251,58 @@
"@newCharacterTitle": { "description": "New character screen title" },
"soldButton": "Sold!",
"@soldButton": { "description": "Confirm character creation button" }
"@soldButton": { "description": "Confirm character creation button" },
"endingCongratulations": "★ CONGRATULATIONS ★",
"@endingCongratulations": { "description": "Victory overlay congratulations" },
"endingGameComplete": "You have completed the game!",
"@endingGameComplete": { "description": "Game completion message" },
"endingTheHero": "THE HERO",
"@endingTheHero": { "description": "Hero section title" },
"endingLevelFormat": "Level {level}",
"@endingLevelFormat": {
"description": "Level display format",
"placeholders": {
"level": { "type": "int" }
}
},
"endingJourneyStats": "JOURNEY STATISTICS",
"@endingJourneyStats": { "description": "Journey statistics section title" },
"endingMonstersSlain": "Monsters Slain",
"@endingMonstersSlain": { "description": "Monsters killed stat label" },
"endingQuestsCompleted": "Quests Completed",
"@endingQuestsCompleted": { "description": "Quests completed stat label" },
"endingPlayTime": "Play Time",
"@endingPlayTime": { "description": "Play time stat label" },
"endingFinalStats": "FINAL STATS",
"@endingFinalStats": { "description": "Final stats section title" },
"endingCredits": "CREDITS",
"@endingCredits": { "description": "Credits section title" },
"endingThankYou": "Thank you for playing!",
"@endingThankYou": { "description": "Thank you message" },
"endingLegendLivesOn": "Your legend lives on...",
"@endingLegendLivesOn": { "description": "Legend message" },
"endingHallOfFameLine1": "Your heroic deeds will be",
"@endingHallOfFameLine1": { "description": "Hall of fame message line 1" },
"endingHallOfFameLine2": "remembered in the Hall of Fame",
"@endingHallOfFameLine2": { "description": "Hall of fame message line 2" },
"endingSkip": "SKIP",
"@endingSkip": { "description": "Skip button" },
"endingTapToSkip": "TAP TO SKIP",
"@endingTapToSkip": { "description": "Tap to skip hint" }
}

View File

@@ -74,5 +74,22 @@
"classTitle": "Class",
"percentComplete": "{percent}% complete",
"newCharacterTitle": "ASCII NEVER DIE - New Character",
"soldButton": "Sold!"
"soldButton": "Sold!",
"endingCongratulations": "★ おめでとうございます ★",
"endingGameComplete": "ゲームをクリアしました!",
"endingTheHero": "英雄",
"endingLevelFormat": "レベル {level}",
"endingJourneyStats": "冒険の記録",
"endingMonstersSlain": "倒したモンスター",
"endingQuestsCompleted": "完了したクエスト",
"endingPlayTime": "プレイ時間",
"endingFinalStats": "最終ステータス",
"endingCredits": "クレジット",
"endingThankYou": "プレイしていただきありがとうございます!",
"endingLegendLivesOn": "あなたの伝説は続く...",
"endingHallOfFameLine1": "あなたの英雄的な功績は",
"endingHallOfFameLine2": "殿堂に記録されます",
"endingSkip": "スキップ",
"endingTapToSkip": "タップでスキップ"
}

View File

@@ -74,5 +74,22 @@
"classTitle": "직업",
"percentComplete": "{percent}% 완료",
"newCharacterTitle": "아스키 네버 다이 - 새 캐릭터",
"soldButton": "확인!"
"soldButton": "확인!",
"endingCongratulations": "★ 축하합니다 ★",
"endingGameComplete": "게임을 클리어하셨습니다!",
"endingTheHero": "영웅",
"endingLevelFormat": "레벨 {level}",
"endingJourneyStats": "여정 통계",
"endingMonstersSlain": "처치한 몬스터",
"endingQuestsCompleted": "완료한 퀘스트",
"endingPlayTime": "플레이 시간",
"endingFinalStats": "최종 능력치",
"endingCredits": "크레딧",
"endingThankYou": "플레이해 주셔서 감사합니다!",
"endingLegendLivesOn": "당신의 전설은 계속됩니다...",
"endingHallOfFameLine1": "당신의 영웅적인 업적이",
"endingHallOfFameLine2": "명예의 전당에 기록됩니다",
"endingSkip": "건너뛰기",
"endingTapToSkip": "탭하여 건너뛰기"
}

View File

@@ -544,6 +544,102 @@ abstract class L10n {
/// In en, this message translates to:
/// **'Sold!'**
String get soldButton;
/// Victory overlay congratulations
///
/// In en, this message translates to:
/// **'★ CONGRATULATIONS ★'**
String get endingCongratulations;
/// Game completion message
///
/// In en, this message translates to:
/// **'You have completed the game!'**
String get endingGameComplete;
/// Hero section title
///
/// In en, this message translates to:
/// **'THE HERO'**
String get endingTheHero;
/// Level display format
///
/// In en, this message translates to:
/// **'Level {level}'**
String endingLevelFormat(int level);
/// Journey statistics section title
///
/// In en, this message translates to:
/// **'JOURNEY STATISTICS'**
String get endingJourneyStats;
/// Monsters killed stat label
///
/// In en, this message translates to:
/// **'Monsters Slain'**
String get endingMonstersSlain;
/// Quests completed stat label
///
/// In en, this message translates to:
/// **'Quests Completed'**
String get endingQuestsCompleted;
/// Play time stat label
///
/// In en, this message translates to:
/// **'Play Time'**
String get endingPlayTime;
/// Final stats section title
///
/// In en, this message translates to:
/// **'FINAL STATS'**
String get endingFinalStats;
/// Credits section title
///
/// In en, this message translates to:
/// **'CREDITS'**
String get endingCredits;
/// Thank you message
///
/// In en, this message translates to:
/// **'Thank you for playing!'**
String get endingThankYou;
/// Legend message
///
/// In en, this message translates to:
/// **'Your legend lives on...'**
String get endingLegendLivesOn;
/// Hall of fame message line 1
///
/// In en, this message translates to:
/// **'Your heroic deeds will be'**
String get endingHallOfFameLine1;
/// Hall of fame message line 2
///
/// In en, this message translates to:
/// **'remembered in the Hall of Fame'**
String get endingHallOfFameLine2;
/// Skip button
///
/// In en, this message translates to:
/// **'SKIP'**
String get endingSkip;
/// Tap to skip hint
///
/// In en, this message translates to:
/// **'TAP TO SKIP'**
String get endingTapToSkip;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {

View File

@@ -241,4 +241,54 @@ class L10nEn extends L10n {
@override
String get soldButton => 'Sold!';
@override
String get endingCongratulations => '★ CONGRATULATIONS ★';
@override
String get endingGameComplete => 'You have completed the game!';
@override
String get endingTheHero => 'THE HERO';
@override
String endingLevelFormat(int level) {
return 'Level $level';
}
@override
String get endingJourneyStats => 'JOURNEY STATISTICS';
@override
String get endingMonstersSlain => 'Monsters Slain';
@override
String get endingQuestsCompleted => 'Quests Completed';
@override
String get endingPlayTime => 'Play Time';
@override
String get endingFinalStats => 'FINAL STATS';
@override
String get endingCredits => 'CREDITS';
@override
String get endingThankYou => 'Thank you for playing!';
@override
String get endingLegendLivesOn => 'Your legend lives on...';
@override
String get endingHallOfFameLine1 => 'Your heroic deeds will be';
@override
String get endingHallOfFameLine2 => 'remembered in the Hall of Fame';
@override
String get endingSkip => 'SKIP';
@override
String get endingTapToSkip => 'TAP TO SKIP';
}

View File

@@ -241,4 +241,54 @@ class L10nJa extends L10n {
@override
String get soldButton => 'Sold!';
@override
String get endingCongratulations => '★ おめでとうございます ★';
@override
String get endingGameComplete => 'ゲームをクリアしました!';
@override
String get endingTheHero => '英雄';
@override
String endingLevelFormat(int level) {
return 'レベル $level';
}
@override
String get endingJourneyStats => '冒険の記録';
@override
String get endingMonstersSlain => '倒したモンスター';
@override
String get endingQuestsCompleted => '完了したクエスト';
@override
String get endingPlayTime => 'プレイ時間';
@override
String get endingFinalStats => '最終ステータス';
@override
String get endingCredits => 'クレジット';
@override
String get endingThankYou => 'プレイしていただきありがとうございます!';
@override
String get endingLegendLivesOn => 'あなたの伝説は続く...';
@override
String get endingHallOfFameLine1 => 'あなたの英雄的な功績は';
@override
String get endingHallOfFameLine2 => '殿堂に記録されます';
@override
String get endingSkip => 'スキップ';
@override
String get endingTapToSkip => 'タップでスキップ';
}

View File

@@ -241,4 +241,54 @@ class L10nKo extends L10n {
@override
String get soldButton => '확인!';
@override
String get endingCongratulations => '★ 축하합니다 ★';
@override
String get endingGameComplete => '게임을 클리어하셨습니다!';
@override
String get endingTheHero => '영웅';
@override
String endingLevelFormat(int level) {
return '레벨 $level';
}
@override
String get endingJourneyStats => '여정 통계';
@override
String get endingMonstersSlain => '처치한 몬스터';
@override
String get endingQuestsCompleted => '완료한 퀘스트';
@override
String get endingPlayTime => '플레이 시간';
@override
String get endingFinalStats => '최종 능력치';
@override
String get endingCredits => '크레딧';
@override
String get endingThankYou => '플레이해 주셔서 감사합니다!';
@override
String get endingLegendLivesOn => '당신의 전설은 계속됩니다...';
@override
String get endingHallOfFameLine1 => '당신의 영웅적인 업적이';
@override
String get endingHallOfFameLine2 => '명예의 전당에 기록됩니다';
@override
String get endingSkip => '건너뛰기';
@override
String get endingTapToSkip => '탭하여 건너뛰기';
}

View File

@@ -241,4 +241,54 @@ class L10nZh extends L10n {
@override
String get soldButton => 'Sold!';
@override
String get endingCongratulations => '★ 恭喜通关 ★';
@override
String get endingGameComplete => '您已完成游戏!';
@override
String get endingTheHero => '英雄';
@override
String endingLevelFormat(int level) {
return '等级 $level';
}
@override
String get endingJourneyStats => '冒险记录';
@override
String get endingMonstersSlain => '击败的怪物';
@override
String get endingQuestsCompleted => '完成的任务';
@override
String get endingPlayTime => '游戏时间';
@override
String get endingFinalStats => '最终属性';
@override
String get endingCredits => '制作人员';
@override
String get endingThankYou => '感谢您的游玩!';
@override
String get endingLegendLivesOn => '您的传奇将永远流传...';
@override
String get endingHallOfFameLine1 => '您的英雄事迹';
@override
String get endingHallOfFameLine2 => '将被铭记于荣誉殿堂';
@override
String get endingSkip => '跳过';
@override
String get endingTapToSkip => '点击跳过';
}

View File

@@ -74,5 +74,22 @@
"classTitle": "Class",
"percentComplete": "{percent}% complete",
"newCharacterTitle": "ASCII NEVER DIE - New Character",
"soldButton": "Sold!"
"soldButton": "Sold!",
"endingCongratulations": "★ 恭喜通关 ★",
"endingGameComplete": "您已完成游戏!",
"endingTheHero": "英雄",
"endingLevelFormat": "等级 {level}",
"endingJourneyStats": "冒险记录",
"endingMonstersSlain": "击败的怪物",
"endingQuestsCompleted": "完成的任务",
"endingPlayTime": "游戏时间",
"endingFinalStats": "最终属性",
"endingCredits": "制作人员",
"endingThankYou": "感谢您的游玩!",
"endingLegendLivesOn": "您的传奇将永远流传...",
"endingHallOfFameLine1": "您的英雄事迹",
"endingHallOfFameLine2": "将被铭记于荣誉殿堂",
"endingSkip": "跳过",
"endingTapToSkip": "点击跳过"
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:asciineverdie/l10n/app_localizations.dart';
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/shared/retro_colors.dart';
@@ -92,7 +93,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
child: TextButton(
onPressed: widget.onComplete,
child: Text(
'SKIP',
L10n.of(context).endingSkip,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
@@ -108,7 +109,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
left: 0,
right: 0,
child: Text(
'TAP TO SKIP',
L10n.of(context).endingTapToSkip,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
@@ -148,6 +149,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
}
Widget _buildCreditContent(BuildContext context) {
final l10n = L10n.of(context);
final gold = RetroColors.goldOf(context);
final textPrimary = RetroColors.textPrimaryOf(context);
@@ -168,7 +170,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// CONGRATULATIONS
// ═══════════════════════════════════
Text(
'★ CONGRATULATIONS ★',
l10n.endingCongratulations,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
@@ -178,7 +180,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
),
const SizedBox(height: 16),
Text(
'You have completed the game!',
l10n.endingGameComplete,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 14,
@@ -190,7 +192,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// ═══════════════════════════════════
// THE HERO
// ═══════════════════════════════════
_buildSectionTitle('THE HERO', gold),
_buildSectionTitle(l10n.endingTheHero, gold),
const SizedBox(height: 20),
_buildHeroInfo(context),
const SizedBox(height: 80),
@@ -198,7 +200,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// ═══════════════════════════════════
// JOURNEY STATISTICS
// ═══════════════════════════════════
_buildSectionTitle('JOURNEY STATISTICS', gold),
_buildSectionTitle(l10n.endingJourneyStats, gold),
const SizedBox(height: 20),
_buildStatistics(context),
const SizedBox(height: 80),
@@ -206,7 +208,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// ═══════════════════════════════════
// FINAL STATS
// ═══════════════════════════════════
_buildSectionTitle('FINAL STATS', gold),
_buildSectionTitle(l10n.endingFinalStats, gold),
const SizedBox(height: 20),
_buildFinalStats(context),
const SizedBox(height: 100),
@@ -220,7 +222,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// ═══════════════════════════════════
// CREDITS
// ═══════════════════════════════════
_buildSectionTitle('CREDITS', gold),
_buildSectionTitle(l10n.endingCredits, gold),
const SizedBox(height: 20),
_buildCredits(context),
const SizedBox(height: 100),
@@ -228,7 +230,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// ═══════════════════════════════════
// THE END
// ═══════════════════════════════════
_buildTheEnd(gold),
_buildTheEnd(context, gold),
const SizedBox(height: 200), // 여백 (스크롤 끝)
],
),
@@ -303,6 +305,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
}
Widget _buildHeroInfo(BuildContext context) {
final l10n = L10n.of(context);
final gold = RetroColors.goldOf(context);
final textPrimary = RetroColors.textPrimaryOf(context);
final textMuted = RetroColors.textMutedOf(context);
@@ -321,7 +324,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
const SizedBox(height: 12),
// 레벨, 종족, 직업
Text(
'Level ${widget.traits.level}',
l10n.endingLevelFormat(widget.traits.level),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
@@ -343,6 +346,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
}
Widget _buildStatistics(BuildContext context) {
final l10n = L10n.of(context);
final textPrimary = RetroColors.textPrimaryOf(context);
final exp = RetroColors.expOf(context);
@@ -357,13 +361,14 @@ class _VictoryOverlayState extends State<VictoryOverlay>
return Column(
children: [
_buildStatLine('Monsters Slain', '${widget.progress.monstersKilled}',
textPrimary, exp),
_buildStatLine(l10n.endingMonstersSlain,
'${widget.progress.monstersKilled}', textPrimary, exp),
const SizedBox(height: 8),
_buildStatLine('Quests Completed', '${widget.progress.questCount}',
textPrimary, exp),
_buildStatLine(l10n.endingQuestsCompleted,
'${widget.progress.questCount}', textPrimary, exp),
const SizedBox(height: 8),
_buildStatLine('Play Time', playTimeStr, textPrimary, textPrimary),
_buildStatLine(
l10n.endingPlayTime, playTimeStr, textPrimary, textPrimary),
],
);
}
@@ -474,6 +479,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
}
Widget _buildCredits(BuildContext context) {
final l10n = L10n.of(context);
final textPrimary = RetroColors.textPrimaryOf(context);
final textMuted = RetroColors.textMutedOf(context);
final gold = RetroColors.goldOf(context);
@@ -481,7 +487,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
return Column(
children: [
Text(
'ASCII NEVER DIE',
l10n.appTitle,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
@@ -490,7 +496,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
),
const SizedBox(height: 16),
Text(
'Thank you for playing!',
l10n.endingThankYou,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 12,
@@ -499,7 +505,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
),
const SizedBox(height: 8),
Text(
'Your legend lives on...',
l10n.endingLegendLivesOn,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 10,
@@ -511,7 +517,8 @@ class _VictoryOverlayState extends State<VictoryOverlay>
);
}
Widget _buildTheEnd(Color gold) {
Widget _buildTheEnd(BuildContext context, Color gold) {
final l10n = L10n.of(context);
const theEnd = '''
████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗
╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗
@@ -534,7 +541,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
),
const SizedBox(height: 24),
Text(
'Your heroic deeds will be',
l10n.endingHallOfFameLine1,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 12,
@@ -543,7 +550,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
),
const SizedBox(height: 4),
Text(
'remembered in the Hall of Fame',
l10n.endingHallOfFameLine2,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 12,

View File

@@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
import 'package:asciineverdie/data/class_data.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
@@ -398,6 +399,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
fontSize: 10,
color: RetroColors.textLight,
),
// 영문 알파벳만 허용 (공백 불가)
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),
],
decoration: InputDecoration(
labelText: l10n.name,
labelStyle: const TextStyle(