mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
# Description of Changes We don't have any strong reasons to keep the Portal as a separate Vite app, and it needs access to so many things from the Editor that it no longer makes sense to keep them separate. This PR moves the Portal code to have direct access to the Editor code and gets rid of the shared folder.
210 lines
6.7 KiB
TypeScript
210 lines
6.7 KiB
TypeScript
import i18n from "i18next";
|
|
import { initReactI18next } from "react-i18next";
|
|
import LanguageDetector from "i18next-browser-languagedetector";
|
|
import TomlBackend from "@app/i18n/tomlBackend";
|
|
import {
|
|
supportedLanguages,
|
|
rtlLanguages,
|
|
I18N_STORAGE_KEYS,
|
|
LanguageSource,
|
|
normalizeLanguageCode,
|
|
toUnderscoreFormat,
|
|
toUnderscoreLanguages,
|
|
} from "@app/i18n/languages";
|
|
|
|
// Language metadata and code helpers are shared with the portal via
|
|
// @app/i18n. Re-export them so existing `@app/i18n` consumers are unchanged.
|
|
export {
|
|
supportedLanguages,
|
|
rtlLanguages,
|
|
I18N_STORAGE_KEYS,
|
|
LanguageSource,
|
|
normalizeLanguageCode,
|
|
toUnderscoreFormat,
|
|
toUnderscoreLanguages,
|
|
};
|
|
|
|
i18n
|
|
.use(TomlBackend)
|
|
.use(LanguageDetector)
|
|
.use(initReactI18next)
|
|
.init({
|
|
fallbackLng: "en-US",
|
|
supportedLngs: Object.keys(supportedLanguages),
|
|
load: "currentOnly",
|
|
nonExplicitSupportedLngs: false,
|
|
debug: process.env.NODE_ENV === "development",
|
|
|
|
// Ensure synchronous loading to prevent timing issues
|
|
initImmediate: false,
|
|
|
|
interpolation: {
|
|
escapeValue: false, // React already escapes values
|
|
},
|
|
|
|
backend: {
|
|
loadPath: (lngs: string[], namespaces: string[]) => {
|
|
const lng = lngs[0];
|
|
const basePath = import.meta.env.BASE_URL || "/";
|
|
const cleanBasePath = basePath.endsWith("/")
|
|
? basePath.slice(0, -1)
|
|
: basePath;
|
|
return `${cleanBasePath}/locales/${lng}/${namespaces[0]}.toml`;
|
|
},
|
|
},
|
|
|
|
detection: {
|
|
order: ["localStorage", "navigator", "htmlTag"],
|
|
caches: [], // Don't cache auto-detected language - only cache when user manually selects
|
|
convertDetectedLanguage: (lng: string) => {
|
|
// Map bare en to en-US
|
|
if (lng === "en") return "en-US";
|
|
return lng;
|
|
},
|
|
},
|
|
|
|
react: {
|
|
useSuspense: true, // Enable suspense to prevent premature rendering
|
|
bindI18n: "languageChanged loaded",
|
|
bindI18nStore: "added removed",
|
|
transEmptyNodeValue: "", // Return empty string for missing keys instead of key name
|
|
transSupportBasicHtmlNodes: true,
|
|
transKeepBasicHtmlNodesFor: ["br", "strong", "i", "p"],
|
|
},
|
|
});
|
|
|
|
// Set document direction based on language
|
|
i18n.on("languageChanged", (lng) => {
|
|
const isRTL = rtlLanguages.includes(lng);
|
|
document.documentElement.dir = isRTL ? "rtl" : "ltr";
|
|
document.documentElement.lang = lng;
|
|
});
|
|
|
|
// Track browser-detected language on first initialization
|
|
i18n.on("initialized", () => {
|
|
// If no source is set yet, mark current language as browser-detected
|
|
if (!localStorage.getItem(I18N_STORAGE_KEYS.LANGUAGE_SOURCE)) {
|
|
const detectedLang = i18n.language;
|
|
if (detectedLang) {
|
|
localStorage.setItem(I18N_STORAGE_KEYS.LANGUAGE, detectedLang);
|
|
localStorage.setItem(
|
|
I18N_STORAGE_KEYS.LANGUAGE_SOURCE,
|
|
String(LanguageSource.Browser),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get the current language source priority
|
|
*/
|
|
function getCurrentSourcePriority(): LanguageSource {
|
|
const sourceStr = localStorage.getItem(I18N_STORAGE_KEYS.LANGUAGE_SOURCE);
|
|
const sourceNum = sourceStr ? parseInt(sourceStr, 10) : null;
|
|
return sourceNum !== null && !isNaN(sourceNum)
|
|
? (sourceNum as LanguageSource)
|
|
: LanguageSource.Fallback;
|
|
}
|
|
|
|
/**
|
|
* Set language with priority tracking
|
|
* Only updates if new source has equal or higher priority than current
|
|
*/
|
|
function setLanguageWithPriority(
|
|
language: string,
|
|
source: LanguageSource,
|
|
): boolean {
|
|
const currentPriority = getCurrentSourcePriority();
|
|
const newPriority = source;
|
|
|
|
// Only apply if new source has higher priority
|
|
if (newPriority >= currentPriority) {
|
|
i18n.changeLanguage(language);
|
|
localStorage.setItem(I18N_STORAGE_KEYS.LANGUAGE, language);
|
|
localStorage.setItem(I18N_STORAGE_KEYS.LANGUAGE_SOURCE, String(source));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Set user-selected language (highest priority)
|
|
* Call this from the UI language selector
|
|
*/
|
|
export function setUserLanguage(language: string): void {
|
|
setLanguageWithPriority(language, LanguageSource.User);
|
|
}
|
|
|
|
/**
|
|
* Updates the supported languages list dynamically based on config
|
|
* If configLanguages is null/empty, all languages remain available
|
|
* Otherwise, only the specified languages are enabled with the first valid
|
|
* option (preferring en-US when present) used as the fallback language.
|
|
*
|
|
* @param configLanguages - Optional array of language codes from server config (ui.languages)
|
|
* @param defaultLocale - Optional default language for new users (system.defaultLocale)
|
|
*/
|
|
export function updateSupportedLanguages(
|
|
configLanguages?: string[] | null,
|
|
defaultLocale?: string | null,
|
|
) {
|
|
// Normalize and validate default locale if provided
|
|
const normalizedDefault = defaultLocale
|
|
? normalizeLanguageCode(defaultLocale)
|
|
: null;
|
|
const validDefault =
|
|
normalizedDefault && normalizedDefault in supportedLanguages
|
|
? normalizedDefault
|
|
: null;
|
|
|
|
if (!configLanguages || configLanguages.length === 0) {
|
|
// No filter specified - keep all languages
|
|
// But still apply default locale if provided and user has no preference
|
|
if (validDefault) {
|
|
applyDefaultLocale(validDefault);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const validLanguages = configLanguages
|
|
.map(normalizeLanguageCode)
|
|
.filter((lang) => lang in supportedLanguages);
|
|
|
|
// If no valid languages were provided, keep existing configuration
|
|
if (validLanguages.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Determine fallback: prefer validDefault if in the list, then en-US, then first valid language
|
|
const fallback =
|
|
validDefault && validLanguages.includes(validDefault)
|
|
? validDefault
|
|
: validLanguages.includes("en-US")
|
|
? "en-US"
|
|
: validLanguages[0];
|
|
|
|
i18n.options.supportedLngs = validLanguages;
|
|
i18n.options.fallbackLng = fallback;
|
|
|
|
// If current language is not in the new supported list, switch to fallback with higher priority to override browser detection
|
|
const currentLang = normalizeLanguageCode(i18n.language || "");
|
|
if (currentLang && !validLanguages.includes(currentLang)) {
|
|
// Use ServerDefault priority to override browser detection when language not in whitelist
|
|
setLanguageWithPriority(fallback, LanguageSource.ServerDefault);
|
|
} else if (validDefault) {
|
|
// Apply server default (respects user choice if already set)
|
|
setLanguageWithPriority(validDefault, LanguageSource.ServerDefault);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply server default locale when user has no saved language preference
|
|
* This respects the priority: user-selected language > defaultLocale > browser detection > fallback
|
|
*/
|
|
function applyDefaultLocale(defaultLocale: string) {
|
|
// Apply server default (respects user choice if already set)
|
|
setLanguageWithPriority(defaultLocale, LanguageSource.ServerDefault);
|
|
}
|
|
|
|
export default i18n;
|