feat(i18n): 다국어 번역 확장

- game_data_l10n 개선
- 일본어, 한국어 번역 추가
This commit is contained in:
JiWoong Sul
2026-01-14 00:18:04 +09:00
parent f89017e5ba
commit d52dea56ea
3 changed files with 118 additions and 37 deletions

View File

@@ -510,6 +510,17 @@ const Map<String, String> weaponTranslationsJa = {
'Dyson Sphere Core': 'ダイソン球コア',
'Black Hole Computer': 'ブラックホールコンピューター',
'Universe Simulator': '宇宙シミュレーター',
'Dimensional Gateway': '次元の門',
'Time Loop Device': 'タイムループデバイス',
'Reality Compiler': 'リアリティコンパイラー',
'Multiverse Bridge': 'マルチバースブリッジ',
'Cosmic Debugger': 'コズミックデバッガー',
'Entropy Reverser': 'エントロピーリバーサー',
'Big Bang Trigger': 'ビッグバントリガー',
'Heat Death Preventer': '熱的死防止装置',
'Infinity Engine': '無限エンジン',
'Omniscience Module': '全知モジュール',
'God Mode Activator': '神モードアクティベーター',
};
/// 鎧名日本語翻訳

View File

@@ -510,6 +510,17 @@ const Map<String, String> weaponTranslationsKo = {
'Dyson Sphere Core': '다이슨 구 코어',
'Black Hole Computer': '블랙홀 컴퓨터',
'Universe Simulator': '우주 시뮬레이터',
'Dimensional Gateway': '차원의 관문',
'Time Loop Device': '시간 루프 장치',
'Reality Compiler': '현실 컴파일러',
'Multiverse Bridge': '다중 우주 다리',
'Cosmic Debugger': '우주 디버거',
'Entropy Reverser': '엔트로피 역전기',
'Big Bang Trigger': '빅뱅 트리거',
'Heat Death Preventer': '열 죽음 방지기',
'Infinity Engine': '무한 엔진',
'Omniscience Module': '전지 모듈',
'God Mode Activator': '신 모드 활성기',
};
/// 갑옷 이름 한국어 번역

View File

@@ -1,4 +1,5 @@
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
import 'package:asciineverdie/data/game_translations_ja.dart';
import 'package:asciineverdie/data/game_translations_ko.dart';
import 'package:asciineverdie/src/core/util/pq_logic.dart';
import 'package:flutter/widgets.dart';
@@ -18,6 +19,13 @@ class GameDataL10n {
return locale.languageCode == 'ko';
}
/// 현재 로케일이 일본어인지 확인 (글로벌 로케일 사용)
static bool _isJapanese(BuildContext context) {
if (l10n.isJapaneseLocale) return true;
final locale = Localizations.localeOf(context);
return locale.languageCode == 'ja';
}
/// 종족 이름 번역
static String getRaceName(BuildContext context, String englishName) {
if (_isKorean(context)) {
@@ -284,7 +292,9 @@ class GameDataL10n {
String equipString,
int slotIndex,
) {
if (!_isKorean(context) || equipString.isEmpty) return equipString;
final isKo = _isKorean(context);
final isJa = _isJapanese(context);
if ((!isKo && !isJa) || equipString.isEmpty) return equipString;
// 1. +/- 값 추출
final plusMatch = RegExp(r'^([+-]?\d+)\s+').firstMatch(equipString);
@@ -296,13 +306,24 @@ class GameDataL10n {
}
// 2. 기본 장비 이름 찾기 (가장 긴 매칭 우선)
// 통합 맵 사용 (추가 번역 포함)
final Map<String, String> baseMap;
if (isKo) {
if (slotIndex == 0) {
baseMap = weaponTranslationsKo;
} else if (slotIndex == 1) {
baseMap = shieldTranslationsKo;
baseMap = allShieldTranslationsKo;
} else {
baseMap = armorTranslationsKo;
baseMap = allArmorTranslationsKo;
}
} else {
if (slotIndex == 0) {
baseMap = weaponTranslationsJa;
} else if (slotIndex == 1) {
baseMap = allShieldTranslationsJa;
} else {
baseMap = allArmorTranslationsJa;
}
}
String baseTranslated = remaining;
@@ -326,6 +347,7 @@ class GameDataL10n {
.where((s) => s.isNotEmpty)
.toList();
final translatedMods = modWords.map((mod) {
if (isKo) {
if (isWeapon) {
return offenseAttribTranslationsKo[mod] ??
offenseBadTranslationsKo[mod] ??
@@ -335,6 +357,17 @@ class GameDataL10n {
defenseBadTranslationsKo[mod] ??
mod;
}
} else {
if (isWeapon) {
return offenseAttribTranslationsJa[mod] ??
offenseBadTranslationsJa[mod] ??
mod;
} else {
return defenseAttribTranslationsJa[mod] ??
defenseBadTranslationsJa[mod] ??
mod;
}
}
}).toList();
// 4. 조합
@@ -353,15 +386,17 @@ class GameDataL10n {
/// 예: "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터"
/// 예: "index out of bounds Array fragment" → "인덱스 초과의 배열 조각"
static String translateItemString(BuildContext context, String itemString) {
if (!_isKorean(context) || itemString.isEmpty) return itemString;
final isKo = _isKorean(context);
final isJa = _isJapanese(context);
if ((!isKo && !isJa) || itemString.isEmpty) return itemString;
// 1. specialItem 형식 체크: "Attrib Special of ItemOf"
// itemOfs에 있는 값으로 끝나는지 확인
final specialItemResult = _tryTranslateSpecialItem(itemString);
final specialItemResult = _tryTranslateSpecialItem(itemString, isKo);
if (specialItemResult != null) return specialItemResult;
// 2. 몬스터 드롭 형식 체크: "{monster_lowercase} {drop_ProperCase}"
final monsterDropResult = _tryTranslateMonsterDrop(itemString);
final monsterDropResult = _tryTranslateMonsterDrop(itemString, isKo);
if (monsterDropResult != null) return monsterDropResult;
// 3. interestingItem 형식: "Attrib Special" (2단어)
@@ -370,21 +405,32 @@ class GameDataL10n {
final attrib = words[0];
final special = words[1];
if (isKo) {
final attribKo = itemAttribTranslationsKo[attrib] ?? attrib;
final specialKo = specialTranslationsKo[special] ?? special;
return '$attribKo $specialKo';
} else {
final attribJa = itemAttribTranslationsJa[attrib] ?? attrib;
final specialJa = specialTranslationsJa[special] ?? special;
return '$attribJa $specialJa';
}
}
// 4. 단일 단어 (boringItem 등) - 잡템 번역 시도
if (isKo) {
return boringItemTranslationsKo[itemString] ??
dropItemTranslationsKo[itemString.toLowerCase()] ??
allDropTranslationsKo[itemString.toLowerCase()] ??
itemString;
} else {
return boringItemTranslationsJa[itemString] ??
allDropTranslationsJa[itemString.toLowerCase()] ??
itemString;
}
}
/// specialItem 형식 번역 시도
/// "Attrib Special of ItemOf" → "ItemOf의 Attrib Special"
static String? _tryTranslateSpecialItem(String itemString) {
static String? _tryTranslateSpecialItem(String itemString, bool isKo) {
// "of" 뒤의 부분이 itemOfs에 있는지 확인
final ofMatch = RegExp(r'^(.+)\s+of\s+(.+)$').firstMatch(itemString);
if (ofMatch == null) return null;
@@ -393,7 +439,8 @@ class GameDataL10n {
final afterOf = ofMatch.group(2)!;
// afterOf가 itemOfs에 있어야 specialItem 형식
if (!itemOfsTranslationsKo.containsKey(afterOf)) return null;
final itemOfsMap = isKo ? itemOfsTranslationsKo : itemOfsTranslationsJa;
if (!itemOfsMap.containsKey(afterOf)) return null;
// beforeOf를 Attrib + Special로 분리
final words = beforeOf.split(' ');
@@ -403,24 +450,32 @@ class GameDataL10n {
final special = words.last;
// Attrib와 Special이 유효한지 확인
if (!itemAttribTranslationsKo.containsKey(attrib) &&
!specialTranslationsKo.containsKey(special)) {
final attribMap = isKo ? itemAttribTranslationsKo : itemAttribTranslationsJa;
final specialMap = isKo ? specialTranslationsKo : specialTranslationsJa;
if (!attribMap.containsKey(attrib) && !specialMap.containsKey(special)) {
return null;
}
final attribKo = itemAttribTranslationsKo[attrib] ?? attrib;
final specialKo = specialTranslationsKo[special] ?? special;
final itemOfKo = itemOfsTranslationsKo[afterOf] ?? afterOf;
final attribT = attribMap[attrib] ?? attrib;
final specialT = specialMap[special] ?? special;
final itemOfT = itemOfsMap[afterOf] ?? afterOf;
return '$itemOfKo의 $attribKo $specialKo';
if (isKo) {
return '$itemOfT의 $attribT $specialT';
} else {
return '$itemOfTの$attribT $specialT';
}
}
/// 몬스터 드롭 형식 번역 시도
/// "{monster_lowercase} {drop_ProperCase}" → "{몬스터}의 {드롭아이템}"
static String? _tryTranslateMonsterDrop(String itemString) {
// dropItemTranslationsKo에서 매칭되는 드롭 아이템 찾기
static String? _tryTranslateMonsterDrop(String itemString, bool isKo) {
// 드롭 아이템 번역 맵 선택 (통합 맵 사용)
final dropMap = isKo ? allDropTranslationsKo : allDropTranslationsJa;
final monsterMap = isKo ? allMonsterTranslationsKo : allMonsterTranslationsJa;
// (대소문자 무시, 아이템 문자열 끝에서 매칭)
for (final entry in dropItemTranslationsKo.entries) {
for (final entry in dropMap.entries) {
final dropItem = entry.key;
final dropItemProperCase = _properCase(dropItem);
@@ -443,10 +498,14 @@ class GameDataL10n {
// 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기)
final monsterNameKey = _toTitleCase(monsterPart);
final monsterKo = monsterTranslationsKo[monsterNameKey] ?? monsterPart;
final monsterT = monsterMap[monsterNameKey] ?? monsterPart;
final dropKo = entry.value;
return '$monsterKo의 $dropKo';
final dropT = entry.value;
if (isKo) {
return '$monsterT의 $dropT';
} else {
return '$monsterTの$dropT';
}
}
}
return null;