fix: set language when init

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-09-12 12:53:04 +08:00
parent 3d39308696
commit fb3b59210e
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 134 additions and 62 deletions

View File

@ -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: "/",

View File

@ -1,5 +1,8 @@
{
"confirm": "Confirm",
"ok": "OK",
"cancel": "Cancel"
"cancel": "Cancel",
"tips": {
"load-lng-error": "Failed to load language pack"
}
}

View File

@ -1,5 +1,8 @@
{
"confirm": "确认",
"ok": "好",
"cancel": "取消"
"cancel": "取消",
"tips": {
"load-lng-error": "加载语言包失败"
}
}

View File

@ -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,
},
}

View File

@ -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<string>()
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/<module-name>/en.json

View File

@ -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<PropsWithChildren> = ({ children }) => {
const [currentI18NInstance, update] = useState(i18next)
const loadingLangLock = new Set<string>()
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<PropsWithChildren> = ({ 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 <I18nextProvider i18n={currentI18NInstance}>{children}</I18nextProvider>
}

View File

@ -26,7 +26,8 @@
"@main/*": ["src/main/*"],
"@shared/*": ["src/shared/src/*"],
"@pkg": ["./package.json"],
"@env": ["./src/env.ts"]
"@env": ["./src/env.ts"],
"@locales/*": ["./locales/*"]
}
}
}