diff --git a/packages/dashboard/.storybook/preview.js b/packages/dashboard/.storybook/preview.js
index 72f2d9cf8cdb..bcb4c09e8fba 100644
--- a/packages/dashboard/.storybook/preview.js
+++ b/packages/dashboard/.storybook/preview.js
@@ -2,11 +2,11 @@
import React from 'react'
import { ThemeProvider, StyledEngineProvider } from '@mui/material'
import { MaskLightTheme, applyMaskColorVars, CustomSnackbarProvider, MaskDarkTheme } from '@masknet/theme'
-import { addSharedI18N } from '@masknet/shared'
+import { addSharedI18N, I18NextProviderHMR } from '@masknet/shared'
import { fallbackLng } from '../../shared-base/src/i18n/fallbackRule'
// import { withMatrix } from 'storybook-addon-matrix'
-import { addDashboardI18N } from '../src/locales'
-import { I18nextProvider, initReactI18next } from 'react-i18next'
+import { addDashboardI18N } from '../src/locales/languages'
+import { initReactI18next } from 'react-i18next'
import i18n from 'i18next'
i18n.init({
keySeparator: false,
@@ -26,11 +26,11 @@ export const decorators = [
-
+
-
+
diff --git a/packages/dashboard/src/initialization/Dashboard.tsx b/packages/dashboard/src/initialization/Dashboard.tsx
index 6ba3ce39215c..268139c5e82b 100644
--- a/packages/dashboard/src/initialization/Dashboard.tsx
+++ b/packages/dashboard/src/initialization/Dashboard.tsx
@@ -7,7 +7,7 @@ import {
MaskDarkTheme,
useSystemPreferencePalette,
} from '@masknet/theme'
-import { ErrorBoundary } from '@masknet/shared'
+import { ErrorBoundary, I18NextProviderHMR } from '@masknet/shared'
import {
createInjectHooksRenderer,
NetworkPluginID,
@@ -18,7 +18,6 @@ import {
import { Web3Provider } from '@masknet/web3-shared-evm'
import i18n from 'i18next'
-import { I18nextProvider } from 'react-i18next'
import '../utils/kv-storage'
@@ -55,7 +54,7 @@ export default function DashboardRoot() {
return (
-
+
@@ -71,7 +70,7 @@ export default function DashboardRoot() {
-
+
)
diff --git a/packages/dashboard/src/locales/languages.ts b/packages/dashboard/src/locales/languages.ts
index f9b9cec364aa..0b542edc3c58 100644
--- a/packages/dashboard/src/locales/languages.ts
+++ b/packages/dashboard/src/locales/languages.ts
@@ -18,3 +18,16 @@ export const languages = {
}
import { createI18NBundle } from '@masknet/shared-base'
export const addDashboardI18N = createI18NBundle('dashboard', languages)
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: ['dashboard', { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW }],
+ }),
+ ),
+ )
+}
diff --git a/packages/mask/shared-ui/locales/languages.ts b/packages/mask/shared-ui/locales/languages.ts
index d895213dbb30..c3a99b8951be 100644
--- a/packages/mask/shared-ui/locales/languages.ts
+++ b/packages/mask/shared-ui/locales/languages.ts
@@ -18,3 +18,16 @@ export const languages = {
}
import { createI18NBundle } from '@masknet/shared-base'
export const addMaskI18N = createI18NBundle('mask', languages)
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: ['mask', { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW }],
+ }),
+ ),
+ )
+}
diff --git a/packages/mask/src/UIRoot.tsx b/packages/mask/src/UIRoot.tsx
index fde79a24baac..f2fb7cfeeb7d 100644
--- a/packages/mask/src/UIRoot.tsx
+++ b/packages/mask/src/UIRoot.tsx
@@ -1,10 +1,9 @@
import { Suspense } from 'react'
-import { I18nextProvider } from 'react-i18next'
import { CustomSnackbarProvider } from '@masknet/theme'
import { Web3Provider } from '@masknet/web3-shared-evm'
import { CssBaseline, StyledEngineProvider, Theme, ThemeProvider } from '@mui/material'
import { NetworkPluginID, PluginsWeb3ContextProvider, useAllPluginsWeb3State } from '@masknet/plugin-infra'
-import { ErrorBoundary, ErrorBoundaryBuildInfoContext, useValueRef } from '@masknet/shared'
+import { ErrorBoundary, ErrorBoundaryBuildInfoContext, useValueRef, I18NextProviderHMR } from '@masknet/shared'
import i18nNextInstance from '../shared-ui/locales_legacy'
import { Web3Context } from './web3/context'
import { buildInfoMarkdown } from './extension/background-script/Jobs/PrintBuildFlags'
@@ -69,7 +68,7 @@ export function MaskUIRoot({ children, kind, useTheme }: MaskUIRootProps) {
),
- (jsx) => ,
+ (jsx) => ,
kind === 'page' ? (jsx) => : identity,
(jsx) => (
diff --git a/packages/mask/src/plugin-infra/I18NFieldRender.tsx b/packages/mask/src/plugin-infra/I18NFieldRender.tsx
index a2c69e0c3813..33996c7cab31 100644
--- a/packages/mask/src/plugin-infra/I18NFieldRender.tsx
+++ b/packages/mask/src/plugin-infra/I18NFieldRender.tsx
@@ -7,7 +7,6 @@ export interface PluginI18NFieldRenderProps {
}
export function PluginI18NFieldRender({ pluginID, field }: PluginI18NFieldRenderProps) {
const [t] = useTranslation()
- console.log(pluginID, field)
if (!field) return null
if (typeof field === 'object' && 'fallback' in field) {
if (field.i18nKey) {
diff --git a/packages/mask/src/social-network-adaptor/twitter.com/locales/languages.ts b/packages/mask/src/social-network-adaptor/twitter.com/locales/languages.ts
index a6ae8c95f092..a618820679a8 100644
--- a/packages/mask/src/social-network-adaptor/twitter.com/locales/languages.ts
+++ b/packages/mask/src/social-network-adaptor/twitter.com/locales/languages.ts
@@ -18,3 +18,16 @@ export const languages = {
}
import { createI18NBundle } from '@masknet/shared-base'
export const addDO_NOT_USEI18N = createI18NBundle('DO_NOT_USE', languages)
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: ['DO_NOT_USE', { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW }],
+ }),
+ ),
+ )
+}
diff --git a/packages/mask/src/utils/i18n-next-ui.ts b/packages/mask/src/utils/i18n-next-ui.ts
index 38eb1db25350..871a4c11460f 100644
--- a/packages/mask/src/utils/i18n-next-ui.ts
+++ b/packages/mask/src/utils/i18n-next-ui.ts
@@ -1,8 +1,6 @@
import type { i18n } from 'i18next'
import { initReactI18next, useTranslation as useTranslation_, UseTranslationOptions } from 'react-i18next'
import type { TOptions } from 'i18next'
-import { useEffect } from 'react'
-import { useUpdate } from 'react-use'
import type en from '../../shared-ui/locales/en-US.json'
import i18nNextInstance from '../../shared-ui/locales_legacy'
import { languageSettings } from '../settings/settings'
@@ -26,15 +24,6 @@ export function useI18N(opt?: UseTranslationOptions): {
i18n: i18n
ready: boolean
} {
- if (process.env.NODE_ENV === 'development') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const update = useUpdate()
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useEffect(() => {
- document.addEventListener('i18n-hmr', update)
- return () => document.removeEventListener('i18n-hmr', update)
- }, [])
- }
return useTranslation_('mask', opt)
}
diff --git a/packages/plugins/FileService/src/locales/languages.ts b/packages/plugins/FileService/src/locales/languages.ts
index 89089d0520eb..2f5bc7979f47 100644
--- a/packages/plugins/FileService/src/locales/languages.ts
+++ b/packages/plugins/FileService/src/locales/languages.ts
@@ -14,3 +14,19 @@ export const languages = {
'zh-CN': zh_CN,
zh: zh_TW,
}
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: [
+ 'com.maskbook.fileservice',
+ { en: en_US, ja: ja_JP, ko: ko_KR, 'zh-CN': zh_CN, zh: zh_TW },
+ ],
+ }),
+ ),
+ )
+}
diff --git a/packages/plugins/Flow/src/locales/languages.ts b/packages/plugins/Flow/src/locales/languages.ts
index 80eb482a049f..b8820e61f18a 100644
--- a/packages/plugins/Flow/src/locales/languages.ts
+++ b/packages/plugins/Flow/src/locales/languages.ts
@@ -16,3 +16,19 @@ export const languages = {
'zh-CN': zh_CN,
zh: zh_TW,
}
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: [
+ 'com.mask.flow',
+ { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW },
+ ],
+ }),
+ ),
+ )
+}
diff --git a/packages/plugins/Wallet/src/locales/languages.ts b/packages/plugins/Wallet/src/locales/languages.ts
index 80eb482a049f..fe23f69e81df 100644
--- a/packages/plugins/Wallet/src/locales/languages.ts
+++ b/packages/plugins/Wallet/src/locales/languages.ts
@@ -16,3 +16,19 @@ export const languages = {
'zh-CN': zh_CN,
zh: zh_TW,
}
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: [
+ 'com.maskbook.wallet',
+ { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW },
+ ],
+ }),
+ ),
+ )
+}
diff --git a/packages/plugins/example/src/locales/languages.ts b/packages/plugins/example/src/locales/languages.ts
index 9a2d33d1d641..cdea8afca882 100644
--- a/packages/plugins/example/src/locales/languages.ts
+++ b/packages/plugins/example/src/locales/languages.ts
@@ -8,3 +8,14 @@ export const languages = {
en: en_US,
zh: zh_TW,
}
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(['./en-US.json', './zh-TW.json'], () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: ['io.mask.example', { en: en_US, zh: zh_TW }],
+ }),
+ ),
+ )
+}
diff --git a/packages/scripts/src/locale-kit-next/index.ts b/packages/scripts/src/locale-kit-next/index.ts
index f0bb47340c0e..1f5bc7da92e0 100644
--- a/packages/scripts/src/locale-kit-next/index.ts
+++ b/packages/scripts/src/locale-kit-next/index.ts
@@ -43,6 +43,26 @@ export async function syncLanguages() {
code += `import { createI18NBundle } from '${target}'\n`
code += `export const add${upperFirst(namespace)}I18N = createI18NBundle('${namespace}', languages)\n`
}
+
+ {
+ const allImportPath: string[] = []
+ const binding: string[] = []
+ for (const [language, familyName] of languages) {
+ allImportPath.push(`./${language}.json`)
+ binding.push(`'${familyName}': ${language.replace('-', '_')}`)
+ }
+ code += `// @ts-ignore
+ if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ${JSON.stringify(allImportPath)},
+ () => globalThis.dispatchEvent?.(new CustomEvent('MASK_I18N_HMR', {
+ detail: ['${namespace}', { ${binding.join(', ')} }]
+ }))
+ )
+ }
+ `
+ }
code = await prettier(code)
await writeFile(join(inputDir, 'languages.ts'), code, { encoding: 'utf8' })
}
diff --git a/packages/shared-base/src/i18n/register-ns.ts b/packages/shared-base/src/i18n/register-ns.ts
index 379a837893b2..481e59576ef8 100644
--- a/packages/shared-base/src/i18n/register-ns.ts
+++ b/packages/shared-base/src/i18n/register-ns.ts
@@ -2,6 +2,19 @@ import type { i18n } from 'i18next'
export function addI18NBundle(instance: i18n, namespace: string, langs: Record) {
if (!instance.addResourceBundle) throw new TypeError('Please call instance.init() first')
+
+ try {
+ if (process.env.NODE_ENV === 'development') {
+ globalThis.addEventListener('MASK_I18N_HMR', (e) => {
+ const [ns, langs] = (e as CustomEvent).detail
+ if (namespace !== ns) return
+
+ for (const lang in langs) {
+ instance.addResourceBundle(lang, namespace, removeEmptyString(langs[lang]))
+ }
+ })
+ }
+ } catch {}
for (const lang in langs) {
instance.addResourceBundle(lang, namespace, removeEmptyString(langs[lang]))
}
diff --git a/packages/shared/src/UI/components/I18NextProviderHMR.tsx b/packages/shared/src/UI/components/I18NextProviderHMR.tsx
new file mode 100644
index 000000000000..3539057ad94f
--- /dev/null
+++ b/packages/shared/src/UI/components/I18NextProviderHMR.tsx
@@ -0,0 +1,27 @@
+import React, { useEffect, useState } from 'react'
+import { I18nextProvider, type I18nextProviderProps } from 'react-i18next'
+
+export const I18NextProviderHMR =
+ process.env.NODE_ENV === 'development'
+ ? function I18NextProviderHMR({ i18n, defaultNS, children }: React.PropsWithChildren) {
+ const [ns, setNS] = useState(defaultNS)
+
+ useEffect(() => {
+ const f = () => setNS('HMR')
+ globalThis.addEventListener('MASK_I18N_HMR', f)
+ return () => globalThis.removeEventListener('MASK_I18N_HMR', f)
+ }, [])
+ // Force trigger a re-render to apply HMR
+ if (ns === 'HMR') defaultNS = Math.random() + ''
+
+ useEffect(() => {
+ if (ns === 'HMR') setNS('')
+ }, [ns])
+ // deliberately call it as a function in order to skip a React component nesting level.
+ return I18nextProvider({
+ i18n,
+ defaultNS,
+ children,
+ })
+ }
+ : I18nextProvider
diff --git a/packages/shared/src/UI/components/index.ts b/packages/shared/src/UI/components/index.ts
index e4dd3323685d..e23325adbf74 100644
--- a/packages/shared/src/UI/components/index.ts
+++ b/packages/shared/src/UI/components/index.ts
@@ -9,3 +9,4 @@ export * from './SelectTokenChip'
export * from './TokenAmountPanel'
export * from './LoadingAnimation'
export * from './MiniNetworkSelector'
+export * from './I18NextProviderHMR'
diff --git a/packages/shared/src/locales/languages.ts b/packages/shared/src/locales/languages.ts
index 6e4875e1c133..e7c75f0ffd17 100644
--- a/packages/shared/src/locales/languages.ts
+++ b/packages/shared/src/locales/languages.ts
@@ -18,3 +18,16 @@ export const languages = {
}
import { createI18NBundle } from '@masknet/shared-base'
export const addSharedI18N = createI18NBundle('shared', languages)
+// @ts-ignore
+if (import.meta.webpack) {
+ // @ts-ignore
+ import.meta.webpackHot.accept(
+ ['./en-US.json', './ja-JP.json', './ko-KR.json', './qya-AA.json', './zh-CN.json', './zh-TW.json'],
+ () =>
+ globalThis.dispatchEvent?.(
+ new CustomEvent('MASK_I18N_HMR', {
+ detail: ['shared', { en: en_US, ja: ja_JP, ko: ko_KR, qy: qya_AA, 'zh-CN': zh_CN, zh: zh_TW }],
+ }),
+ ),
+ )
+}