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 { 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() {

View File

@ -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,7 +24,31 @@ window.addEventListener("unhandledrejection", (event) => {
//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,
parent: "app",
scale: {
@ -69,30 +92,7 @@ const config: Phaser.Types.Core.GameConfig = {
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
*/
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;
};

View File

@ -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<string>
}
//#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<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) {
const results = await Promise.allSettled(
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> {
// Prevent reinitialization
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.
*/
i18next.use(HttpBackend);
i18next.use(LanguageDetector);
i18next.use(processor);
i18next.use(new KoreanPostpositionProcessor());
@ -120,8 +138,112 @@ export async function initI18n(): Promise<void> {
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<void> {
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<void> {
await initFonts(localStorage.getItem("prLang") ?? undefined);
}
export default i18next;
export function getIsInitialized(): boolean {
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 {
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());
}