src/i18n.ts
Rama beta · réplica de lectura del repositorio original.
import { timedEventManager } from "#app/global-event-manager";
import { namespaceMap } from "#app/i18n-namespace-map";
import { SUPPORTED_LANGUAGES } from "#system/supported-languages";
import { getCachedUrl } from "#utils/fetch-utils";
import { toKebabCase } from "#utils/strings";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";
import { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor";
// #region Interfaces/Types
interface LoadingFontFaceProperty {
face: FontFace;
extraOptions?: { [key: string]: any };
only?: string[];
}
// #endregion Interfaces/Types
// #region Constants
const unicodeRanges = {
fullwidth: "U+FF00-FFEF",
CJKCommon: "U+2E80-2EFF,U+3000-303F,U+31C0-31EF,U+3200-32FF,U+3400-4DBF,U+F900-FAFF,U+FE30-FE4F",
CJKIdeograph: "U+4E00-9FFF",
devanagari: "U+0900-097F",
thai: "U+0E00-0E7F",
specialCharacters: "U+266A,U+2605,U+2665,U+2663", // ♪, ★, ♥, ♣
};
const rangesByLanguage = {
chinese: [unicodeRanges.CJKCommon, unicodeRanges.fullwidth, unicodeRanges.CJKIdeograph].join(","),
};
const fonts: LoadingFontFaceProperty[] = [
// unicode (special characters)
{
face: new FontFace("pkmnems", `url(${getCachedUrl("./fonts/pokemon-emerald-pro.ttf")})`, {
unicodeRange: unicodeRanges.specialCharacters,
}),
extraOptions: { sizeAdjust: "133%" },
},
// unicode (chinese)
{
face: new FontFace("pkmnems", `url(${getCachedUrl("./fonts/pokemon-emerald-pro.ttf")})`, {
unicodeRange: rangesByLanguage.chinese,
}),
extraOptions: { sizeAdjust: "133%" },
only: [
"en",
"es",
"fr",
"it",
"de",
"pt",
"ko",
"ja",
"ca",
"da",
"tr",
"th",
"ru",
"uk",
"id",
"hi",
"tl",
"sv",
"eu",
"zh",
],
},
// devanagari
{
face: new FontFace("emerald", `url(${getCachedUrl("./fonts/8-bit-devanagari.ttf")})`, {
unicodeRange: unicodeRanges.devanagari,
}),
},
{
face: new FontFace("pkmnems", `url(${getCachedUrl("./fonts/8-bit-devanagari.ttf")})`, {
unicodeRange: unicodeRanges.devanagari,
}),
},
// thai
{
face: new FontFace("emerald", `url(${getCachedUrl("./fonts/rogue-thai.ttf")})`, {
unicodeRange: unicodeRanges.thai,
}),
extraOptions: { sizeAdjust: "40%" },
},
{
face: new FontFace("pkmnems", `url(${getCachedUrl("./fonts/terrible-thaifix.ttf")})`, {
unicodeRange: unicodeRanges.thai,
}),
extraOptions: { sizeAdjust: "133%" },
},
];
// #endregion Constants
// #region Functions
async function initFonts(language: string | undefined) {
const results = await Promise.allSettled(
fonts
.filter(font => !font.only || font.only.some(exclude => language?.indexOf(exclude) === 0))
.map(font => Object.assign(font.face, font.extraOptions ?? {}).load()),
);
for (const result of results) {
if (result.status === "fulfilled") {
document.fonts?.add(result.value);
} else {
console.error(result.reason);
}
}
}
/**
* I18n money formatter with. (useful for BBCode coloring of text) \
* _If you don't want the BBCode tag applied, just use 'number' formatter_
* @example Input: `{{myMoneyValue, money}}`
* Output: `@[MONEY]{₽100,000,000}`
* @param amount the money amount
* @returns a money formatted string
*/
function i18nMoneyFormatter(amount: any): string {
if (Number.isNaN(Number(amount))) {
console.warn(`i18nMoneyFormatter: value "${amount}" is not a number!`);
}
return `@[MONEY]{${i18next.t("common:money", { amount })}}`;
}
// #endregion Functions
// populated during post-processing in `plugins/vite/namespaces-i18n-plugin.ts`
const nsEn: string[] = [];
// #region Init
/*
* 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 (e.g. `menu.json`) with the translations.
* Don't forget to add an entry to `SUPPORTED_LANGUAGE_ENTRIES` in `src/system/supported-languages.ts`
*
* Q: How do I add a new namespace?
* 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 (see 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 `src/i18n-namespace-map.ts`.
*/
await i18next
.use(HttpBackend)
.use(LanguageDetector)
.use(new KoreanPostpositionProcessor())
.init(
{
fallbackLng: {
"es-419": ["es-ES", "en"],
default: ["en"],
},
supportedLngs: SUPPORTED_LANGUAGES,
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?t=1234567890"
return getCachedUrl(`./locales/${lng}/${fileName}.json`);
},
},
defaultNS: "menu",
detection: {
lookupLocalStorage: "prLang",
caches: ["localStorage"],
order: ["localStorage", "navigator"],
},
ns: nsEn,
debug: import.meta.env.VITE_I18N_DEBUG === "1",
interpolation: {
// Phaser is unable to parse the HTML escape codes produced by i18next, so turning this on results in codes like
// & being displayed verbatim on screen.
// We use i18next solely for localizing values inside code anyways, so the risk of XSS from user input is virtually nonexistent.
escapeValue: false,
// While using the `$t()` string syntax can allow nested locale keys to lazily look up interpolated text in the current chosen language,
// the fact that we reload the page after a language change renders this entirely moot - any nested lookups will be re-evaluated anyway.
// Moreover, this means of passing nested keys lacks the type-safety of calling `i18next.t` directly,
// which will be important once we get said strong typing set up correctly.
skipOnVariables: true,
},
postProcess: ["korean-postposition"],
},
async () => {
i18next.services.formatter?.add("money", i18nMoneyFormatter);
await initFonts(localStorage.getItem("prLang") ?? undefined);
},
);
// #endregion Init
// #region Event Proxy
if (timedEventManager.hasEventTextReplacement()) {
console.warn("Event text replacements are active.");
i18next.t = new Proxy(i18next.t.bind(i18next), {
apply(target, _, args: [key: string, options?: any]) {
const key = timedEventManager.getEventTextReplacement(args[0]);
if (args[0] !== key) {
console.debug(`Replacing i18n key "${args[0]}" with "${key}"`);
args[0] = key;
}
return target(...args);
},
});
}
// #endregion Event Proxy