import i18next from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import processor, { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor"; import { caEsConfig} from "#app/locales/ca_ES/config"; import { deConfig } from "#app/locales/de/config"; import { enConfig } from "#app/locales/en/config"; import { esConfig } from "#app/locales/es/config"; import { frConfig } from "#app/locales/fr/config"; import { itConfig } from "#app/locales/it/config"; import { koConfig } from "#app/locales/ko/config"; import { jaConfig } from "#app/locales/ja/config"; import { ptBrConfig } from "#app/locales/pt_BR/config"; import { zhCnConfig } from "#app/locales/zh_CN/config"; import { zhTwConfig } from "#app/locales/zh_TW/config"; interface LoadingFontFaceProperty { face: FontFace, extraOptions?: { [key:string]: any }, only?: Array } const unicodeRanges = { fullwidth: "U+FF00-FFEF", hangul: "U+1100-11FF,U+3130-318F,U+A960-A97F,U+AC00-D7AF,U+D7B0-D7FF", kana: "U+3040-30FF", 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", 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(","), japanese: [unicodeRanges.CJKCommon, unicodeRanges.fullwidth, unicodeRanges.kana, unicodeRanges.CJKIdeograph].join(",") }; const fonts: Array = [ // unicode (special character from PokePT) { face: new FontFace("emerald", "url(./fonts/PokePT_Wansung.woff2)", { unicodeRange: unicodeRanges.specialCharacters }), }, { face: new FontFace("pkmnems", "url(./fonts/PokePT_Wansung.woff2)", { unicodeRange: unicodeRanges.specialCharacters }), extraOptions: { sizeAdjust: "133%" }, }, // unicode (korean) { face: new FontFace("emerald", "url(./fonts/PokePT_Wansung.woff2)", { unicodeRange: rangesByLanguage.korean }), }, { face: new FontFace("pkmnems", "url(./fonts/PokePT_Wansung.woff2)", { unicodeRange: rangesByLanguage.korean }), extraOptions: { sizeAdjust: "133%" }, }, // unicode (chinese) { face: new FontFace("emerald", "url(./fonts/unifont-15.1.05.subset.woff2)", { unicodeRange: rangesByLanguage.chinese }), extraOptions: { sizeAdjust: "70%", format: "woff2" }, only: [ "en", "es", "fr", "it", "de", "zh", "pt", "ko", "ca" ], }, { face: new FontFace("pkmnems", "url(./fonts/unifont-15.1.05.subset.woff2)", { unicodeRange: rangesByLanguage.chinese }), extraOptions: { format: "woff2" }, only: [ "en", "es", "fr", "it", "de", "zh", "pt", "ko", "ca" ], }, // japanese { face: new FontFace("emerald", "url(./fonts/Galmuri11.subset.woff2)", { unicodeRange: rangesByLanguage.japanese }), extraOptions: { sizeAdjust: "66%" }, only: [ "ja" ], }, { face: new FontFace("pkmnems", "url(./fonts/Galmuri9.subset.woff2)", { unicodeRange: rangesByLanguage.japanese }), only: [ "ja" ], }, ]; 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); } } } export async function initI18n(): Promise { // Prevent reinitialization if (isInitialized) { return; } isInitialized = true; /** * 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. */ i18next.use(LanguageDetector); i18next.use(processor); i18next.use(new KoreanPostpositionProcessor()); await i18next.init({ nonExplicitSupportedLngs: true, fallbackLng: "en", supportedLngs: ["en", "es", "fr", "it", "de", "zh", "pt", "ko", "ja", "ca"], defaultNS: "menu", ns: Object.keys(enConfig), detection: { lookupLocalStorage: "prLang" }, debug: Number(import.meta.env.VITE_I18N_DEBUG) === 1, 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"], }); // Input: {{myMoneyValue, money}} // Output: @[MONEY]{₽100,000,000} (useful for BBCode coloring of text) // If you don't want the BBCode tag applied, just use 'number' formatter i18next.services.formatter?.add("money", (value, lng, options) => { const numberFormattedString = Intl.NumberFormat(lng, options).format(value); switch (lng) { case "ja": return `@[MONEY]{${numberFormattedString}}円`; case "de": case "es": case "fr": case "it": return `@[MONEY]{${numberFormattedString} ₽}`; default: // English and other languages that use same format return `@[MONEY]{₽${numberFormattedString}}`; } }); await initFonts(localStorage.getItem("prLang") ?? undefined); } export default i18next; export function getIsInitialized(): boolean { return isInitialized; } let isInitialized = false;