From 2476e70cb67966f15ff54be142c42c4217888d60 Mon Sep 17 00:00:00 2001 From: flx-sta <50131232+flx-sta@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:30:59 -0700 Subject: [PATCH] implement: i18next language lazy-loading --- src/loading-scene.ts | 2 - src/main.ts | 100 +++++++++++------------ src/plugins/i18n.ts | 186 +++++++++++++++++++++++++++++++------------ src/utils.ts | 12 +++ 4 files changed, 198 insertions(+), 102 deletions(-) diff --git a/src/loading-scene.ts b/src/loading-scene.ts index c3cb494d497..234375ae473 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -6,7 +6,6 @@ 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"; import { initPokemonPrevolutions } from "#app/data/pokemon-evolutions"; import { initBiomes } from "#app/data/biomes"; import { initEggMoves } from "#app/data/egg-moves"; @@ -33,7 +32,6 @@ export class LoadingScene extends SceneBase { super(LoadingScene.KEY); Phaser.Plugins.PluginCache.register("Loader", CacheBustedLoaderPlugin, "load"); - initI18n(); } preload() { diff --git a/src/main.ts b/src/main.ts index b5f813bdf2f..92ee267bf65 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,11 @@ import Phaser from "phaser"; -import BattleScene from "./battle-scene"; import InvertPostFX from "./pipelines/invert"; import { version } from "../package.json"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import BBCodeTextPlugin from "phaser3-rex-plugins/plugins/bbcodetext-plugin"; import InputTextPlugin from "phaser3-rex-plugins/plugins/inputtext-plugin"; import TransitionImagePackPlugin from "phaser3-rex-plugins/templates/transitionimagepack/transitionimagepack-plugin"; -import { LoadingScene } from "./loading-scene"; +import { initI18n } from "./plugins/i18n"; // Catch global errors and display them in an alert so users can report the issue. @@ -25,52 +24,6 @@ window.addEventListener("unhandledrejection", (event) => { //alert(errorString); }); -const config: Phaser.Types.Core.GameConfig = { - type: Phaser.WEBGL, - parent: "app", - scale: { - width: 1920, - height: 1080, - mode: Phaser.Scale.FIT - }, - plugins: { - global: [{ - key: "rexInputTextPlugin", - plugin: InputTextPlugin, - start: true - }, { - key: "rexBBCodeTextPlugin", - plugin: BBCodeTextPlugin, - start: true - }, { - key: "rexTransitionImagePackPlugin", - plugin: TransitionImagePackPlugin, - start: true - }], - scene: [{ - key: "rexUI", - plugin: UIPlugin, - mapping: "rexUI" - }] - }, - input: { - mouse: { - target: "app" - }, - touch: { - target: "app" - }, - gamepad: true - }, - dom: { - createContainer: true - }, - pixelArt: true, - pipeline: [ InvertPostFX ] as unknown as Phaser.Types.Core.PipelineConfig, - scene: [ LoadingScene, BattleScene ], - version: version -}; - /** * Sets this object's position relative to another object with a given offset */ @@ -91,8 +44,55 @@ document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems let game; -const startGame = () => { - game = new Phaser.Game(config); +const startGame = async () => { + await initI18n(); + const LoadingScene = (await import("./loading-scene")).LoadingScene; + const BattleScene = (await import("./battle-scene")).default; + game = new Phaser.Game({ + type: Phaser.WEBGL, + parent: "app", + scale: { + width: 1920, + height: 1080, + mode: Phaser.Scale.FIT + }, + plugins: { + global: [{ + key: "rexInputTextPlugin", + plugin: InputTextPlugin, + start: true + }, { + key: "rexBBCodeTextPlugin", + plugin: BBCodeTextPlugin, + start: true + }, { + key: "rexTransitionImagePackPlugin", + plugin: TransitionImagePackPlugin, + start: true + }], + scene: [{ + key: "rexUI", + plugin: UIPlugin, + mapping: "rexUI" + }] + }, + input: { + mouse: { + target: "app" + }, + touch: { + target: "app" + }, + gamepad: true + }, + dom: { + createContainer: true + }, + pixelArt: true, + pipeline: [ InvertPostFX ] as unknown as Phaser.Types.Core.PipelineConfig, + scene: [ LoadingScene, BattleScene ], + version: version + }); game.sound.pauseOnBlur = false; }; diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 26cb359a409..83b79504b79 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -1,18 +1,11 @@ import i18next from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import processor, { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor"; +import HttpBackend from "i18next-http-backend"; +import { camelCaseToKebabCase } from "#app/utils"; +import pkg from "../../package.json"; -import { caEsConfig} from "../../public/locales/ca_ES/config"; -import { deConfig } from "../../public/locales/de/config"; -import { enConfig } from "../../public/locales/en/config"; -import { esConfig } from "../../public/locales/es/config"; -import { frConfig } from "../../public/locales/fr/config"; -import { itConfig } from "../../public/locales/it/config"; -import { koConfig } from "../../public/locales/ko/config"; -import { jaConfig } from "../../public/locales/ja/config"; -import { ptBrConfig } from "../../public/locales/pt_BR/config"; -import { zhCnConfig } from "../../public/locales/zh_CN/config"; -import { zhTwConfig } from "../../public/locales/zh_TW/config"; +//#region Interfaces/Types interface LoadingFontFaceProperty { face: FontFace, @@ -20,6 +13,10 @@ interface LoadingFontFaceProperty { only?: Array } +//#region Constants + +let isInitialized = false; + const unicodeRanges = { fullwidth: "U+FF00-FFEF", hangul: "U+1100-11FF,U+3130-318F,U+A960-A97F,U+AC00-D7AF,U+D7B0-D7FF", @@ -28,6 +25,7 @@ const unicodeRanges = { CJKIdeograph: "U+4E00-9FFF", specialCharacters: "U+266A,U+2605,U+2665,U+2663" //♪.★,♥,♣ }; + const rangesByLanguage = { korean: [unicodeRanges.CJKCommon, unicodeRanges.hangul].join(","), chinese: [unicodeRanges.CJKCommon, unicodeRanges.fullwidth, unicodeRanges.CJKIdeograph].join(","), @@ -74,6 +72,19 @@ const fonts: Array = [ }, ]; +/** maps namespaces that deviate from the file-name */ +const namespaceMap = { + titles: "trainer-titles", + moveTriggers: "move-trigger", + abilityTriggers: "ability-trigger", + battlePokemonForm: "pokemon-form-battle", + miscDialogue: "dialogue-misc", + battleSpecDialogue: "dialogue-final-boss", + doubleBattleDialogue: "dialogue-double-battle", +}; + +//#region Functions + async function initFonts(language: string | undefined) { const results = await Promise.allSettled( fonts @@ -89,6 +100,12 @@ async function initFonts(language: string | undefined) { } } +//#region Exports + +/** + * Initialize i18n with fonts + * @returns {Promise} + */ export async function initI18n(): Promise { // Prevent reinitialization if (isInitialized) { @@ -113,6 +130,7 @@ export async function initI18n(): Promise { * A: In src/system/settings.ts, add a new case to the Setting.Language switch statement. */ + i18next.use(HttpBackend); i18next.use(LanguageDetector); i18next.use(processor); i18next.use(new KoreanPostpositionProcessor()); @@ -120,8 +138,112 @@ export async function initI18n(): Promise { nonExplicitSupportedLngs: true, fallbackLng: "en", supportedLngs: ["en", "es", "fr", "it", "de", "zh", "pt", "ko", "ja", "ca"], + backend: { + loadPath(lng: string, [ ns ]: string[]) { + let fileName: string; + if (namespaceMap[ns]) { + fileName = namespaceMap[ns]; + } else { + fileName = camelCaseToKebabCase(ns); + if (fileName.startsWith("mystery-encounters/")) { + fileName += "-dialogue"; + } + } + return `/locales/${lng}/${fileName}.json?v=${pkg.version}`; + }, + }, defaultNS: "menu", - ns: Object.keys(enConfig), + ns: [ + "ability", + "abilityTriggers", + "arenaFlyout", + "arenaTag", + "battle", + "battleScene", + "battleInfo", + "battleMessageUiHandler", + "battlePokemonForm", + "battlerTags", + "berry", + "bgmName", + "biome", + "challenges", + "commandUiHandler", + "common", + "achv", + "dialogue", + "battleSpecDialogue", + "miscDialogue", + "doubleBattleDialogue", + "egg", + "fightUiHandler", + "filterBar", + "gameMode", + "gameStatsUiHandler", + "growth", + "menu", + "menuUiHandler", + "modifier", + "modifierType", + "move", + "nature", + "pokeball", + "pokemon", + "pokemonForm", + "pokemonInfo", + "pokemonInfoContainer", + "pokemonSummary", + "saveSlotSelectUiHandler", + "settings", + "splashMessages", + "starterSelectUiHandler", + "statusEffect", + "terrain", + "titles", + "trainerClasses", + "trainerNames", + "tutorial", + "voucher", + "weather", + "partyUiHandler", + "modifierSelectUiHandler", + "moveTriggers", + "runHistory", + // DO NOT REMOVE + // "mysteryEncounter/unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + "mysteryEncounters/mysteriousChallengers", + "mysteryEncounters/mysteriousChest", + "mysteryEncounters/darkDeal", + "mysteryEncounters/fightOrFlight", + "mysteryEncounters/slumberingSnorlax", + "mysteryEncounters/trainingSession", + "mysteryEncounters/departmentStoreSale", + "mysteryEncounters/shadyVitaminDealer", + "mysteryEncounters/fieldTrip", + "mysteryEncounters/safariZone", + "mysteryEncounters/lostAtSea", + "mysteryEncounters/fieryFallout", + "mysteryEncounters/theStrongStuff", + "mysteryEncounters/pokemonSalesman", + "mysteryEncounters/offerYouCantRefuse", + "mysteryEncounters/delibirdy", + "mysteryEncounters/absoluteAvarice", + "mysteryEncounters/aTrainersTest", + "mysteryEncounters/trashToTreasure", + "mysteryEncounters/berriesAbound", + "mysteryEncounters/clowningAround", + "mysteryEncounters/partTimer", + "mysteryEncounters/dancingLessons", + "mysteryEncounters/weirdDream", + "mysteryEncounters/theWinstrateChallenge", + "mysteryEncounters/teleportingHijinks", + "mysteryEncounters/bugTypeSuperfan", + "mysteryEncounters/funAndGames", + "mysteryEncounters/uncommonBreed", + "mysteryEncounters/globalTradeSystem", + "mysteryEncounterMessages", + + ], detection: { lookupLocalStorage: "prLang" }, @@ -129,41 +251,6 @@ export async function initI18n(): Promise { interpolation: { escapeValue: false, }, - resources: { - en: { - ...enConfig - }, - es: { - ...esConfig - }, - fr: { - ...frConfig - }, - it: { - ...itConfig - }, - de: { - ...deConfig - }, - "pt-BR": { - ...ptBrConfig - }, - "zh-CN": { - ...zhCnConfig - }, - "zh-TW": { - ...zhTwConfig - }, - ko: { - ...koConfig - }, - ja: { - ...jaConfig - }, - "ca-ES": { - ...caEsConfig - } - }, postProcess: ["korean-postposition"], }); @@ -189,11 +276,10 @@ export async function initI18n(): Promise { await initFonts(localStorage.getItem("prLang") ?? undefined); } -export default i18next; - export function getIsInitialized(): boolean { return isInitialized; } -let isInitialized = false; +export default i18next; +//#endregion diff --git a/src/utils.ts b/src/utils.ts index e526d086316..989d1ec08f3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -638,3 +638,15 @@ export function isBetween(num: number, min: number, max: number): boolean { export function animationFileName(move: Moves): string { return Moves[move].toLowerCase().replace(/\_/g, "-"); } + +/** + * Transforms a camelCase string into a kebab-case string + * @param str The camelCase string + * @returns A kebab-case string + * + * @source {@link https://stackoverflow.com/a/67243723/} + */ +export function camelCaseToKebabCase(str: string): string { + console.log("str:", str); + return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase()); +}