This commit is contained in:
Adrián T. 2025-04-23 09:18:16 +08:00 committed by GitHub
commit 81dd9cc033
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 161 additions and 107 deletions

View File

@ -4,6 +4,7 @@ import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";
import processor, { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor";
import pkg from "../../package.json";
import { namespaceMap } from "./utils-plugins";
//#region Interfaces/Types
@ -90,18 +91,6 @@ 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",
splashMessages: "splash-texts",
mysteryEncounterMessages: "mystery-encounter-texts",
};
//#region Functions
@ -136,6 +125,8 @@ function i18nMoneyFormatter(amount: any): string {
return `@[MONEY]{${i18next.t("common:money", { amount })}}`;
}
const nsEn = [];
//#region Exports
/**
@ -157,7 +148,9 @@ export async function initI18n(): Promise<void> {
* 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.
* A: To add a new namespace, create a new file .json in each language folder with the translations.
* The expected format for the file-name is kebab-case {@link https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case}
* If you want the namespace name to be different from the file name, configure it in namespacemap.ts.
* Then update the config file for that language in its locale directory
* and the CustomTypeOptions interface in the @types/i18next.d.ts file.
*
@ -189,99 +182,7 @@ export async function initI18n(): Promise<void> {
},
},
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",
"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",
],
ns: nsEn, // assigned with #app/plugins/vite/namespaces-i18n-plugin.ts
detection: {
lookupLocalStorage: "prLang",
},

View File

@ -0,0 +1,50 @@
import path from "path"; // vite externalize in production, see https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility
/**
* Maps namespaces that deviate from the file-name
*
* @remarks expects file-name as value and custom-namespace as key
* */
export 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",
splashMessages: "splash-texts",
mysteryEncounterMessages: "mystery-encounter-texts",
};
/**
* Transform a kebab-case string into a camelCase string
* @param str - The kebabCase string
* @returns A camelCase string
*
* @source {@link https://stackoverflow.com/a/23013726}
*/
export function kebabCaseToCamelCase(str: string): string {
return str.replace(/-./g, x => x[1].toUpperCase());
}
/**
* Swap the value with the key and the key with the value
* @param json type {[key: string]: string}
* @returns [value]: key
*
* @source {@link https://stackoverflow.com/a/23013726}
*/
export function objectSwap(json: { [key: string]: string }): { [value: string]: string } {
const ret = {};
for (const key in json) {
ret[json[key]] = key;
}
return ret;
}
export function isFileInsideDir(file: string, dir: string): boolean {
const filePath = path.normalize(file);
const dirPath = path.normalize(dir);
return filePath.startsWith(dirPath);
}

View File

@ -0,0 +1,102 @@
import { normalizePath, type Plugin as VitePlugin } from "vite";
import fs from "fs";
import path from "path";
import "#app/plugins/utils-plugins";
import { objectSwap, namespaceMap, kebabCaseToCamelCase, isFileInsideDir } from "#app/plugins/utils-plugins";
const namespaceMapSwap = objectSwap(namespaceMap);
/**
* Crawl a directory recursively for json files to return their name with camelCase format.
* Also if file is in directory returns format "dir/fileName" format
* @param dir - The directory to crawl
*/
function getNameSpaces(dir: string): string[] {
const namespace: string[] = [];
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.lstatSync(filePath);
if (stat.isDirectory()) {
processDirectory(file, filePath, namespace);
} else if (path.extname(file) === ".json") {
processJsonFile(file, namespace);
}
}
return namespace;
}
function processDirectory(file: string, filePath: string, namespace: string[]) {
const subnamespace = getNameSpaces(filePath);
for (let i = 0; i < subnamespace.length; i++) {
let ns = subnamespace[i];
if (namespaceMapSwap[file.replace(".json", "")]) {
ns = namespaceMapSwap[file.replace(".json", "")];
} else if (kebabCaseToCamelCase(file).replace(".json", "").startsWith("mysteryEncounters")) {
ns = subnamespace[i].replace(/Dialogue$/, "");
}
// format "directory/namespace" for namespace in folder
namespace.push(`${kebabCaseToCamelCase(file).replace(".json", "")}/${ns}`);
}
}
function processJsonFile(file: string, namespace: string[]) {
let ns = kebabCaseToCamelCase(file).replace(".json", "");
if (namespaceMapSwap[file.replace(".json", "")]) {
ns = namespaceMapSwap[file.replace(".json", "")];
}
namespace.push(ns);
}
export function LocaleNamespace(): VitePlugin {
const nsRelativePath = "./public/locales";
const nsEn = nsRelativePath + "/en"; // Default namespace
let namespaces = getNameSpaces(nsEn);
const nsAbsolutePath = path.resolve(process.cwd(), nsRelativePath);
return {
name: "namespaces-i18next",
buildStart() {
if (process.env.NODE_ENV === "production") {
console.log("Collect namespaces");
}
},
configureServer(server) {
const restartHandler = async (file: string, action: string) => {
/*
* If any JSON file in nsLocation is created/modified..
* refresh the page to update the namespaces of i18next
*/
if (isFileInsideDir(file, nsAbsolutePath) && file.endsWith(".json")) {
const timestamp = new Date().toLocaleTimeString();
const filePath = nsRelativePath.replace(/^\.\/(?=.*)/, "") + normalizePath(file.replace(nsAbsolutePath, ""));
console.info(
`${timestamp} \x1b[36m\x1b[1m[ns-plugin]\x1b[0m reloading page, \x1b[32m${filePath}\x1b[0m ${action}...`,
);
namespaces = getNameSpaces(nsEn);
server.moduleGraph.invalidateAll();
server.ws.send({
type: "full-reload",
});
}
};
server.watcher
.on("change", file => restartHandler(file, "updated"))
.on("add", file => restartHandler(file, "added"))
.on("unlink", file => restartHandler(file, "removed"));
},
transform: {
handler(code, id) {
if (id.endsWith("i18n.ts")) {
return code.replace("const nsEn = [];", `const nsEn = ${JSON.stringify(namespaces)};`);
}
return code;
},
},
};
}

View File

@ -1,9 +1,10 @@
import { defineConfig, loadEnv, type Rollup, type UserConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { minifyJsonPlugin } from "./src/plugins/vite/vite-minify-json-plugin";
import { LocaleNamespace } from "./src/plugins/vite/namespaces-i18n-plugin";
export const defaultConfig: UserConfig = {
plugins: [tsconfigPaths(), minifyJsonPlugin(["images", "battle-anims"], true)],
plugins: [tsconfigPaths(), minifyJsonPlugin(["images", "battle-anims"], true), LocaleNamespace()],
clearScreen: false,
appType: "mpa",
build: {