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 }], + }), + ), + ) +}