2026-01-01 21:05:54 +00:00

259 lines
7.5 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {i18n, type Messages} from '@lingui/core';
import AppStorage from '~/lib/AppStorage';
import {getNativeLocaleIdentifier} from '~/lib/Platform';
import {messages as messagesAr} from '~/locales/ar/messages.mjs';
import {messages as messagesBg} from '~/locales/bg/messages.mjs';
import {messages as messagesCs} from '~/locales/cs/messages.mjs';
import {messages as messagesDa} from '~/locales/da/messages.mjs';
import {messages as messagesDe} from '~/locales/de/messages.mjs';
import {messages as messagesEl} from '~/locales/el/messages.mjs';
import {messages as messagesEnGB} from '~/locales/en-GB/messages.mjs';
import {messages as messagesEnUS} from '~/locales/en-US/messages.mjs';
import {messages as messagesEs419} from '~/locales/es-419/messages.mjs';
import {messages as messagesEsES} from '~/locales/es-ES/messages.mjs';
import {messages as messagesFi} from '~/locales/fi/messages.mjs';
import {messages as messagesFr} from '~/locales/fr/messages.mjs';
import {messages as messagesHe} from '~/locales/he/messages.mjs';
import {messages as messagesHi} from '~/locales/hi/messages.mjs';
import {messages as messagesHr} from '~/locales/hr/messages.mjs';
import {messages as messagesHu} from '~/locales/hu/messages.mjs';
import {messages as messagesId} from '~/locales/id/messages.mjs';
import {messages as messagesIt} from '~/locales/it/messages.mjs';
import {messages as messagesJa} from '~/locales/ja/messages.mjs';
import {messages as messagesKo} from '~/locales/ko/messages.mjs';
import {messages as messagesLt} from '~/locales/lt/messages.mjs';
import {messages as messagesNl} from '~/locales/nl/messages.mjs';
import {messages as messagesNo} from '~/locales/no/messages.mjs';
import {messages as messagesPl} from '~/locales/pl/messages.mjs';
import {messages as messagesPtBR} from '~/locales/pt-BR/messages.mjs';
import {messages as messagesRo} from '~/locales/ro/messages.mjs';
import {messages as messagesRu} from '~/locales/ru/messages.mjs';
import {messages as messagesSvSE} from '~/locales/sv-SE/messages.mjs';
import {messages as messagesTh} from '~/locales/th/messages.mjs';
import {messages as messagesTr} from '~/locales/tr/messages.mjs';
import {messages as messagesUk} from '~/locales/uk/messages.mjs';
import {messages as messagesVi} from '~/locales/vi/messages.mjs';
import {messages as messagesZhCN} from '~/locales/zh-CN/messages.mjs';
import {messages as messagesZhTW} from '~/locales/zh-TW/messages.mjs';
const supportedLocales = [
'ar',
'bg',
'cs',
'da',
'de',
'el',
'en-GB',
'en-US',
'es-ES',
'es-419',
'fi',
'fr',
'he',
'hi',
'hr',
'hu',
'id',
'it',
'ja',
'ko',
'lt',
'nl',
'no',
'pl',
'pt-BR',
'ro',
'ru',
'sv-SE',
'th',
'tr',
'uk',
'vi',
'zh-CN',
'zh-TW',
] as const;
type LocaleCode = (typeof supportedLocales)[number];
const DEFAULT_LOCALE: LocaleCode = 'en-US';
const supportedLocaleSet = new Set<LocaleCode>(supportedLocales);
const LANGUAGE_OVERRIDES: Record<string, LocaleCode> = {
en: 'en-US',
};
type LocaleLoader = () => {messages: Messages};
const loaders: Record<LocaleCode, LocaleLoader> = {
ar: () => ({messages: messagesAr}),
bg: () => ({messages: messagesBg}),
cs: () => ({messages: messagesCs}),
da: () => ({messages: messagesDa}),
de: () => ({messages: messagesDe}),
el: () => ({messages: messagesEl}),
'en-GB': () => ({messages: messagesEnGB}),
'en-US': () => ({messages: messagesEnUS}),
'es-ES': () => ({messages: messagesEsES}),
'es-419': () => ({messages: messagesEs419}),
fi: () => ({messages: messagesFi}),
fr: () => ({messages: messagesFr}),
he: () => ({messages: messagesHe}),
hi: () => ({messages: messagesHi}),
hr: () => ({messages: messagesHr}),
hu: () => ({messages: messagesHu}),
id: () => ({messages: messagesId}),
it: () => ({messages: messagesIt}),
ja: () => ({messages: messagesJa}),
ko: () => ({messages: messagesKo}),
lt: () => ({messages: messagesLt}),
nl: () => ({messages: messagesNl}),
no: () => ({messages: messagesNo}),
pl: () => ({messages: messagesPl}),
'pt-BR': () => ({messages: messagesPtBR}),
ro: () => ({messages: messagesRo}),
ru: () => ({messages: messagesRu}),
'sv-SE': () => ({messages: messagesSvSE}),
th: () => ({messages: messagesTh}),
tr: () => ({messages: messagesTr}),
uk: () => ({messages: messagesUk}),
vi: () => ({messages: messagesVi}),
'zh-CN': () => ({messages: messagesZhCN}),
'zh-TW': () => ({messages: messagesZhTW}),
};
function formatLocaleValue(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return '';
}
const segments = trimmed.split(/[-_]/).filter(Boolean);
if (segments.length === 0) {
return '';
}
const language = segments[0].toLowerCase();
if (segments.length === 1) {
return language;
}
const region = segments
.slice(1)
.map((segment) => segment.toUpperCase())
.join('-');
return `${language}-${region}`;
}
function normalizeLocale(value?: string | null): LocaleCode {
if (!value) {
return DEFAULT_LOCALE;
}
const formatted = formatLocaleValue(value);
if (!formatted) {
return DEFAULT_LOCALE;
}
if (supportedLocaleSet.has(formatted as LocaleCode)) {
return formatted as LocaleCode;
}
const [language] = formatted.split('-');
if (!language) {
return DEFAULT_LOCALE;
}
const override = LANGUAGE_OVERRIDES[language];
if (override) {
return override;
}
const fallback = supportedLocales.find((code) => code.split('-')[0].toLowerCase() === language);
if (fallback) {
return fallback;
}
return DEFAULT_LOCALE;
}
function detectBrowserLocale(): string | null {
if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
return navigator.languages[0];
}
return navigator.language ?? null;
}
function detectPreferredLocale(forceLocale?: string): LocaleCode {
if (forceLocale) {
return normalizeLocale(forceLocale);
}
const storedLocale = AppStorage.getItem('locale');
if (storedLocale) {
return normalizeLocale(storedLocale);
}
const nativeLocale = getNativeLocaleIdentifier();
if (nativeLocale) {
return normalizeLocale(nativeLocale);
}
const browserLocale = detectBrowserLocale();
if (browserLocale) {
return normalizeLocale(browserLocale);
}
return DEFAULT_LOCALE;
}
export function loadLocaleCatalog(localeCode: string): LocaleCode {
const normalized = normalizeLocale(localeCode);
const {messages} = loaders[normalized]();
i18n.loadAndActivate({locale: normalized, messages});
AppStorage.setItem('locale', normalized);
return normalized;
}
let initPromise: Promise<typeof i18n> | null = null;
export async function initI18n(forceLocale?: string) {
if (!initPromise) {
initPromise = (async () => {
try {
const localeToLoad = detectPreferredLocale(forceLocale);
loadLocaleCatalog(localeToLoad);
} catch (error) {
console.error('Failed to initialize i18n, falling back to default locale', error);
loadLocaleCatalog(DEFAULT_LOCALE);
}
return i18n;
})();
}
return initPromise;
}
export default i18n;