diff --git a/web/astro.config.mjs b/web/astro.config.mjs index 80fb200..400691a 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -5,12 +5,42 @@ import tailwind from "@astrojs/tailwind"; import node from "@astrojs/node"; import { fileURLToPath } from "url"; import { dirname, resolve, join } from "path"; -import { readdirSync, existsSync } from "fs"; +import { readdirSync, existsSync, readFileSync } from "fs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const API_PORT = process.env.API_PORT || 8081; +function i18nResourcesPlugin() { + const virtualId = "virtual:i18n-resources"; + const resolvedId = "\0" + virtualId; + return { + name: "i18n-resources", + resolveId(id) { + if (id === virtualId) return resolvedId; + }, + load(id) { + if (id !== resolvedId) return; + const localesDir = join(__dirname, "public/locales"); + const resources = /** @type {Record} */ ({}); + readdirSync(localesDir, { withFileTypes: true }) + .filter( + (d) => + d.isDirectory() && + existsSync(join(localesDir, d.name, "translation.json")), + ) + .forEach((d) => { + const content = readFileSync( + join(localesDir, d.name, "translation.json"), + "utf-8", + ); + resources[d.name] = { translation: JSON.parse(content) }; + }); + return `export const resources = ${JSON.stringify(resources)};`; + }, + }; +} + function i18nLanguagesPlugin() { const virtualId = "virtual:i18n-languages"; const resolvedId = "\0" + virtualId; @@ -55,7 +85,7 @@ export default defineConfig({ defaultStrategy: "viewport", }, vite: { - plugins: [i18nLanguagesPlugin()], + plugins: [i18nResourcesPlugin(), i18nLanguagesPlugin()], resolve: { alias: { "@": resolve(__dirname, "src"), diff --git a/web/src/env.d.ts b/web/src/env.d.ts index 666fd8b..86352c1 100644 --- a/web/src/env.d.ts +++ b/web/src/env.d.ts @@ -1,5 +1,12 @@ /// +declare module "virtual:i18n-resources" { + export const resources: Record< + string, + { translation: Record } + >; +} + declare module "virtual:i18n-languages" { export const languages: { code: string; name: string; nativeName: string }[]; } diff --git a/web/src/i18n.ts b/web/src/i18n.ts index f455c43..28a0321 100644 --- a/web/src/i18n.ts +++ b/web/src/i18n.ts @@ -1,19 +1,16 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import HttpBackend from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; +import { resources } from "virtual:i18n-resources"; i18n - .use(HttpBackend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: "en", + resources, ns: ["translation"], defaultNS: "translation", - backend: { - loadPath: "/locales/{{lng}}/{{ns}}.json", - }, detection: { order: ["localStorage", "navigator", "htmlTag"], caches: ["localStorage"], @@ -21,9 +18,6 @@ i18n interpolation: { escapeValue: false, }, - react: { - useSuspense: false, - }, }); export default i18n; diff --git a/web/src/views/core/Settings.tsx b/web/src/views/core/Settings.tsx index 1ef585a..61e3796 100644 --- a/web/src/views/core/Settings.tsx +++ b/web/src/views/core/Settings.tsx @@ -86,14 +86,15 @@ export default function Settings() { const preferences = useStore($preferences); const { i18n: i18nInstance } = useTranslation(); const [currentLanguage, setCurrentLanguage] = useState( - () => i18nInstance.resolvedLanguage ?? i18nInstance.language ?? "en" + () => i18nInstance.resolvedLanguage ?? i18nInstance.language ?? "en", ); useEffect(() => { - const handler = (lng: string) => - setCurrentLanguage(lng); + const handler = (lng: string) => setCurrentLanguage(lng); i18nInstance.on("languageChanged", handler); - return () => { i18nInstance.off("languageChanged", handler); }; + return () => { + i18nInstance.off("languageChanged", handler); + }; }, [i18nInstance]); useEffect(() => {