返回 AiToEarn
client.ts
根目录 / project / aitoearn-web / src / app / i18n / client.ts
1 'use client'
2
3 import type { FlatNamespace } from 'i18next'
4 import type { UseTranslationOptions, UseTranslationResponse } from 'react-i18next'
5 import { getCookie, setCookie } from 'cookies-next'
6 import i18next from 'i18next'
7 import LanguageDetector from 'i18next-browser-languagedetector'
8 import resourcesToBackend from 'i18next-resources-to-backend'
9 import { useEffect, useMemo, useState } from 'react'
10 import { initReactI18next, useTranslation } from 'react-i18next'
11
12 import { useGetClientLng } from '@/hooks/useSystem'
13 import { cookieName, getOptions, languages } from './settings'
14
15 const runsOnServerSide = typeof window === 'undefined'
16
17 function getClientPathLanguage() {
18 if (runsOnServerSide)
19 return undefined
20
21 const pathLng = window.location.pathname.split('/')[1]
22 return pathLng && languages.includes(pathLng) ? pathLng : undefined
23 }
24
25 const initialClientLng = getClientPathLanguage()
26
27 // 全局标志:是否禁用语言自动切换(用于图片生成等场景)
28 let disableLanguageSwitch = false
29
30 /**
31 * 临时禁用语言自动切换
32 * 用于在离屏渲染 React 组件时防止语言被意外切换
33 */
34 export function setDisableLanguageSwitch(disabled: boolean): void {
35 disableLanguageSwitch = disabled
36 }
37
38 // on client side the normal singleton is ok
39 i18next
40 .use(initReactI18next)
41 .use(LanguageDetector)
42 .use(
43 resourcesToBackend(
44 (language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`),
45 ),
46 )
47 // .use(LocizeBackend) // locize backend could be used on client side, but prefer to keep it in sync with server side
48 .init({
49 ...getOptions(initialClientLng),
50 lng: initialClientLng,
51 detection: {
52 order: ['path', 'htmlTag', 'cookie', 'navigator'],
53 },
54 preload: runsOnServerSide ? languages : [],
55 })
56
57 export function useTransClient<Ns extends string | undefined = undefined>(
58 ns?: Ns | Ns[],
59 options?: UseTranslationOptions<FlatNamespace>,
60 ): UseTranslationResponse<string, FlatNamespace> {
61 const i18nextCookie = getCookie(cookieName)
62 const lng = useGetClientLng()
63 if (typeof lng !== 'string')
64 throw new Error('useT is only available inside /app/[lng]')
65
66 const nsKey = Array.isArray(ns) ? ns.join('|') : ns ?? ''
67 const stableNs = useMemo(() => ns, [nsKey])
68 const translationOptions: UseTranslationOptions<FlatNamespace> = useMemo(() => ({
69 ...options,
70 lng,
71 }), [lng, options])
72
73 if (runsOnServerSide && i18next.resolvedLanguage !== lng) {
74 i18next.changeLanguage(lng)
75 }
76 else {
77 const [activeLng, setActiveLng] = useState(i18next.resolvedLanguage)
78
79 // 监听 i18next 语言变化
80 useEffect(() => {
81 if (activeLng === i18next.resolvedLanguage)
82 return
83 setActiveLng(i18next.resolvedLanguage)
84 }, [activeLng, i18next.resolvedLanguage])
85
86 // 强制同步 URL 参数中的语言到 i18next
87 useEffect(() => {
88 // 如果禁用了语言切换(如图片生成场景),跳过
89 if (disableLanguageSwitch)
90 return
91
92 if (!lng)
93 return
94
95 // 始终使用 URL 参数中的语言,忽略 i18next 的自动检测
96 if (i18next.resolvedLanguage !== lng) {
97 i18next.changeLanguage(lng).then(() => {
98 // 语言切换完成后,强制重新渲染
99 setActiveLng(currentLng => currentLng === lng ? currentLng : lng)
100 })
101 }
102 else {
103 // 即使语言相同,也确保状态同步
104 setActiveLng(currentLng => currentLng === lng ? currentLng : lng)
105 }
106 }, [lng])
107
108 // 同步 cookie
109 useEffect(() => {
110 if (i18nextCookie === lng)
111 return
112 setCookie(cookieName, lng, { path: '/' })
113 }, [lng, i18nextCookie])
114 }
115 // 强制类型断言以匹配 react-i18next 的签名
116 return useTranslation(stableNs as Ns | Ns[] | undefined, translationOptions) as UseTranslationResponse<
117 string,
118 FlatNamespace
119 >
120 }
121
122 export default i18next
123
124 // 静态方法,注意这个方法的国际化不会自动更新
125 export function directTrans<Ns extends FlatNamespace>(ns: Ns, key: string): string {
126 // @ts-ignore
127 const t = (key: string) => i18next.t(key, { ns })
128 // @ts-ignore
129 return t(key)
130 }
131
131 lines TYPESCRIPT