This commit is contained in:
Bertie690 2025-09-21 15:52:39 -04:00 committed by GitHub
commit c18640078c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 179 additions and 266 deletions

View File

@ -22,7 +22,6 @@ import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon } from "#field/pokemon"; import type { EnemyPokemon } from "#field/pokemon";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { getIsInitialized, initI18n } from "#plugins/i18n";
import type { EvilTeam } from "#trainers/evil-admin-trainer-pools"; import type { EvilTeam } from "#trainers/evil-admin-trainer-pools";
import { evilAdminTrainerPools } from "#trainers/evil-admin-trainer-pools"; import { evilAdminTrainerPools } from "#trainers/evil-admin-trainer-pools";
import { import {
@ -176,14 +175,8 @@ export class TrainerConfig {
setName(name: string): TrainerConfig { setName(name: string): TrainerConfig {
if (name === "Finn") { if (name === "Finn") {
// Give the rival a localized name // Give the rival a localized name
// First check if i18n is initialized
if (!getIsInitialized()) {
initI18n();
}
// This is only the male name, because the female name is handled in a different function (setHasGenders) // This is only the male name, because the female name is handled in a different function (setHasGenders)
if (name === "Finn") { name = i18next.t("trainerNames:rival");
name = i18next.t("trainerNames:rival");
}
} }
this.name = name; this.name = name;
@ -200,11 +193,6 @@ export class TrainerConfig {
} }
setTitle(title: string): TrainerConfig { setTitle(title: string): TrainerConfig {
// First check if i18n is initialized
if (!getIsInitialized()) {
initI18n();
}
title = toCamelCase(title); title = toCamelCase(title);
// Get the title from the i18n file // Get the title from the i18n file
@ -293,11 +281,6 @@ export class TrainerConfig {
setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig { setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig {
// If the female name is 'Ivy' (the rival), assign a localized name. // If the female name is 'Ivy' (the rival), assign a localized name.
if (nameFemale === "Ivy") { if (nameFemale === "Ivy") {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
// Initialize the i18n system if it is not already initialized.
initI18n();
}
// Set the localized name for the female rival. // Set the localized name for the female rival.
this.nameFemale = i18next.t("trainerNames:rivalFemale"); this.nameFemale = i18next.t("trainerNames:rivalFemale");
} else { } else {
@ -371,11 +354,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance. * @returns The updated TrainerConfig instance.
*/ */
setDoubleTitle(titleDouble: string): TrainerConfig { setDoubleTitle(titleDouble: string): TrainerConfig {
// First check if i18n is initialized
if (!getIsInitialized()) {
initI18n();
}
titleDouble = toCamelCase(titleDouble); titleDouble = toCamelCase(titleDouble);
// Get the title from the i18n file // Get the title from the i18n file
@ -564,10 +542,6 @@ export class TrainerConfig {
signatureSpecies: (SpeciesId | SpeciesId[])[], signatureSpecies: (SpeciesId | SpeciesId[])[],
specialtyType?: PokemonType, specialtyType?: PokemonType,
): TrainerConfig { ): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
}
if (specialtyType != null) { if (specialtyType != null) {
this.setSpecialtyType(specialtyType); this.setSpecialtyType(specialtyType);
} }
@ -600,10 +574,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance. * @returns The updated TrainerConfig instance.
*/ */
initForStatTrainer(_isMale = false): TrainerConfig { initForStatTrainer(_isMale = false): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
}
this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR); this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR);
const nameForCall = toCamelCase(this.name); const nameForCall = toCamelCase(this.name);
@ -632,9 +602,6 @@ export class TrainerConfig {
rematch = false, rematch = false,
specialtyType?: PokemonType, specialtyType?: PokemonType,
): TrainerConfig { ): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
}
if (rematch) { if (rematch) {
this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR); this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR);
} else { } else {
@ -676,11 +643,6 @@ export class TrainerConfig {
ignoreMinTeraWave = false, ignoreMinTeraWave = false,
teraSlot?: number, teraSlot?: number,
): TrainerConfig { ): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
// Set the function to generate the Gym Leader's party template. // Set the function to generate the Gym Leader's party template.
this.setPartyTemplateFunc(getGymLeaderPartyTemplate); this.setPartyTemplateFunc(getGymLeaderPartyTemplate);
@ -733,11 +695,6 @@ export class TrainerConfig {
specialtyType?: PokemonType, specialtyType?: PokemonType,
teraSlot?: number, teraSlot?: number,
): TrainerConfig { ): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
// Set the party templates for the Elite Four. // Set the party templates for the Elite Four.
this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR); this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR);
@ -784,11 +741,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance. * @returns The updated TrainerConfig instance.
*/ */
initForChampion(isMale: boolean): TrainerConfig { initForChampion(isMale: boolean): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
// Set the party templates for the Champion. // Set the party templates for the Champion.
this.setPartyTemplates(trainerPartyTemplates.CHAMPION); this.setPartyTemplates(trainerPartyTemplates.CHAMPION);
@ -819,10 +771,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance. * @returns The updated TrainerConfig instance.
*/ */
setLocalizedName(name: string): TrainerConfig { setLocalizedName(name: string): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
this.name = i18next.t(`trainerNames:${toCamelCase(name)}`); this.name = i18next.t(`trainerNames:${toCamelCase(name)}`);
return this; return this;
} }
@ -854,15 +802,9 @@ export class TrainerConfig {
} }
} }
// Check if !variant is true, if so return the name, else return the name with _female appended // Check if !variant is true, if so return the name, else return the name with _female appended
else if (variant) { // Check if the female version exists in the i18n file
if (!getIsInitialized()) { else if (variant && i18next.exists(`trainerClasses:${toCamelCase(this.name)}Female`)) {
initI18n(); return ret + "Female";
}
// Check if the female version exists in the i18n file
if (i18next.exists(`trainerClasses:${toCamelCase(this.name)}Female`)) {
// If it does, return
return ret + "Female";
}
} }
} }

View File

@ -13,7 +13,6 @@ import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon } from "#field/pokemon"; import type { EnemyPokemon } from "#field/pokemon";
import type { PersistentModifier } from "#modifiers/modifier"; import type { PersistentModifier } from "#modifiers/modifier";
import { getIsInitialized, initI18n } from "#plugins/i18n";
import type { TrainerConfig } from "#trainers/trainer-config"; import type { TrainerConfig } from "#trainers/trainer-config";
import { trainerConfigs } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config";
import { TrainerPartyCompoundTemplate, type TrainerPartyTemplate } from "#trainers/trainer-party-template"; import { TrainerPartyCompoundTemplate, type TrainerPartyTemplate } from "#trainers/trainer-party-template";
@ -174,11 +173,6 @@ export class Trainer extends Phaser.GameObjects.Container {
if (this.name) { if (this.name) {
// If the title should be included. // If the title should be included.
if (includeTitle) { if (includeTitle) {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
// Initialize the i18n system if it is not already initialized.
initI18n();
}
// Get the localized trainer class name from the i18n file and set it as the title. // Get the localized trainer class name from the i18n file and set it as the title.
// This is used for trainer class names, not titles like "Elite Four, Champion, etc." // This is used for trainer class names, not titles like "Elite Four, Champion, etc."
title = i18next.t(`trainerClasses:${toCamelCase(name)}`); title = i18next.t(`trainerClasses:${toCamelCase(name)}`);

View File

@ -1,8 +1,9 @@
import "#app/polyfills";
// All polyfills MUST be loaded first for side effects // All polyfills MUST be loaded first for side effects
import "#app/polyfills";
// Initializes i18n on import
import "#app/plugins/i18n";
import { InvertPostFX } from "#app/pipelines/invert"; import { InvertPostFX } from "#app/pipelines/invert";
import { initI18n } from "#app/plugins/i18n";
import { version } from "#package.json"; import { version } from "#package.json";
import Phaser from "phaser"; import Phaser from "phaser";
import BBCodeTextPlugin from "phaser3-rex-plugins/plugins/bbcodetext-plugin"; import BBCodeTextPlugin from "phaser3-rex-plugins/plugins/bbcodetext-plugin";
@ -53,7 +54,6 @@ let game;
let manifest; let manifest;
const startGame = async () => { const startGame = async () => {
await initI18n();
const LoadingScene = (await import("./loading-scene")).LoadingScene; const LoadingScene = (await import("./loading-scene")).LoadingScene;
const BattleScene = (await import("./battle-scene")).BattleScene; const BattleScene = (await import("./battle-scene")).BattleScene;
game = new Phaser.Game({ game = new Phaser.Game({

View File

@ -3,7 +3,7 @@ import { toKebabCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend"; import HttpBackend from "i18next-http-backend";
import processor, { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor"; import processor from "i18next-korean-postposition-processor";
//#region Interfaces/Types //#region Interfaces/Types
@ -15,8 +15,6 @@ interface LoadingFontFaceProperty {
//#region Constants //#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",
@ -136,193 +134,179 @@ function i18nMoneyFormatter(amount: any): string {
return `@[MONEY]{${i18next.t("common:money", { amount })}}`; return `@[MONEY]{${i18next.t("common:money", { amount })}}`;
} }
//#region Exports //#region Initialization
/** /*
* Initialize i18n with fonts * 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.
* Don't forget to declare new language in `supportedLngs` i18next initializer
*
* 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 config file for that language in its locale directory
* and the CustomTypeOptions interface in the @types/i18next.d.ts file.
*
* Q: How do I make a language selectable in the settings?
* A: In src/system/settings.ts, add a new case to the Setting.Language switch statement.
*/ */
export async function initI18n(): Promise<void> {
// Prevent reinitialization
if (isInitialized) {
return;
}
isInitialized = true;
/** await i18next
* i18next is a localization library for maintaining and using translation resources. .use(HttpBackend)
* .use(LanguageDetector)
* Q: How do I add a new language? .use(processor)
* A: To add a new language, create a new folder in the locales directory with the language code. .init(
* Each language folder should contain a file for each namespace (ex. menu.ts) with the translations. {
* Don't forget to declare new language in `supportedLngs` i18next initializer fallbackLng: {
* "es-MX": ["es-ES", "en"],
* Q: How do I add a new namespace? default: ["en"],
* A: To add a new namespace, create a new file in each language folder with the translations.
* Then update the config file for that language in its locale directory
* and the CustomTypeOptions interface in the @types/i18next.d.ts file.
*
* Q: How do I make a language selectable in the settings?
* 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());
await i18next.init({
fallbackLng: {
"es-MX": ["es-ES", "en"],
default: ["en"],
},
supportedLngs: [
"en",
"es-ES",
"es-MX",
"fr",
"it",
"de",
"zh-CN",
"zh-TW",
"pt-BR",
"ko",
"ja",
"ca",
"da",
"tr",
"ro",
"ru",
"tl",
],
backend: {
loadPath(lng: string, [ns]: string[]) {
// Use namespace maps where required
let fileName: string;
if (namespaceMap[ns]) {
fileName = namespaceMap[ns];
} else if (ns.startsWith("mysteryEncounters/")) {
fileName = toKebabCase(ns + "-dialogue"); // mystery-encounters/a-trainers-test-dialogue
} else {
fileName = toKebabCase(ns);
}
// ex: "./locales/en/move-anims"
return `./locales/${lng}/${fileName}.json?v=${pkg.version}`;
}, },
supportedLngs: [
"en",
"es-ES",
"es-MX",
"fr",
"it",
"de",
"zh-CN",
"zh-TW",
"pt-BR",
"ko",
"ja",
"ca",
"da",
"tr",
"ro",
"ru",
],
backend: {
loadPath(lng: string, [ns]: string[]) {
// Use namespace maps where required
let fileName: string;
if (namespaceMap[ns]) {
fileName = namespaceMap[ns];
} else if (ns.startsWith("mysteryEncounters/")) {
fileName = toKebabCase(ns + "-dialogue"); // mystery-encounters/a-trainers-test-dialogue
} else {
fileName = toKebabCase(ns);
}
// ex: "./locales/en/move-anims"
return `./locales/${lng}/${fileName}.json?v=${pkg.version}`;
},
},
defaultNS: "menu",
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",
"filterText",
"gameMode",
"gameStatsUiHandler",
"growth",
"menu",
"menuUiHandler",
"modifier",
"modifierType",
"move",
"nature",
"pokeball",
"pokedexUiHandler",
"pokemon",
"pokemonCategory",
"pokemonEvolutions",
"pokemonForm",
"pokemonInfo",
"pokemonInfoContainer",
"pokemonSummary",
"saveSlotSelectUiHandler",
"settings",
"splashMessages",
"starterSelectUiHandler",
"statusEffect",
"terrain",
"titles",
"trainerClasses",
"trainersCommon",
"trainerNames",
"tutorial",
"voucher",
"weather",
"partyUiHandler",
"modifierSelectUiHandler",
"moveTriggers",
"runHistory",
"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/thePokemonSalesman",
"mysteryEncounters/anOfferYouCantRefuse",
"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",
"mysteryEncounters/theExpertPokemonBreeder",
"mysteryEncounterMessages",
],
detection: {
lookupLocalStorage: "prLang",
},
debug: import.meta.env.VITE_I18N_DEBUG === "1",
interpolation: {
escapeValue: false,
},
postProcess: ["korean-postposition"],
}, },
defaultNS: "menu", async () => {
ns: [ if (i18next.services.formatter) {
"ability", i18next.services.formatter.add("money", i18nMoneyFormatter);
"abilityTriggers", }
"arenaFlyout", await initFonts(localStorage.getItem("prLang") ?? undefined);
"arenaTag",
"battle",
"battleScene",
"battleInfo",
"battleMessageUiHandler",
"battlePokemonForm",
"battlerTags",
"berry",
"bgmName",
"biome",
"challenges",
"commandUiHandler",
"common",
"achv",
"dialogue",
"battleSpecDialogue",
"miscDialogue",
"doubleBattleDialogue",
"egg",
"fightUiHandler",
"filterBar",
"filterText",
"gameMode",
"gameStatsUiHandler",
"growth",
"menu",
"menuUiHandler",
"modifier",
"modifierType",
"move",
"nature",
"pokeball",
"pokedexUiHandler",
"pokemon",
"pokemonCategory",
"pokemonEvolutions",
"pokemonForm",
"pokemonInfo",
"pokemonInfoContainer",
"pokemonSummary",
"saveSlotSelectUiHandler",
"settings",
"splashMessages",
"starterSelectUiHandler",
"statusEffect",
"terrain",
"titles",
"trainerClasses",
"trainersCommon",
"trainerNames",
"tutorial",
"voucher",
"weather",
"partyUiHandler",
"modifierSelectUiHandler",
"moveTriggers",
"runHistory",
"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/thePokemonSalesman",
"mysteryEncounters/anOfferYouCantRefuse",
"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",
"mysteryEncounters/theExpertPokemonBreeder",
"mysteryEncounterMessages",
],
detection: {
lookupLocalStorage: "prLang",
}, },
debug: Number(import.meta.env.VITE_I18N_DEBUG) === 1, );
interpolation: {
escapeValue: false,
},
postProcess: ["korean-postposition"],
});
if (i18next.services.formatter) {
i18next.services.formatter.add("money", i18nMoneyFormatter);
}
await initFonts(localStorage.getItem("prLang") ?? undefined);
}
export function getIsInitialized(): boolean {
return isInitialized;
}
export default i18next; export default i18next;

View File

@ -1,4 +1,6 @@
import "vitest-canvas-mock"; import "vitest-canvas-mock";
import "#plugins/i18n"; // Initializes i18n on import
import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console"; import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console";
import { logTestEnd, logTestStart } from "#test/test-utils/setup/test-end-log"; import { logTestEnd, logTestStart } from "#test/test-utils/setup/test-end-log";
import { initTests } from "#test/test-utils/test-file-initialization"; import { initTests } from "#test/test-utils/test-file-initialization";

View File

@ -1,6 +1,5 @@
import { SESSION_ID_COOKIE_NAME } from "#app/constants"; import { SESSION_ID_COOKIE_NAME } from "#app/constants";
import { initializeGame } from "#app/init/init"; import { initializeGame } from "#app/init/init";
import { initI18n } from "#plugins/i18n";
import { blobToString } from "#test/test-utils/game-manager-utils"; import { blobToString } from "#test/test-utils/game-manager-utils";
import { manageListeners } from "#test/test-utils/listeners-manager"; import { manageListeners } from "#test/test-utils/listeners-manager";
import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console"; import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console";
@ -15,26 +14,18 @@ import InputText from "phaser3-rex-plugins/plugins/inputtext";
let wasInitialized = false; let wasInitialized = false;
/** /**
* Run initialization code upon starting a new file, both per-suite and per-instance oncess. * Run initialization code upon starting a new file, both per-suite and per-instance ones.
*/ */
export function initTests(): void { export function initTests(): void {
setupStubs(); setupStubs();
if (!wasInitialized) { if (!wasInitialized) {
initTestFile(); initializeGame();
wasInitialized = true; wasInitialized = true;
} }
manageListeners(); manageListeners();
} }
/**
* Initialize various values at the beginning of each testing instance.
*/
function initTestFile(): void {
initI18n();
initializeGame();
}
/** /**
* Setup various stubs for testing. * Setup various stubs for testing.
* @todo Move this into a dedicated stub file instead of running it once per test instance * @todo Move this into a dedicated stub file instead of running it once per test instance