diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 08ab75e6332..6e5973d23a3 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -15,7 +15,7 @@ import { GameData, PlayerGender } from './system/game-data'; import StarterSelectUiHandler from './ui/starter-select-ui-handler'; import { TextStyle, addTextObject } from './ui/text'; import { Moves } from "./data/enums/moves"; -import { } from "./data/move"; +import { allMoves } from "./data/move"; import { initMoves } from './data/move'; import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave } from './modifier/modifier-type'; import AbilityBar from './ui/ability-bar'; @@ -58,6 +58,7 @@ import { UiTheme } from './enums/ui-theme'; import { SceneBase } from './scene-base'; import CandyBar from './ui/candy-bar'; import { Variant, variantData } from './data/variant'; +import { Localizable } from './plugins/i18n'; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -454,7 +455,7 @@ export default class BattleScene extends SceneBase { hideOnComplete: true }); - this.reset(); + this.reset(false, false, true); const ui = new UI(this); this.uiContainer.add(ui); @@ -738,7 +739,7 @@ export default class BattleScene extends SceneBase { return this.currentBattle.randSeedInt(this, range, min); } - reset(clearScene: boolean = false, clearData: boolean = false): void { + reset(clearScene: boolean = false, clearData: boolean = false, reloadI18n: boolean = false): void { if (clearData) this.gameData = new GameData(this); @@ -791,7 +792,13 @@ export default class BattleScene extends SceneBase { this.trainer.setTexture(`trainer_${this.gameData.gender === PlayerGender.FEMALE ? 'f' : 'm'}_back`); this.trainer.setPosition(406, 186); - this.trainer.setVisible(true) + this.trainer.setVisible(true); + + if (reloadI18n) { + const localizable: Localizable[] = [ ...allMoves ]; + for (let item of localizable) + item.localize(); + } if (clearScene) { this.fadeOutBgm(250, false); diff --git a/src/data/move.ts b/src/data/move.ts index 313aa8a42a2..cd527661e27 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -24,7 +24,7 @@ import { Species } from "./enums/species"; import { ModifierPoolType } from "#app/modifier/modifier-type"; import { Command } from "../ui/command-ui-handler"; import { Biome } from "./enums/biome"; -import i18next from '../plugins/i18n'; +import i18next, { Localizable } from '../plugins/i18n'; export enum MoveCategory { PHYSICAL, @@ -75,7 +75,7 @@ export enum MoveFlags { type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; -export default class Move { +export default class Move implements Localizable { public id: Moves; public name: string; public type: Type; @@ -97,14 +97,14 @@ export default class Move { const i18nKey = Moves[id].split('_').filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join('') as unknown as string; - this.name = id ? i18next.t(`move:${i18nKey}.name`) as string : ''; + this.name = id ? i18next.t(`move:${i18nKey}.name`).toString() : ''; this.type = type; this.category = category; this.moveTarget = defaultMoveTarget; this.power = power; this.accuracy = accuracy; this.pp = pp; - this.effect = id ? i18next.t(`move:${i18nKey}.effect`) as string : ''; + this.effect = id ? i18next.t(`move:${i18nKey}.effect`).toString() : ''; this.chance = chance; this.priority = priority; this.generation = generation; @@ -119,6 +119,13 @@ export default class Move { this.setFlag(MoveFlags.MAKES_CONTACT, true); } + localize() { + const i18nKey = Moves[this.id].split('_').filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join('') as unknown as string; + + this.name = this.id ? i18next.t(`move:${i18nKey}.name`).toString() : ''; + this.effect = this.id ? i18next.t(`move:${i18nKey}.effect`).toString() : ''; + } + getAttrs(attrType: { new(...args: any[]): MoveAttr }): MoveAttr[] { return this.attrs.filter(a => a instanceof attrType); } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index e987129093f..431e99ca72c 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -8,12 +8,14 @@ import { SceneBase } from "./scene-base"; import { WindowVariant, getWindowVariantSuffix } from "./ui/ui-theme"; import { isMobile } from "./touch-controls"; import * as Utils from "./utils"; +import { initI18n } from "./plugins/i18n"; export class LoadingScene extends SceneBase { constructor() { super('loading'); Phaser.Plugins.PluginCache.register('Loader', CacheBustedLoaderPlugin, 'load'); + initI18n(); } preload() { diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 8e4997f5d8d..9a72ff55f9d 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -15,41 +15,52 @@ export interface MoveTranslations { [key: string]: MoveTranslationEntry } +export interface Localizable { + localize(): void; +} + const DEFAULT_LANGUAGE_OVERRIDE = ''; -/** - * i18next is a localization library for maintaining and using translation resources. - * - * Q: How do I add a new language? - * A: To add a new language, create a new folder in the locales directory with the language code. - * Each language folder should contain a file for each namespace (ex. menu.ts) with the translations. - * - * Q: How do I add a new namespace? - * A: To add a new namespace, create a new file in each language folder with the translations. - * Then update the `resources` field in the init() call and the CustomTypeOptions interface. - */ +export function initI18n(): void { + let lang = 'en'; -i18next.init({ - lng: DEFAULT_LANGUAGE_OVERRIDE ? DEFAULT_LANGUAGE_OVERRIDE : 'en', - fallbackLng: 'en', - debug: true, - interpolation: { - escapeValue: false, - }, - resources: { - en: { - menu: enMenu, - move: enMove, + if (localStorage.getItem('prLang')) + lang = localStorage.getItem('prLang'); + + /** + * i18next is a localization library for maintaining and using translation resources. + * + * Q: How do I add a new language? + * A: To add a new language, create a new folder in the locales directory with the language code. + * Each language folder should contain a file for each namespace (ex. menu.ts) with the translations. + * + * Q: How do I add a new namespace? + * A: To add a new namespace, create a new file in each language folder with the translations. + * Then update the `resources` field in the init() call and the CustomTypeOptions interface. + */ + + i18next.init({ + lng: DEFAULT_LANGUAGE_OVERRIDE ? DEFAULT_LANGUAGE_OVERRIDE : lang, + fallbackLng: 'en', + debug: true, + interpolation: { + escapeValue: false, }, - it: { - menu: itMenu, + resources: { + en: { + menu: enMenu, + move: enMove, + }, + it: { + menu: itMenu, + }, + fr: { + menu: frMenu, + move: frMove, + } }, - fr: { - menu: frMenu, - move: frMove, - } - }, -}); + }); +} // Module declared to make referencing keys in the localization files type-safe. declare module 'i18next' { diff --git a/src/system/settings.ts b/src/system/settings.ts index 68199d9aa5b..bc302543a13 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -1,13 +1,17 @@ +import i18next from "i18next"; import BattleScene from "../battle-scene"; import { hasTouchscreen } from "../touch-controls"; import { updateWindowType } from "../ui/ui-theme"; import { PlayerGender } from "./game-data"; +import { Mode } from "#app/ui/ui"; +import SettingsUiHandler from "#app/ui/settings-ui-handler"; export enum Setting { Game_Speed = "GAME_SPEED", Master_Volume = "MASTER_VOLUME", BGM_Volume = "BGM_VOLUME", SE_Volume = "SE_VOLUME", + Language = "LANGUAGE", Damage_Numbers = "DAMAGE_NUMBERS", UI_Theme = "UI_THEME", Window_Type = "WINDOW_TYPE", @@ -38,6 +42,7 @@ export const settingOptions: SettingOptions = { [Setting.Master_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : 'Mute'), [Setting.BGM_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : 'Mute'), [Setting.SE_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : 'Mute'), + [Setting.Language]: [ 'English', 'Change' ], [Setting.Damage_Numbers]: [ 'Off', 'Simple', 'Fancy' ], [Setting.UI_Theme]: [ 'Default', 'Legacy' ], [Setting.Window_Type]: new Array(5).fill(null).map((_, i) => (i + 1).toString()), @@ -60,6 +65,7 @@ export const settingDefaults: SettingDefaults = { [Setting.Master_Volume]: 5, [Setting.BGM_Volume]: 10, [Setting.SE_Volume]: 10, + [Setting.Language]: 0, [Setting.Damage_Numbers]: 0, [Setting.UI_Theme]: 0, [Setting.Window_Type]: 0, @@ -77,7 +83,7 @@ export const settingDefaults: SettingDefaults = { [Setting.Vibration]: 0 }; -export const reloadSettings: Setting[] = [ Setting.UI_Theme ]; +export const reloadSettings: Setting[] = [ Setting.UI_Theme, Setting.Language ]; export function setSetting(scene: BattleScene, setting: Setting, value: integer): boolean { switch (setting) { @@ -151,6 +157,39 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) case Setting.Vibration: scene.enableVibration = settingOptions[setting][value] !== 'Disabled' && hasTouchscreen(); break; + case Setting.Language: + if (value) { + if (scene.ui) { + const cancelHandler = () => { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(Object.values(Setting).indexOf(Setting.Language), 0, true); + }; + const changeLocaleHandler = (locale: string) => { + i18next.changeLanguage(locale); + localStorage.setItem('prLang', locale); + cancelHandler(); + scene.reset(true, false, true); + }; + scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: [ + { + label: 'English', + handler: () => changeLocaleHandler('en') + }, + { + label: 'French', + handler: () => changeLocaleHandler('fr') + }, + { + label: 'Cancel', + handler: () => cancelHandler() + } + ] + }); + return false; + } + } + break; } return true; diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index ce8a7542d89..8f43b377d21 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -22,11 +22,13 @@ export default class SettingsUiHandler extends UiHandler { private cursorObj: Phaser.GameObjects.NineSlice; private reloadRequired: boolean; + private reloadI18n: boolean; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); this.reloadRequired = false; + this.reloadI18n = false; } setup() { @@ -197,8 +199,11 @@ export default class SettingsUiHandler extends UiHandler { if (save) { this.scene.gameData.saveSetting(setting, cursor) - if (reloadSettings.includes(setting)) + if (reloadSettings.includes(setting)) { this.reloadRequired = true; + if (setting === Setting.Language) + this.reloadI18n = true; + } } return true; @@ -234,7 +239,7 @@ export default class SettingsUiHandler extends UiHandler { this.eraseCursor(); if (this.reloadRequired) { this.reloadRequired = false; - this.scene.reset(true); + this.scene.reset(true, false, true); } }