implement: i18next language lazy-loading

This commit is contained in:
flx-sta 2024-09-19 09:30:59 -07:00
parent 685bda2b99
commit 2476e70cb6
4 changed files with 198 additions and 102 deletions

View File

@ -6,7 +6,6 @@ import { SceneBase } from "./scene-base";
import { WindowVariant, getWindowVariantSuffix } from "./ui/ui-theme"; import { WindowVariant, getWindowVariantSuffix } from "./ui/ui-theme";
import { isMobile } from "./touch-controls"; import { isMobile } from "./touch-controls";
import * as Utils from "./utils"; import * as Utils from "./utils";
import { initI18n } from "./plugins/i18n";
import { initPokemonPrevolutions } from "#app/data/pokemon-evolutions"; import { initPokemonPrevolutions } from "#app/data/pokemon-evolutions";
import { initBiomes } from "#app/data/biomes"; import { initBiomes } from "#app/data/biomes";
import { initEggMoves } from "#app/data/egg-moves"; import { initEggMoves } from "#app/data/egg-moves";
@ -33,7 +32,6 @@ export class LoadingScene extends SceneBase {
super(LoadingScene.KEY); super(LoadingScene.KEY);
Phaser.Plugins.PluginCache.register("Loader", CacheBustedLoaderPlugin, "load"); Phaser.Plugins.PluginCache.register("Loader", CacheBustedLoaderPlugin, "load");
initI18n();
} }
preload() { preload() {

View File

@ -1,12 +1,11 @@
import Phaser from "phaser"; import Phaser from "phaser";
import BattleScene from "./battle-scene";
import InvertPostFX from "./pipelines/invert"; import InvertPostFX from "./pipelines/invert";
import { version } from "../package.json"; import { version } from "../package.json";
import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin";
import BBCodeTextPlugin from "phaser3-rex-plugins/plugins/bbcodetext-plugin"; import BBCodeTextPlugin from "phaser3-rex-plugins/plugins/bbcodetext-plugin";
import InputTextPlugin from "phaser3-rex-plugins/plugins/inputtext-plugin"; import InputTextPlugin from "phaser3-rex-plugins/plugins/inputtext-plugin";
import TransitionImagePackPlugin from "phaser3-rex-plugins/templates/transitionimagepack/transitionimagepack-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. // Catch global errors and display them in an alert so users can report the issue.
@ -25,7 +24,31 @@ window.addEventListener("unhandledrejection", (event) => {
//alert(errorString); //alert(errorString);
}); });
const config: Phaser.Types.Core.GameConfig = { /**
* Sets this object's position relative to another object with a given offset
*/
const setPositionRelative = function (guideObject: Phaser.GameObjects.GameObject, x: number, y: number) {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative;
document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems"));
let game;
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, type: Phaser.WEBGL,
parent: "app", parent: "app",
scale: { scale: {
@ -69,30 +92,7 @@ const config: Phaser.Types.Core.GameConfig = {
pipeline: [ InvertPostFX ] as unknown as Phaser.Types.Core.PipelineConfig, pipeline: [ InvertPostFX ] as unknown as Phaser.Types.Core.PipelineConfig,
scene: [ LoadingScene, BattleScene ], scene: [ LoadingScene, BattleScene ],
version: version version: version
}; });
/**
* Sets this object's position relative to another object with a given offset
*/
const setPositionRelative = function (guideObject: Phaser.GameObjects.GameObject, x: number, y: number) {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative;
document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems"));
let game;
const startGame = () => {
game = new Phaser.Game(config);
game.sound.pauseOnBlur = false; game.sound.pauseOnBlur = false;
}; };

View File

@ -1,18 +1,11 @@
import i18next from "i18next"; import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import processor, { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor"; 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"; //#region Interfaces/Types
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";
interface LoadingFontFaceProperty { interface LoadingFontFaceProperty {
face: FontFace, face: FontFace,
@ -20,6 +13,10 @@ interface LoadingFontFaceProperty {
only?: Array<string> only?: Array<string>
} }
//#region Constants
let isInitialized = false;
const unicodeRanges = { const unicodeRanges = {
fullwidth: "U+FF00-FFEF", fullwidth: "U+FF00-FFEF",
hangul: "U+1100-11FF,U+3130-318F,U+A960-A97F,U+AC00-D7AF,U+D7B0-D7FF", 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", CJKIdeograph: "U+4E00-9FFF",
specialCharacters: "U+266A,U+2605,U+2665,U+2663" //♪.★,♥,♣ specialCharacters: "U+266A,U+2605,U+2665,U+2663" //♪.★,♥,♣
}; };
const rangesByLanguage = { const rangesByLanguage = {
korean: [unicodeRanges.CJKCommon, unicodeRanges.hangul].join(","), korean: [unicodeRanges.CJKCommon, unicodeRanges.hangul].join(","),
chinese: [unicodeRanges.CJKCommon, unicodeRanges.fullwidth, unicodeRanges.CJKIdeograph].join(","), chinese: [unicodeRanges.CJKCommon, unicodeRanges.fullwidth, unicodeRanges.CJKIdeograph].join(","),
@ -74,6 +72,19 @@ const fonts: Array<LoadingFontFaceProperty> = [
}, },
]; ];
/** 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) { async function initFonts(language: string | undefined) {
const results = await Promise.allSettled( const results = await Promise.allSettled(
fonts fonts
@ -89,6 +100,12 @@ async function initFonts(language: string | undefined) {
} }
} }
//#region Exports
/**
* Initialize i18n with fonts
* @returns {Promise<void>}
*/
export async function initI18n(): Promise<void> { export async function initI18n(): Promise<void> {
// Prevent reinitialization // Prevent reinitialization
if (isInitialized) { if (isInitialized) {
@ -113,6 +130,7 @@ export async function initI18n(): Promise<void> {
* A: In src/system/settings.ts, add a new case to the Setting.Language switch statement. * A: In src/system/settings.ts, add a new case to the Setting.Language switch statement.
*/ */
i18next.use(HttpBackend);
i18next.use(LanguageDetector); i18next.use(LanguageDetector);
i18next.use(processor); i18next.use(processor);
i18next.use(new KoreanPostpositionProcessor()); i18next.use(new KoreanPostpositionProcessor());
@ -120,8 +138,112 @@ export async function initI18n(): Promise<void> {
nonExplicitSupportedLngs: true, nonExplicitSupportedLngs: true,
fallbackLng: "en", fallbackLng: "en",
supportedLngs: ["en", "es", "fr", "it", "de", "zh", "pt", "ko", "ja", "ca"], 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", 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: { detection: {
lookupLocalStorage: "prLang" lookupLocalStorage: "prLang"
}, },
@ -129,41 +251,6 @@ export async function initI18n(): Promise<void> {
interpolation: { interpolation: {
escapeValue: false, 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"], postProcess: ["korean-postposition"],
}); });
@ -189,11 +276,10 @@ export async function initI18n(): Promise<void> {
await initFonts(localStorage.getItem("prLang") ?? undefined); await initFonts(localStorage.getItem("prLang") ?? undefined);
} }
export default i18next;
export function getIsInitialized(): boolean { export function getIsInitialized(): boolean {
return isInitialized; return isInitialized;
} }
let isInitialized = false; export default i18next;
//#endregion

View File

@ -638,3 +638,15 @@ export function isBetween(num: number, min: number, max: number): boolean {
export function animationFileName(move: Moves): string { export function animationFileName(move: Moves): string {
return Moves[move].toLowerCase().replace(/\_/g, "-"); 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());
}