Skip to content

Commit bbe4ea6

Browse files
committed
fix: wait language load then switch to target language
Signed-off-by: Innei <i@innei.in>
1 parent b9c6a8b commit bbe4ea6

File tree

4 files changed

+96
-87
lines changed

4 files changed

+96
-87
lines changed

locales/settings/zh-CN.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"feeds.tableHeaders.entryCount": "条目数",
7979
"feeds.tableHeaders.name": "名称",
8080
"feeds.tableHeaders.subscriptionCount": "订阅数",
81-
"feeds.tableHeaders.tipAmount": "收到的打赏" ,
81+
"feeds.tableHeaders.tipAmount": "收到的打赏",
8282
"general.app": "应用程序",
8383
"general.data_persist.description": "在本地保留数据以启用离线访问和本地搜索",
8484
"general.data_persist.label": "保留数据以供离线使用",
@@ -159,7 +159,7 @@
159159
"titles.about": "关于",
160160
"titles.actions": "自动化",
161161
"titles.appearance": "外观",
162-
"titles.general": "常规",
162+
"titles.general": "通用",
163163
"titles.integration": "集成",
164164
"titles.invitations": "邀请",
165165
"titles.power": "Power",

src/renderer/src/lib/load-language.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { currentSupportedLanguages, dayjsLocaleImportMap } from "@renderer/@types/constants"
2+
import { defaultResources } from "@renderer/@types/default-resource"
3+
import { fallbackLanguage, i18nAtom, LocaleCache } from "@renderer/i18n"
4+
import { jotaiStore } from "@renderer/lib/jotai"
5+
import { isEmptyObject } from "@renderer/lib/utils"
6+
import dayjs from "dayjs"
7+
import i18next from "i18next"
8+
import { toast } from "sonner"
9+
10+
const loadingLangLock = new Set<string>()
11+
const loadedLangs = new Set<string>([fallbackLanguage])
12+
13+
export const loadLanguageAndApply = async (lang: string) => {
14+
const dayjsImport = dayjsLocaleImportMap[lang]
15+
16+
if (dayjsImport) {
17+
const [locale, loader] = dayjsImport
18+
loader().then(() => {
19+
dayjs.locale(locale)
20+
})
21+
}
22+
23+
const { t } = jotaiStore.get(i18nAtom)
24+
if (loadingLangLock.has(lang)) return
25+
const isSupport = currentSupportedLanguages.includes(lang)
26+
if (!isSupport) {
27+
return
28+
}
29+
const loaded = loadedLangs.has(lang)
30+
31+
if (loaded) {
32+
return
33+
}
34+
35+
loadingLangLock.add(lang)
36+
37+
if (import.meta.env.DEV) {
38+
const nsGlobbyMap = import.meta.glob("@locales/*/*.json")
39+
40+
const namespaces = Object.keys(defaultResources.en)
41+
42+
const res = await Promise.allSettled(
43+
namespaces.map(async (ns) => {
44+
const loader = nsGlobbyMap[`../../locales/${ns}/${lang}.json`]
45+
46+
if (!loader) return
47+
const nsResources = await loader().then((m: any) => m.default)
48+
49+
i18next.addResourceBundle(lang, ns, nsResources, true, true)
50+
}),
51+
)
52+
53+
for (const r of res) {
54+
if (r.status === "rejected") {
55+
toast.error(`${t("common:tips.load-lng-error")}: ${lang}`)
56+
loadingLangLock.delete(lang)
57+
58+
return
59+
}
60+
}
61+
} else {
62+
const res = await eval(`import('/locales/${lang}.js')`)
63+
.then((res: any) => res?.default || res)
64+
.catch(() => {
65+
toast.error(`${t("common:tips.load-lng-error")}: ${lang}`)
66+
loadingLangLock.delete(lang)
67+
return {}
68+
})
69+
70+
if (isEmptyObject(res)) {
71+
return
72+
}
73+
for (const namespace in res) {
74+
i18next.addResourceBundle(lang, namespace, res[namespace], true, true)
75+
}
76+
}
77+
78+
await i18next.reloadResources()
79+
await i18next.changeLanguage(lang)
80+
LocaleCache.shared.set(lang)
81+
loadedLangs.add(lang)
82+
loadingLangLock.delete(lang)
83+
}

src/renderer/src/modules/settings/tabs/general.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import { IS_MANUAL_CHANGE_LANGUAGE_KEY } from "@renderer/constants"
2323
import { fallbackLanguage } from "@renderer/i18n"
2424
import { initPostHog } from "@renderer/initialize/posthog"
2525
import { tipcClient } from "@renderer/lib/client"
26+
import { loadLanguageAndApply } from "@renderer/lib/load-language"
2627
import { clearLocalPersistStoreData } from "@renderer/store/utils/clear"
2728
import { useQuery } from "@tanstack/react-query"
29+
import i18next from "i18next"
2830
import { useCallback, useEffect } from "react"
2931
import { useTranslation } from "react-i18next"
3032

@@ -200,7 +202,7 @@ export const VoiceSelector = () => {
200202
}
201203

202204
export const LanguageSelector = () => {
203-
const { t, i18n } = useTranslation("settings")
205+
const { t } = useTranslation("settings")
204206
const { t: langT } = useTranslation("lang")
205207
const language = useGeneralSettingSelector((state) => state.language)
206208

@@ -215,8 +217,10 @@ export const LanguageSelector = () => {
215217
value={finalRenderLanguage}
216218
onValueChange={(value) => {
217219
localStorage.setItem(IS_MANUAL_CHANGE_LANGUAGE_KEY, "true")
218-
setGeneralSetting("language", value as string)
219-
i18n.changeLanguage(value as string)
220+
loadLanguageAndApply(value as string).then(() => {
221+
i18next.changeLanguage(value as string)
222+
setGeneralSetting("language", value as string)
223+
})
220224
}}
221225
>
222226
<SelectTrigger size="sm" className="w-48">

src/renderer/src/providers/i18n-provider.tsx

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,16 @@
1-
import { currentSupportedLanguages, dayjsLocaleImportMap } from "@renderer/@types/constants"
2-
import { defaultResources } from "@renderer/@types/default-resource"
1+
import { currentSupportedLanguages } from "@renderer/@types/constants"
32
import { getGeneralSettings, setGeneralSetting } from "@renderer/atoms/settings/general"
43
import { IS_MANUAL_CHANGE_LANGUAGE_KEY } from "@renderer/constants"
5-
import { fallbackLanguage, i18nAtom, LocaleCache } from "@renderer/i18n"
4+
import { i18nAtom } from "@renderer/i18n"
65
import { EventBus } from "@renderer/lib/event-bus"
7-
import { jotaiStore } from "@renderer/lib/jotai"
8-
import { isEmptyObject } from "@renderer/lib/utils"
9-
import dayjs from "dayjs"
6+
import { loadLanguageAndApply } from "@renderer/lib/load-language"
107
import i18next from "i18next"
118
import LanguageDetector from "i18next-browser-languagedetector"
129
import { useAtom } from "jotai"
1310
import type { FC, PropsWithChildren } from "react"
1411
import { useEffect, useLayoutEffect, useRef } from "react"
1512
import { I18nextProvider } from "react-i18next"
16-
import { toast } from "sonner"
1713

18-
const loadingLangLock = new Set<string>()
19-
const loadedLangs = new Set<string>([fallbackLanguage])
20-
21-
const langChangedHandler = async (lang: string) => {
22-
const dayjsImport = dayjsLocaleImportMap[lang]
23-
24-
if (dayjsImport) {
25-
const [locale, loader] = dayjsImport
26-
loader().then(() => {
27-
dayjs.locale(locale)
28-
})
29-
}
30-
31-
const { t } = jotaiStore.get(i18nAtom)
32-
if (loadingLangLock.has(lang)) return
33-
const isSupport = currentSupportedLanguages.includes(lang)
34-
if (!isSupport) {
35-
return
36-
}
37-
const loaded = loadedLangs.has(lang)
38-
39-
if (loaded) {
40-
return
41-
}
42-
43-
loadingLangLock.add(lang)
44-
45-
if (import.meta.env.DEV) {
46-
const nsGlobbyMap = import.meta.glob("@locales/*/*.json")
47-
48-
const namespaces = Object.keys(defaultResources.en)
49-
50-
const res = await Promise.allSettled(
51-
namespaces.map(async (ns) => {
52-
const loader = nsGlobbyMap[`../../locales/${ns}/${lang}.json`]
53-
54-
if (!loader) return
55-
const nsResources = await loader().then((m: any) => m.default)
56-
57-
i18next.addResourceBundle(lang, ns, nsResources, true, true)
58-
}),
59-
)
60-
61-
for (const r of res) {
62-
if (r.status === "rejected") {
63-
toast.error(`${t("common:tips.load-lng-error")}: ${lang}`)
64-
loadingLangLock.delete(lang)
65-
66-
return
67-
}
68-
}
69-
} else {
70-
const res = await eval(`import('/locales/${lang}.js')`)
71-
.then((res: any) => res?.default || res)
72-
.catch(() => {
73-
toast.error(`${t("common:tips.load-lng-error")}: ${lang}`)
74-
loadingLangLock.delete(lang)
75-
return {}
76-
})
77-
78-
if (isEmptyObject(res)) {
79-
return
80-
}
81-
for (const namespace in res) {
82-
i18next.addResourceBundle(lang, namespace, res[namespace], true, true)
83-
}
84-
}
85-
86-
await i18next.reloadResources()
87-
await i18next.changeLanguage(lang)
88-
LocaleCache.shared.set(lang)
89-
loadedLangs.add(lang)
90-
loadingLangLock.delete(lang)
91-
}
9214
export const I18nProvider: FC<PropsWithChildren> = ({ children }) => {
9315
const [currentI18NInstance, update] = useAtom(i18nAtom)
9416

@@ -112,7 +34,7 @@ export const I18nProvider: FC<PropsWithChildren> = ({ children }) => {
11234
useLayoutEffect(() => {
11335
const i18next = currentI18NInstance
11436

115-
i18next.on("languageChanged", langChangedHandler)
37+
i18next.on("languageChanged", loadLanguageAndApply)
11638

11739
return () => {
11840
i18next.off("languageChanged")

0 commit comments

Comments
 (0)