| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- import i18n, { type BackendModule, type ResourceLanguage } from 'i18next';
- import { initReactI18next } from 'react-i18next';
- import LanguageDetector from 'i18next-browser-languagedetector';
- import { recoverFromMissingChunk } from '../utils/lazyPage';
- // English is bundled: it is the fallback for every missing key. The other
- // languages are separate files, and only the one in use is loaded (#3175).
- import en from './locales/en';
- const LOCALE_LOADERS: Record<string, () => Promise<{ default: ResourceLanguage }>> = {
- de: () => import('./locales/de'),
- es: () => import('./locales/es'),
- fr: () => import('./locales/fr'),
- ja: () => import('./locales/ja'),
- it: () => import('./locales/it'),
- nl: () => import('./locales/nl'),
- ko: () => import('./locales/ko'),
- 'pt-BR': () => import('./locales/pt-BR'),
- 'zh-CN': () => import('./locales/zh-CN'),
- 'zh-TW': () => import('./locales/zh-TW'),
- sv: () => import('./locales/sv'),
- tr: () => import('./locales/tr'),
- ru: () => import('./locales/ru'),
- uk: () => import('./locales/uk'),
- };
- /** Hands i18next a language's file when it needs that language. i18next
- * loads it before switching, so the interface never shows a half-loaded
- * language. */
- export const localeBackend: BackendModule = {
- type: 'backend',
- init() {},
- read(language, _namespace, callback) {
- const load = LOCALE_LOADERS[language];
- if (!load) {
- callback(null, {});
- return;
- }
- load().then(
- (module) => callback(null, module.default),
- (error) => {
- // A tab from before an update asks for a file the update deleted.
- void recoverFromMissingChunk().then((reloading) => {
- if (!reloading) callback(error, null);
- });
- },
- );
- },
- };
- const SUPPORTED_LNGS = ['en', 'de', 'es', 'fr', 'ja', 'it', 'ko', 'nl', 'pt-BR', 'ru', 'sv', 'tr', 'uk', 'zh-CN', 'zh-TW'];
- const APPLIANCE_CONSUMED_KEY = 'bambuddy_appliance_locale_consumed';
- /** Settles once the language in use has loaded (or failed to). */
- export const i18nReady = i18n
- .use(localeBackend)
- .use(LanguageDetector)
- .use(initReactI18next)
- .init({
- resources: { en: { translation: en } },
- // Load every other language through localeBackend.
- partialBundledLanguages: true,
- fallbackLng: 'en',
- supportedLngs: SUPPORTED_LNGS,
- detection: {
- // Order of detection methods
- order: ['localStorage', 'navigator', 'htmlTag'],
- // Key to use in localStorage
- lookupLocalStorage: 'bambutrack_language',
- // Cache user language
- caches: ['localStorage'],
- },
- interpolation: {
- escapeValue: false, // React already escapes
- },
- react: {
- useSuspense: false,
- },
- });
- /**
- * Bambuddy Appliance hook: on the first SPA load after the firstboot wizard
- * runs, /api/v1/system/appliance returns the locale the user picked. We
- * apply it once (gated by a localStorage flag) and stop. On non-appliance
- * installs the endpoint either 404s or returns nulls — silent no-op.
- *
- * This runs AFTER i18n.init so the LanguageDetector has already populated a
- * default; we override that default exactly once for fresh appliances. The
- * appliance is then "consumed" and the language picker is the only way to
- * change locale going forward (the wizard ran once; future intent comes from
- * the running UI).
- */
- function applyApplianceLocale() {
- if (typeof window === 'undefined' || !window.localStorage) return;
- const storage = window.localStorage;
- if (typeof storage.getItem !== 'function' || typeof storage.setItem !== 'function') return;
- if (storage.getItem(APPLIANCE_CONSUMED_KEY)) return;
- fetch('/api/v1/system/appliance')
- .then((r) => (r.ok ? r.json() : null))
- .then((data) => {
- if (!data || typeof data.locale !== 'string') return;
- if (!SUPPORTED_LNGS.includes(data.locale)) return;
- i18n.changeLanguage(data.locale);
- storage.setItem(APPLIANCE_CONSUMED_KEY, '1');
- })
- .catch(() => {
- // Endpoint absent or unreachable — non-appliance install or dev environment.
- // Leave the detector's choice in place.
- });
- }
- applyApplianceLocale();
- export default i18n;
- // Helper to get available languages
- export const availableLanguages = [
- { code: 'en', name: 'English', nativeName: 'English' },
- { code: 'de', name: 'German', nativeName: 'Deutsch' },
- { code: 'es', name: 'Spanish', nativeName: 'Español' },
- { code: 'fr', name: 'French', nativeName: 'Français' },
- { code: 'ja', name: 'Japanese', nativeName: '日本語' },
- { code: 'it', name: 'Italian', nativeName: 'Italiano' },
- { code: 'ko', name: 'Korean', nativeName: '한국어' },
- { code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
- { code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)' },
- { code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文' },
- { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文' },
- { code: 'sv', name: 'Swedish', nativeName: 'Svenska' },
- { code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
- { code: 'ru', name: 'Russian', nativeName: 'Русский' },
- { code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
- ];
|