Ctrl K
ComunidadGuías…

src/i18n.ts

Rama beta · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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