From fb3b59210ea1f54af234653b5146ba8a9d4bb8cb Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 12 Sep 2024 12:53:04 +0800 Subject: [PATCH] fix: set language when init Signed-off-by: Innei --- configs/vite.render.config.ts | 1 + locales/namespaces/common/en.json | 5 +- locales/namespaces/common/zh_CN.json | 5 +- src/renderer/src/@types/default-resource.ts | 13 +++ src/renderer/src/i18n.ts | 56 +-------- src/renderer/src/providers/i18n-provider.tsx | 113 +++++++++++++++++-- tsconfig.web.json | 3 +- 7 files changed, 134 insertions(+), 62 deletions(-) diff --git a/configs/vite.render.config.ts b/configs/vite.render.config.ts index 9d543f12d..7173d08e2 100644 --- a/configs/vite.render.config.ts +++ b/configs/vite.render.config.ts @@ -22,6 +22,7 @@ export const viteRenderBaseConfig = { "@shared": resolve("src/shared/src"), "@pkg": resolve("./package.json"), "@env": resolve("./src/env.ts"), + "@locales": resolve("./locales"), }, }, base: "/", diff --git a/locales/namespaces/common/en.json b/locales/namespaces/common/en.json index 75966ec17..434f2f1c1 100644 --- a/locales/namespaces/common/en.json +++ b/locales/namespaces/common/en.json @@ -1,5 +1,8 @@ { "confirm": "Confirm", "ok": "OK", - "cancel": "Cancel" + "cancel": "Cancel", + "tips": { + "load-lng-error": "Failed to load language pack" + } } diff --git a/locales/namespaces/common/zh_CN.json b/locales/namespaces/common/zh_CN.json index cdeef8285..b07eacdbf 100644 --- a/locales/namespaces/common/zh_CN.json +++ b/locales/namespaces/common/zh_CN.json @@ -1,5 +1,8 @@ { "confirm": "确认", "ok": "好", - "cancel": "取消" + "cancel": "取消", + "tips": { + "load-lng-error": "加载语言包失败" + } } diff --git a/src/renderer/src/@types/default-resource.ts b/src/renderer/src/@types/default-resource.ts index 6795ad950..aad03bde4 100644 --- a/src/renderer/src/@types/default-resource.ts +++ b/src/renderer/src/@types/default-resource.ts @@ -1,11 +1,24 @@ import en from "../../../../locales/en.json" import common_en from "../../../../locales/namespaces/common/en.json" +import common_zhCN from "../../../../locales/namespaces/common/zh_CN.json" import lang_en from "../../../../locales/namespaces/lang/en.json" +/** + * This file is the language resource that is loaded in full when the app is initialized. + * When switching languages, the app will automatically download the required language resources, + * we will not load all the language resources to minimize the first screen loading time of the app. + * Generally, we only load english resources synchronously by default. + * In addition, we attach common resources for other languages, and the size of the common resources must be controlled. + */ + export const defaultResources = { en: { translation: en, lang: lang_en, common: common_en, }, + + zh_CN: { + common: common_zhCN, + }, } diff --git a/src/renderer/src/i18n.ts b/src/renderer/src/i18n.ts index 179376e5b..e5fc3b8cb 100644 --- a/src/renderer/src/i18n.ts +++ b/src/renderer/src/i18n.ts @@ -1,16 +1,18 @@ -import { getGeneralSettings } from "@renderer/atoms/settings/general" import { EventBus } from "@renderer/lib/event-bus" import i18next from "i18next" +import { atom } from "jotai" import { initReactI18next } from "react-i18next" -import { currentSupportedLanguages } from "./@types/constants" import { defaultResources } from "./@types/default-resource" +import { jotaiStore } from "./lib/jotai" + +export const i18nAtom = atom(i18next) export const defaultNS = "translation" -const loadingLangLock = new Set() export const fallbackLanguage = "en" export const initI18n = async () => { + const i18next = jotaiStore.get(i18nAtom) await i18next.use(initReactI18next).init({ lng: "en", fallbackLng: fallbackLanguage, @@ -18,58 +20,12 @@ export const initI18n = async () => { debug: import.meta.env.DEV, resources: defaultResources, }) - - i18next.on("languageChanged", async (lang) => { - if (loadingLangLock.has(lang)) return - const isSupport = currentSupportedLanguages.includes(lang) - if (!isSupport) { - return - } - const loaded = i18next.getResourceBundle(lang, defaultNS) - - if (loaded) return - - loadingLangLock.add(lang) - - const rootGlobbyMap = import.meta.glob("../../../locales/*.json") - const nsGlobbyMap = import.meta.glob("../../../locales/namespaces/*/*.json") - - const rootResources = await rootGlobbyMap[`../../../locales/${lang}.json`]().then( - (m: any) => m.default, - ) - - i18next.addResourceBundle(lang, defaultNS, rootResources, true, true) - - const namespaces = Object.keys(defaultResources.en) - - await Promise.all( - namespaces.map(async (ns) => { - if (ns === defaultNS) return - - const loader = nsGlobbyMap[`../../../locales/namespaces/${ns}/${lang}.json`] - - if (!loader) return - const nsResources = await loader().then((m: any) => m.default) - - i18next.addResourceBundle(lang, ns, nsResources, true, true) - }), - ) - - await i18next.reloadResources() - await i18next.changeLanguage(lang) - loadingLangLock.delete(lang) - EventBus.dispatch("I18N_UPDATE", "") - }) - - const { language } = getGeneralSettings() - - i18next.changeLanguage(language) } if (import.meta.hot) { import.meta.hot.on("i18n-update", ({ file, content }: { file: string; content: string }) => { const resources = JSON.parse(content) - + const i18next = jotaiStore.get(i18nAtom) // `file` is absolute path e.g. /Users/innei/git/follow/locales/en.json // Absolute path e.g. /Users/innei/git/follow/locales/namespaces//en.json diff --git a/src/renderer/src/providers/i18n-provider.tsx b/src/renderer/src/providers/i18n-provider.tsx index 0854aaef7..d27b4ad8b 100644 --- a/src/renderer/src/providers/i18n-provider.tsx +++ b/src/renderer/src/providers/i18n-provider.tsx @@ -1,18 +1,113 @@ +import { defaultResources } from "@renderer/@types/default-resource" +import { getGeneralSettings } from "@renderer/atoms/settings/general" +import { currentSupportedLanguages, defaultNS, i18nAtom } from "@renderer/i18n" +import { nextFrame } from "@renderer/lib/dom" import { EventBus } from "@renderer/lib/event-bus" +import { jotaiStore } from "@renderer/lib/jotai" import i18next from "i18next" +import { useAtom } from "jotai" import type { FC, PropsWithChildren } from "react" -import { useEffect, useState } from "react" +import { useEffect, useLayoutEffect, useRef } from "react" import { I18nextProvider } from "react-i18next" +import { toast } from "sonner" -export const I18nProvider: FC = ({ children }) => { - const [currentI18NInstance, update] = useState(i18next) +const loadingLangLock = new Set() - useEffect( - () => - EventBus.subscribe("I18N_UPDATE", () => { - update(i18next.cloneInstance()) - }), - [], +const langChangedHandler = async (lang: string) => { + const { t } = jotaiStore.get(i18nAtom) + if (loadingLangLock.has(lang)) return + const isSupport = currentSupportedLanguages.includes(lang) + if (!isSupport) { + return + } + const loaded = i18next.getResourceBundle(lang, defaultNS) + + if (loaded) { + EventBus.dispatch("I18N_UPDATE", lang) + return + } + + loadingLangLock.add(lang) + + const rootGlobbyMap = import.meta.glob("@locales/*.json") + const nsGlobbyMap = import.meta.glob("@locales/namespaces/*/*.json") + + const rootResources = await rootGlobbyMap[`../../locales/${lang}.json`]() + .then((m: any) => m.default) + .catch(() => { + toast.error(`${t("common:tips.load-lng-error")}: ${lang}`) + }) + + i18next.addResourceBundle(lang, defaultNS, rootResources, true, true) + + const namespaces = Object.keys(defaultResources.en) + + const res = await Promise.allSettled( + namespaces.map(async (ns) => { + if (ns === defaultNS) return + + const loader = nsGlobbyMap[`../../locales/namespaces/${ns}/${lang}.json`] + + if (!loader) return + const nsResources = await loader().then((m: any) => m.default) + + i18next.addResourceBundle(lang, ns, nsResources, true, true) + }), ) + + for (const r of res) { + if (r.status === "rejected") { + toast.error(`${t("common:tips.load-lng-error")}: ${lang}`) + loadingLangLock.delete(lang) + + return + } + } + + await i18next.reloadResources() + await i18next.changeLanguage(lang) + loadingLangLock.delete(lang) +} + +langChangedHandler(getGeneralSettings().language) +export const I18nProvider: FC = ({ children }) => { + const [currentI18NInstance, update] = useAtom(i18nAtom) + + if (import.meta.env.DEV) + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/rules-of-hooks + useEffect( + () => + EventBus.subscribe("I18N_UPDATE", (lang) => { + nextFrame(() => { + update( + i18next.cloneInstance({ + lng: lang, + }), + ) + }) + }), + [update], + ) + + const callOnce = useRef(false) + + useLayoutEffect(() => { + const i18next = currentI18NInstance + + i18next.on("languageChanged", langChangedHandler) + + return () => { + i18next.off("languageChanged") + } + }, [currentI18NInstance]) + + useLayoutEffect(() => { + if (callOnce.current) return + const { language } = getGeneralSettings() + + i18next.changeLanguage(language) + callOnce.current = true + }, []) + return {children} } diff --git a/tsconfig.web.json b/tsconfig.web.json index 975c19a20..b7b3175fc 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -26,7 +26,8 @@ "@main/*": ["src/main/*"], "@shared/*": ["src/shared/src/*"], "@pkg": ["./package.json"], - "@env": ["./src/env.ts"] + "@env": ["./src/env.ts"], + "@locales/*": ["./locales/*"] } } }