feat(l10n): 게임 텍스트 다국어 지원 확장

- game_text_l10n.dart: 스탯/UI 텍스트 추가 (+61 라인)
- 한국어/일본어 번역 업데이트
- game_data_l10n.dart: 텍스트 접근자 추가
- equipment_stats_panel: l10n 적용 및 레이아웃 개선
- active_buff_panel, potion_inventory_panel: 코드 정리
- new_character_screen: 코드 정리
- progress_service: 마이너 개선
This commit is contained in:
JiWoong Sul
2025-12-23 15:51:56 +09:00
parent 99f5b74802
commit 1da6fa7a2b
10 changed files with 137 additions and 26 deletions

View File

@@ -1129,6 +1129,17 @@ String translateFaction(String englishFaction) {
return englishFaction; return englishFaction;
} }
/// 스킬/주문 이름 번역 (전투 로그용)
String translateSpell(String englishName) {
if (isKoreanLocale) {
return spellTranslationsKo[englishName] ?? englishName;
}
if (isJapaneseLocale) {
return spellTranslationsJa[englishName] ?? englishName;
}
return englishName;
}
// ============================================================================ // ============================================================================
// 프론트 화면 텍스트 // 프론트 화면 텍스트
// ============================================================================ // ============================================================================
@@ -1418,3 +1429,53 @@ String get uiDot {
if (isJapaneseLocale) return 'DOT'; if (isJapaneseLocale) return 'DOT';
return 'DOT'; return 'DOT';
} }
// ============================================================================
// 아이템 희귀도
// ============================================================================
String get rarityCommon {
if (isKoreanLocale) return '일반';
if (isJapaneseLocale) return 'コモン';
return 'COMMON';
}
String get rarityUncommon {
if (isKoreanLocale) return '고급';
if (isJapaneseLocale) return 'アンコモン';
return 'UNCOMMON';
}
String get rarityRare {
if (isKoreanLocale) return '희귀';
if (isJapaneseLocale) return 'レア';
return 'RARE';
}
String get rarityEpic {
if (isKoreanLocale) return '영웅';
if (isJapaneseLocale) return 'エピック';
return 'EPIC';
}
String get rarityLegendary {
if (isKoreanLocale) return '전설';
if (isJapaneseLocale) return 'レジェンダリー';
return 'LEGENDARY';
}
// ============================================================================
// 캐릭터 생성 화면 텍스트
// ============================================================================
String uiRollHistory(int count) {
if (isKoreanLocale) return '리롤 기록: $count회';
if (isJapaneseLocale) return 'リロール履歴: $count回';
return '$count roll(s) in history';
}
String get uiEnterName {
if (isKoreanLocale) return '이름을 입력해주세요.';
if (isJapaneseLocale) return '名前を入力してください。';
return 'Please enter a name.';
}

View File

@@ -119,6 +119,17 @@ const Map<String, String> spellTranslationsJa = {
'Deploy': 'デプロイ', 'Deploy': 'デプロイ',
'Scale Up': 'スケールアップ', 'Scale Up': 'スケールアップ',
'Failover': 'フェイルオーバー', 'Failover': 'フェイルオーバー',
// ポーション (Potions)
'Minor Health Patch': 'マイナーHPパッチ',
'Health Patch': 'HPパッチ',
'Major Health Patch': 'メジャーHPパッチ',
'Super Health Patch': 'スーパーHPパッチ',
'Ultra Health Patch': 'ウルトラHPパッチ',
'Minor Mana Cache': 'マイナーMPキャッシュ',
'Mana Cache': 'MPキャッシュ',
'Major Mana Cache': 'メジャーMPキャッシュ',
'Super Mana Cache': 'スーパーMPキャッシュ',
'Ultra Mana Cache': 'ウルトラMPキャッシュ',
}; };
/// モンスター名日本語翻訳 (主要モンスター) /// モンスター名日本語翻訳 (主要モンスター)

View File

@@ -119,6 +119,17 @@ const Map<String, String> spellTranslationsKo = {
'Deploy': '배포', 'Deploy': '배포',
'Scale Up': '스케일 업', 'Scale Up': '스케일 업',
'Failover': '페일오버', 'Failover': '페일오버',
// 포션 (Potions)
'Minor Health Patch': '소형 HP 패치',
'Health Patch': 'HP 패치',
'Major Health Patch': '대형 HP 패치',
'Super Health Patch': '초대형 HP 패치',
'Ultra Health Patch': '최고급 HP 패치',
'Minor Mana Cache': '소형 MP 캐시',
'Mana Cache': 'MP 캐시',
'Major Mana Cache': '대형 MP 캐시',
'Super Mana Cache': '초대형 MP 캐시',
'Ultra Mana Cache': '최고급 MP 캐시',
}; };
/// 몬스터 이름 한국어 번역 (주요 몬스터만) /// 몬스터 이름 한국어 번역 (주요 몬스터만)

View File

@@ -1000,11 +1000,13 @@ class ProgressService {
final damage = dot.damagePerTick * ticksTriggered; final damage = dot.damagePerTick * ticksTriggered;
dotDamageThisTick += damage; dotDamageThisTick += damage;
// DOT 데미지 이벤트 생성 // DOT 데미지 이벤트 생성 (skillId → name 변환)
final dotSkillName =
SkillData.getSkillById(dot.skillId)?.name ?? dot.skillId;
newEvents.add( newEvents.add(
CombatEvent.dotTick( CombatEvent.dotTick(
timestamp: timestamp, timestamp: timestamp,
skillName: dot.skillId, skillName: dotSkillName,
damage: damage, damage: damage,
targetName: monsterStats.name, targetName: monsterStats.name,
), ),

View File

@@ -459,11 +459,22 @@ class GameDataL10n {
} }
/// 각 단어의 첫 글자를 대문자로 (Title Case) /// 각 단어의 첫 글자를 대문자로 (Title Case)
/// 하이픈으로 연결된 단어도 처리 (예: "off-by-one" → "Off-by-One")
static String _toTitleCase(String s) { static String _toTitleCase(String s) {
return s return s
.split(' ') .split(' ')
.map((word) { .map((word) {
if (word.isEmpty) return word; if (word.isEmpty) return word;
// 하이픈 포함 단어 처리
if (word.contains('-')) {
return word
.split('-')
.map((part) {
if (part.isEmpty) return part;
return part[0].toUpperCase() + part.substring(1);
})
.join('-');
}
return word[0].toUpperCase() + word.substring(1); return word[0].toUpperCase() + word.substring(1);
}) })
.join(' '); .join(' ');

View File

@@ -174,6 +174,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
/// 전투 이벤트를 메시지와 타입으로 변환 /// 전투 이벤트를 메시지와 타입으로 변환
(String, CombatLogType) _formatCombatEvent(CombatEvent event) { (String, CombatLogType) _formatCombatEvent(CombatEvent event) {
final target = event.targetName ?? ''; final target = event.targetName ?? '';
// 스킬/포션 이름 번역 (전역 로케일 사용)
final skillName = event.skillName != null
? game_l10n.translateSpell(event.skillName!)
: '';
return switch (event.type) { return switch (event.type) {
CombatEventType.playerAttack => CombatEventType.playerAttack =>
event.isCritical event.isCritical
@@ -208,41 +212,34 @@ class _GamePlayScreenState extends State<GamePlayScreen>
CombatEventType.playerSkill => CombatEventType.playerSkill =>
event.isCritical event.isCritical
? ( ? (
game_l10n.combatSkillCritical( game_l10n.combatSkillCritical(skillName, event.damage),
event.skillName ?? '',
event.damage,
),
CombatLogType.critical, CombatLogType.critical,
) )
: ( : (
game_l10n.combatSkillDamage(event.skillName ?? '', event.damage), game_l10n.combatSkillDamage(skillName, event.damage),
CombatLogType.spell, CombatLogType.spell,
), ),
CombatEventType.playerHeal => ( CombatEventType.playerHeal => (
game_l10n.combatSkillHeal( game_l10n.combatSkillHeal(
event.skillName ?? game_l10n.uiHeal, skillName.isNotEmpty ? skillName : game_l10n.uiHeal,
event.healAmount, event.healAmount,
), ),
CombatLogType.heal, CombatLogType.heal,
), ),
CombatEventType.playerBuff => ( CombatEventType.playerBuff => (
game_l10n.combatBuffActivated(event.skillName ?? ''), game_l10n.combatBuffActivated(skillName),
CombatLogType.buff, CombatLogType.buff,
), ),
CombatEventType.dotTick => ( CombatEventType.dotTick => (
game_l10n.combatDotTick(event.skillName ?? '', event.damage), game_l10n.combatDotTick(skillName, event.damage),
CombatLogType.dotTick, CombatLogType.dotTick,
), ),
CombatEventType.playerPotion => ( CombatEventType.playerPotion => (
game_l10n.combatPotionUsed( game_l10n.combatPotionUsed(skillName, event.healAmount, target),
event.skillName ?? '',
event.healAmount,
target,
),
CombatLogType.potion, CombatLogType.potion,
), ),
CombatEventType.potionDrop => ( CombatEventType.potionDrop => (
game_l10n.combatPotionDrop(event.skillName ?? ''), game_l10n.combatPotionDrop(skillName),
CombatLogType.potionDrop, CombatLogType.potionDrop,
), ),
}; };

View File

@@ -127,7 +127,7 @@ class _BuffRow extends StatelessWidget {
if (effect.atkModifier != 0) { if (effect.atkModifier != 0) {
modifiers.add( modifiers.add(
_ModifierChip( _ModifierChip(
label: 'ATK', label: l10n.statAtk,
value: effect.atkModifier, value: effect.atkModifier,
isPositive: effect.atkModifier > 0, isPositive: effect.atkModifier > 0,
), ),
@@ -137,7 +137,7 @@ class _BuffRow extends StatelessWidget {
if (effect.defModifier != 0) { if (effect.defModifier != 0) {
modifiers.add( modifiers.add(
_ModifierChip( _ModifierChip(
label: 'DEF', label: l10n.statDef,
value: effect.defModifier, value: effect.defModifier,
isPositive: effect.defModifier > 0, isPositive: effect.defModifier > 0,
), ),
@@ -147,7 +147,7 @@ class _BuffRow extends StatelessWidget {
if (effect.criRateModifier != 0) { if (effect.criRateModifier != 0) {
modifiers.add( modifiers.add(
_ModifierChip( _ModifierChip(
label: 'CRI', label: l10n.statCri,
value: effect.criRateModifier, value: effect.criRateModifier,
isPositive: effect.criRateModifier > 0, isPositive: effect.criRateModifier > 0,
), ),
@@ -157,7 +157,7 @@ class _BuffRow extends StatelessWidget {
if (effect.evasionModifier != 0) { if (effect.evasionModifier != 0) {
modifiers.add( modifiers.add(
_ModifierChip( _ModifierChip(
label: 'EVA', label: l10n.statEva,
value: effect.evasionModifier, value: effect.evasionModifier,
isPositive: effect.evasionModifier > 0, isPositive: effect.evasionModifier > 0,
), ),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/core/engine/item_service.dart'; import 'package:askiineverdie/src/core/engine/item_service.dart';
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:askiineverdie/src/core/model/equipment_item.dart'; import 'package:askiineverdie/src/core/model/equipment_item.dart';
import 'package:askiineverdie/src/core/model/equipment_slot.dart'; import 'package:askiineverdie/src/core/model/equipment_slot.dart';
import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/game_state.dart';
@@ -80,6 +81,12 @@ class _EquipmentSlotTile extends StatelessWidget {
final score = ItemService.calculateEquipmentScore(item); final score = ItemService.calculateEquipmentScore(item);
final rarityColor = _getRarityColor(item.rarity); final rarityColor = _getRarityColor(item.rarity);
// 슬롯 인덱스로 아이템 이름 번역 (0: weapon, 1: shield, 2+: armor)
final translatedName = GameDataL10n.translateEquipString(
context,
item.name,
item.slot.index,
);
return ExpansionTile( return ExpansionTile(
initiallyExpanded: initiallyExpanded, initiallyExpanded: initiallyExpanded,
@@ -92,7 +99,7 @@ class _EquipmentSlotTile extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
item.name, translatedName,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: rarityColor, color: rarityColor,
@@ -426,7 +433,7 @@ class _ItemMetaRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final rarityName = item.rarity.name.toUpperCase(); final rarityName = _getTranslatedRarity(item.rarity);
return Row( return Row(
children: [ children: [
@@ -479,3 +486,14 @@ String _getSlotName(EquipmentSlot slot) {
EquipmentSlot.sollerets => l10n.slotSollerets, EquipmentSlot.sollerets => l10n.slotSollerets,
}; };
} }
/// 희귀도 번역 반환
String _getTranslatedRarity(ItemRarity rarity) {
return switch (rarity) {
ItemRarity.common => l10n.rarityCommon,
ItemRarity.uncommon => l10n.rarityUncommon,
ItemRarity.rare => l10n.rarityRare,
ItemRarity.epic => l10n.rarityEpic,
ItemRarity.legendary => l10n.rarityLegendary,
};
}

View File

@@ -110,10 +110,10 @@ class _PotionRow extends StatelessWidget {
_PotionIcon(type: potion.type, tier: potion.tier), _PotionIcon(type: potion.type, tier: potion.tier),
const SizedBox(width: 4), const SizedBox(width: 4),
// 물약 이름 // 물약 이름 (번역 적용)
Expanded( Expanded(
child: Text( child: Text(
potion.name, l10n.translateSpell(potion.name),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: color, color: color,

View File

@@ -145,7 +145,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('이름을 입력해주세요.'))); ).showSnackBar(SnackBar(content: Text(game_l10n.uiEnterName)));
return; return;
} }
@@ -359,7 +359,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text( child: Text(
'${_rollHistory.length} roll(s) in history', game_l10n.uiRollHistory(_rollHistory.length),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),