From b9bf8fcc80bfc54d46d112650fb8b7858a37880d Mon Sep 17 00:00:00 2001 From: guenhter Date: Fri, 7 Aug 2026 09:17:06 +0200 Subject: [PATCH] feat: allow custom translations without rebuilding --- i18next-parser.config.ts | 6 +++++- src/lib/i18n/index.ts | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/i18next-parser.config.ts b/i18next-parser.config.ts index 37ce57ee14..8da8083cac 100644 --- a/i18next-parser.config.ts +++ b/i18next-parser.config.ts @@ -1,5 +1,9 @@ // i18next-parser.config.ts -import { getLanguages } from './src/lib/i18n/index.ts'; + +const getLanguages = async () => { + const languages = (await import('./src/lib/i18n/locales/languages.json')).default; + return languages; +}; const getLangCodes = async () => { const languages = await getLanguages(); diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index 520f9337a2..b133b9a629 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -3,6 +3,7 @@ import resourcesToBackend from 'i18next-resources-to-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; import type { i18n as i18nType } from 'i18next'; import { writable } from 'svelte/store'; +import { WEBUI_BASE_URL } from '$lib/constants'; const createI18nStore = (i18n: i18nType) => { const i18nWritable = writable(i18n); @@ -40,15 +41,47 @@ const createIsLoadingStore = (i18n: i18nType) => { return isLoading; }; +const loadBundledResource = async ( + language: string, + namespace: string +): Promise> => { + const mod = await import(`./locales/${language}/${namespace}.json`); + return (mod.default ?? mod) as Record; +}; + +const loadRuntimeOverrides = async ( + language: string, + namespace: string +): Promise> => { + try { + const res = await fetch(`${WEBUI_BASE_URL}/static/locales/${language}/${namespace}.json`, { + credentials: 'same-origin' + }); + if (!res.ok) { + return {}; + } + const data = await res.json(); + if (data && typeof data === 'object' && !Array.isArray(data)) { + return data as Record; + } + } catch { + // Missing override file or network error — keep bundled translations + } + return {}; +}; + +const loadResource = async (language: string, namespace: string) => { + const bundled = await loadBundledResource(language, namespace); + const overrides = await loadRuntimeOverrides(language, namespace); + return { ...bundled, ...overrides }; +}; + export const initI18n = (defaultLocale?: string | undefined) => { const detectionOrder = defaultLocale ? ['querystring', 'localStorage'] : ['querystring', 'localStorage', 'navigator']; const fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US']; - const loadResource = (language: string, namespace: string) => - import(`./locales/${language}/${namespace}.json`); - i18next .use(resourcesToBackend(loadResource)) .use(LanguageDetector)