From 145a5ce3a4182aa17b90da8b17aaf52b0e68ae8e Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Thu, 20 Feb 2025 14:37:37 +0100 Subject: [PATCH 1/7] feat: virtual messages hmr --- packages/unplugin-vue-i18n/src/core/resource.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/unplugin-vue-i18n/src/core/resource.ts b/packages/unplugin-vue-i18n/src/core/resource.ts index 94426d62..5b76bfa0 100644 --- a/packages/unplugin-vue-i18n/src/core/resource.ts +++ b/packages/unplugin-vue-i18n/src/core/resource.ts @@ -584,7 +584,19 @@ const mergeDeep = (target, ...sources) => { export default mergeDeep({}, ${codes.map(code => `{${code}}`).join(',\n')} -);` +); + +if(import.meta.hot) { + import.meta.hot.accept(mod => { + // retrieve global i18n instance + const i18n = document.querySelector('#app').__vue_app__.__VUE_I18N__.global + + // set locale messages per locale + for(const locale in mod.default){ + i18n.setLocaleMessage(locale, mod.default[locale]) + } + }) +}` } async function getCode( From bc5217818546a624ceb7ff96ecff05c975c87a27 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 21 Feb 2025 12:39:17 +0100 Subject: [PATCH 2/7] fix: conditionally add HMR code --- .../unplugin-vue-i18n/src/core/resource.ts | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/unplugin-vue-i18n/src/core/resource.ts b/packages/unplugin-vue-i18n/src/core/resource.ts index 5b76bfa0..8722cc03 100644 --- a/packages/unplugin-vue-i18n/src/core/resource.ts +++ b/packages/unplugin-vue-i18n/src/core/resource.ts @@ -562,6 +562,34 @@ async function generateBundleResources( } } + const hmrCode = ` +if(import.meta.hot) { + function uniqueKeys(...objects) { + const keySet = new Set() + + for (const obj of objects) { + for (const key of Object.keys(obj)) { + keySet.add(key) + } + } + + return Array.from(keySet) + } + + import.meta.hot.accept(mod => { + // retrieve global i18n instance + const i18n = document.querySelector('#app').__vue_app__.__VUE_I18N__.global + + // locale keys of both original and updated merged messages + const localeKeys = uniqueKeys(merged, mod.default) + + // set locale messages for each locale key or overwrite with empty object if deleted + for(const locale of localeKeys){ + i18n.setLocaleMessage(locale, mod.default[locale] || {}) + } + }) +}` + return `const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item); const mergeDeep = (target, ...sources) => { @@ -582,21 +610,13 @@ const mergeDeep = (target, ...sources) => { return mergeDeep(target, ...sources); } -export default mergeDeep({}, +const merged = mergeDeep({}, ${codes.map(code => `{${code}}`).join(',\n')} ); +export default merged -if(import.meta.hot) { - import.meta.hot.accept(mod => { - // retrieve global i18n instance - const i18n = document.querySelector('#app').__vue_app__.__VUE_I18N__.global - - // set locale messages per locale - for(const locale in mod.default){ - i18n.setLocaleMessage(locale, mod.default[locale]) - } - }) -}` +${isProduction ? '' : hmrCode} +` } async function getCode( From 201110c3324f6b219cabbe8248a019954a011f14 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 21 Feb 2025 13:00:53 +0100 Subject: [PATCH 3/7] feat: configurable HMR with warning --- packages/unplugin-vue-i18n/README.md | 9 +++++++++ packages/unplugin-vue-i18n/src/core/options.ts | 2 ++ packages/unplugin-vue-i18n/src/core/resource.ts | 8 ++++++-- packages/unplugin-vue-i18n/src/index.ts | 16 +++++++++++----- packages/unplugin-vue-i18n/src/types.ts | 1 + packages/unplugin-vue-i18n/src/utils/log.ts | 8 ++++++++ 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/unplugin-vue-i18n/README.md b/packages/unplugin-vue-i18n/README.md index 77aa44e7..fad44957 100644 --- a/packages/unplugin-vue-i18n/README.md +++ b/packages/unplugin-vue-i18n/README.md @@ -607,6 +607,15 @@ If do you will use this option, you need to enable `jitCompilation` option. By using it you can exclude from the bundle those localizations that are not specified in this option. +### `hot` + +- **Type:** `boolean` +- **Default:** `false` + + Enables HMR without page reload for bundled virtual messages `@intlify/unplugin-vue-i18n/messages`. + + > [!IMPORTANT] > Vite support only + ### `useVueI18nImportName` (Experimental) - **Type:** `boolean` diff --git a/packages/unplugin-vue-i18n/src/core/options.ts b/packages/unplugin-vue-i18n/src/core/options.ts index 1a76615a..e5439232 100644 --- a/packages/unplugin-vue-i18n/src/core/options.ts +++ b/packages/unplugin-vue-i18n/src/core/options.ts @@ -57,6 +57,7 @@ export function resolveOptions(options: PluginOptions) { const ssrBuild = !!options.ssr const allowDynamic = !!options.allowDynamic + const hot = !!options.hot const strictMessage = isBoolean(options.strictMessage) ? options.strictMessage @@ -83,6 +84,7 @@ export function resolveOptions(options: PluginOptions) { return { include, exclude, + hot, module: moduleType, onlyLocales, forceStringify, diff --git a/packages/unplugin-vue-i18n/src/core/resource.ts b/packages/unplugin-vue-i18n/src/core/resource.ts index 8722cc03..c5d153bf 100644 --- a/packages/unplugin-vue-i18n/src/core/resource.ts +++ b/packages/unplugin-vue-i18n/src/core/resource.ts @@ -61,6 +61,7 @@ export function resourcePlugin( strictMessage, allowDynamic, escapeHtml, + hot, transformI18nBlock }: ResolvedOptions, meta: UnpluginContextMeta @@ -326,7 +327,8 @@ export function resourcePlugin( { forceStringify, strictMessage, - escapeHtml + escapeHtml, + hot } ) // TODO: support virtual import identifier @@ -526,6 +528,7 @@ async function generateBundleResources( strictMessage = true, escapeHtml = false, jit = true, + hot = false, transformI18nBlock = undefined }: { forceStringify?: boolean @@ -534,6 +537,7 @@ async function generateBundleResources( strictMessage?: boolean escapeHtml?: boolean jit?: boolean + hot?: boolean transformI18nBlock?: PluginOptions['transformI18nBlock'] } ) { @@ -615,7 +619,7 @@ const merged = mergeDeep({}, ); export default merged -${isProduction ? '' : hmrCode} +${isProduction || !hot ? '' : hmrCode} ` } diff --git a/packages/unplugin-vue-i18n/src/index.ts b/packages/unplugin-vue-i18n/src/index.ts index 6c8d8ed4..51f59e9f 100644 --- a/packages/unplugin-vue-i18n/src/index.ts +++ b/packages/unplugin-vue-i18n/src/index.ts @@ -1,6 +1,11 @@ import { createUnplugin } from 'unplugin' import createDebug from 'debug' -import { raiseError, resolveNamespace } from './utils' +import { + getWebpackNotSupportedMessage, + raiseError, + resolveNamespace, + warn +} from './utils' import { resolveOptions, resourcePlugin, directivePlugin } from './core' import type { UnpluginFactory, UnpluginInstance } from 'unplugin' @@ -24,13 +29,14 @@ export const unpluginFactory: UnpluginFactory = ( const resolvedOptions = resolveOptions(options) debug('plugin options (resolved):', resolvedOptions) + if (resolvedOptions.hot && meta.framework === 'webpack') { + warn(getWebpackNotSupportedMessage('hot')) + } + const plugins = [resourcePlugin(resolvedOptions, meta)] if (resolvedOptions.optimizeTranslationDirective) { if (meta.framework === 'webpack') { - raiseError( - `The 'optimizeTranslationDirective' option still is not supported for webpack.\n` + - `We are waiting for your Pull Request 🙂.` - ) + raiseError(getWebpackNotSupportedMessage('optimizeTranslationDirective')) } plugins.push(directivePlugin(resolvedOptions)) } diff --git a/packages/unplugin-vue-i18n/src/types.ts b/packages/unplugin-vue-i18n/src/types.ts index 5b8276d5..262a49de 100644 --- a/packages/unplugin-vue-i18n/src/types.ts +++ b/packages/unplugin-vue-i18n/src/types.ts @@ -10,6 +10,7 @@ export interface PluginOptions { runtimeOnly?: boolean compositionOnly?: boolean ssr?: boolean + hot?: boolean fullInstall?: boolean forceStringify?: boolean defaultSFCLang?: SFCLangFormat diff --git a/packages/unplugin-vue-i18n/src/utils/log.ts b/packages/unplugin-vue-i18n/src/utils/log.ts index e079e247..4f5b54ff 100644 --- a/packages/unplugin-vue-i18n/src/utils/log.ts +++ b/packages/unplugin-vue-i18n/src/utils/log.ts @@ -12,3 +12,11 @@ export function error(...args: unknown[]) { export function raiseError(message: string) { throw new Error(`[${PKG_NAME}] ${message}`) } + +// TODO: extract warn/error messages similar to vue-i18n structure +export function getWebpackNotSupportedMessage(optionName: string) { + return ( + `The '${optionName}' option still is not supported for webpack.\n` + + `We are waiting for your Pull Request 🙂.` + ) +} From b20e49993cb2f0e0dac58e40b26e3558afa245b9 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 21 Feb 2025 13:18:37 +0100 Subject: [PATCH 4/7] chore: rename option to `hmr` --- packages/unplugin-vue-i18n/src/core/options.ts | 4 ++-- packages/unplugin-vue-i18n/src/core/resource.ts | 10 +++++----- packages/unplugin-vue-i18n/src/index.ts | 4 ++-- packages/unplugin-vue-i18n/src/types.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/unplugin-vue-i18n/src/core/options.ts b/packages/unplugin-vue-i18n/src/core/options.ts index e5439232..1186c7d3 100644 --- a/packages/unplugin-vue-i18n/src/core/options.ts +++ b/packages/unplugin-vue-i18n/src/core/options.ts @@ -57,7 +57,7 @@ export function resolveOptions(options: PluginOptions) { const ssrBuild = !!options.ssr const allowDynamic = !!options.allowDynamic - const hot = !!options.hot + const hmr = !!options.hmr const strictMessage = isBoolean(options.strictMessage) ? options.strictMessage @@ -84,7 +84,7 @@ export function resolveOptions(options: PluginOptions) { return { include, exclude, - hot, + hmr, module: moduleType, onlyLocales, forceStringify, diff --git a/packages/unplugin-vue-i18n/src/core/resource.ts b/packages/unplugin-vue-i18n/src/core/resource.ts index c5d153bf..03ca1361 100644 --- a/packages/unplugin-vue-i18n/src/core/resource.ts +++ b/packages/unplugin-vue-i18n/src/core/resource.ts @@ -61,7 +61,7 @@ export function resourcePlugin( strictMessage, allowDynamic, escapeHtml, - hot, + hmr, transformI18nBlock }: ResolvedOptions, meta: UnpluginContextMeta @@ -328,7 +328,7 @@ export function resourcePlugin( forceStringify, strictMessage, escapeHtml, - hot + hmr } ) // TODO: support virtual import identifier @@ -528,7 +528,7 @@ async function generateBundleResources( strictMessage = true, escapeHtml = false, jit = true, - hot = false, + hmr = false, transformI18nBlock = undefined }: { forceStringify?: boolean @@ -537,7 +537,7 @@ async function generateBundleResources( strictMessage?: boolean escapeHtml?: boolean jit?: boolean - hot?: boolean + hmr?: boolean transformI18nBlock?: PluginOptions['transformI18nBlock'] } ) { @@ -619,7 +619,7 @@ const merged = mergeDeep({}, ); export default merged -${isProduction || !hot ? '' : hmrCode} +${isProduction || !hmr ? '' : hmrCode} ` } diff --git a/packages/unplugin-vue-i18n/src/index.ts b/packages/unplugin-vue-i18n/src/index.ts index 51f59e9f..8b5d0bb4 100644 --- a/packages/unplugin-vue-i18n/src/index.ts +++ b/packages/unplugin-vue-i18n/src/index.ts @@ -29,8 +29,8 @@ export const unpluginFactory: UnpluginFactory = ( const resolvedOptions = resolveOptions(options) debug('plugin options (resolved):', resolvedOptions) - if (resolvedOptions.hot && meta.framework === 'webpack') { - warn(getWebpackNotSupportedMessage('hot')) + if (resolvedOptions.hmr && meta.framework === 'webpack') { + warn(getWebpackNotSupportedMessage('hmr')) } const plugins = [resourcePlugin(resolvedOptions, meta)] diff --git a/packages/unplugin-vue-i18n/src/types.ts b/packages/unplugin-vue-i18n/src/types.ts index 262a49de..a04a76b0 100644 --- a/packages/unplugin-vue-i18n/src/types.ts +++ b/packages/unplugin-vue-i18n/src/types.ts @@ -10,7 +10,7 @@ export interface PluginOptions { runtimeOnly?: boolean compositionOnly?: boolean ssr?: boolean - hot?: boolean + hmr?: boolean fullInstall?: boolean forceStringify?: boolean defaultSFCLang?: SFCLangFormat From e7fd84a9f32f900c5d599f7c038c2515505013fc Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 21 Feb 2025 13:18:55 +0100 Subject: [PATCH 5/7] chore: rename option to `hmr` --- packages/unplugin-vue-i18n/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/unplugin-vue-i18n/README.md b/packages/unplugin-vue-i18n/README.md index fad44957..4bbc7365 100644 --- a/packages/unplugin-vue-i18n/README.md +++ b/packages/unplugin-vue-i18n/README.md @@ -607,7 +607,7 @@ If do you will use this option, you need to enable `jitCompilation` option. By using it you can exclude from the bundle those localizations that are not specified in this option. -### `hot` +### `hmr` - **Type:** `boolean` - **Default:** `false` From 2f45ae7bed8695f0afa2a8cd2cbd2c2e926e3d23 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 21 Feb 2025 17:27:46 +0100 Subject: [PATCH 6/7] feat: configurable vue app root container selector --- packages/unplugin-vue-i18n/README.md | 7 ++++++ .../unplugin-vue-i18n/src/core/options.ts | 2 ++ .../unplugin-vue-i18n/src/core/resource.ts | 22 +++++++++++++++++-- packages/unplugin-vue-i18n/src/types.ts | 1 + 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/unplugin-vue-i18n/README.md b/packages/unplugin-vue-i18n/README.md index 4bbc7365..7aedeb9b 100644 --- a/packages/unplugin-vue-i18n/README.md +++ b/packages/unplugin-vue-i18n/README.md @@ -616,6 +616,13 @@ If do you will use this option, you need to enable `jitCompilation` option. > [!IMPORTANT] > Vite support only +### `appRootContainer` + +- **Type:** `string` +- **Default:** `'#app'` + + The selector used find the Vue app root container during HMR. + ### `useVueI18nImportName` (Experimental) - **Type:** `boolean` diff --git a/packages/unplugin-vue-i18n/src/core/options.ts b/packages/unplugin-vue-i18n/src/core/options.ts index 1186c7d3..dd6becff 100644 --- a/packages/unplugin-vue-i18n/src/core/options.ts +++ b/packages/unplugin-vue-i18n/src/core/options.ts @@ -58,6 +58,7 @@ export function resolveOptions(options: PluginOptions) { const allowDynamic = !!options.allowDynamic const hmr = !!options.hmr + const appRootContainer = options.appRootContainer ?? '#app' const strictMessage = isBoolean(options.strictMessage) ? options.strictMessage @@ -85,6 +86,7 @@ export function resolveOptions(options: PluginOptions) { include, exclude, hmr, + appRootContainer, module: moduleType, onlyLocales, forceStringify, diff --git a/packages/unplugin-vue-i18n/src/core/resource.ts b/packages/unplugin-vue-i18n/src/core/resource.ts index 03ca1361..36c8aeec 100644 --- a/packages/unplugin-vue-i18n/src/core/resource.ts +++ b/packages/unplugin-vue-i18n/src/core/resource.ts @@ -51,6 +51,7 @@ export function resourcePlugin( exclude, module, forceStringify, + appRootContainer, defaultSFCLang, globalSFCScope, runtimeOnly, @@ -231,6 +232,16 @@ export function resourcePlugin( } } }, + configureServer(server) { + // client could not find vue app root during HMR + server.ws.on('unplugin-vue-i18n:app-root-missing', _ => { + console.error( + `[unplugin-vue-i18n] Unable to find vue-i18n instance on vue app root container with selector "${appRootContainer}" - skipping HMR and reloading page.\n` + + `The selector used can be configured using the \`appRootcontainer\` option.` + ) + server.ws.send({ type: 'full-reload' }) + }) + }, async handleHotUpdate({ file, server }) { if (/\.(json5?|ya?ml)$/.test(file)) { @@ -328,7 +339,8 @@ export function resourcePlugin( forceStringify, strictMessage, escapeHtml, - hmr + hmr, + appRootContainer } ) // TODO: support virtual import identifier @@ -529,6 +541,7 @@ async function generateBundleResources( escapeHtml = false, jit = true, hmr = false, + appRootContainer, transformI18nBlock = undefined }: { forceStringify?: boolean @@ -538,6 +551,7 @@ async function generateBundleResources( escapeHtml?: boolean jit?: boolean hmr?: boolean + appRootContainer: string transformI18nBlock?: PluginOptions['transformI18nBlock'] } ) { @@ -582,7 +596,11 @@ if(import.meta.hot) { import.meta.hot.accept(mod => { // retrieve global i18n instance - const i18n = document.querySelector('#app').__vue_app__.__VUE_I18N__.global + const i18n = document.querySelector('${appRootContainer}')?.__vue_app__?.__VUE_I18N__?.global + if(i18n == null) { + import.meta.hot.send('unplugin-vue-i18n:app-root-missing', {}) + return + } // locale keys of both original and updated merged messages const localeKeys = uniqueKeys(merged, mod.default) diff --git a/packages/unplugin-vue-i18n/src/types.ts b/packages/unplugin-vue-i18n/src/types.ts index a04a76b0..eb8cba60 100644 --- a/packages/unplugin-vue-i18n/src/types.ts +++ b/packages/unplugin-vue-i18n/src/types.ts @@ -11,6 +11,7 @@ export interface PluginOptions { compositionOnly?: boolean ssr?: boolean hmr?: boolean + appRootContainer?: string fullInstall?: boolean forceStringify?: boolean defaultSFCLang?: SFCLangFormat From 11c7603960dea3e8522ea35f2c39a5107b18fa03 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 21 Feb 2025 18:55:38 +0100 Subject: [PATCH 7/7] feat: enable HMR by default --- packages/unplugin-vue-i18n/README.md | 4 ++-- packages/unplugin-vue-i18n/src/core/options.ts | 2 +- packages/unplugin-vue-i18n/src/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/unplugin-vue-i18n/README.md b/packages/unplugin-vue-i18n/README.md index 7aedeb9b..d0b820b4 100644 --- a/packages/unplugin-vue-i18n/README.md +++ b/packages/unplugin-vue-i18n/README.md @@ -610,9 +610,9 @@ If do you will use this option, you need to enable `jitCompilation` option. ### `hmr` - **Type:** `boolean` -- **Default:** `false` +- **Default:** `true` - Enables HMR without page reload for bundled virtual messages `@intlify/unplugin-vue-i18n/messages`. + HMR without page reload for bundled virtual messages `@intlify/unplugin-vue-i18n/messages`. > [!IMPORTANT] > Vite support only diff --git a/packages/unplugin-vue-i18n/src/core/options.ts b/packages/unplugin-vue-i18n/src/core/options.ts index dd6becff..467376f8 100644 --- a/packages/unplugin-vue-i18n/src/core/options.ts +++ b/packages/unplugin-vue-i18n/src/core/options.ts @@ -57,7 +57,7 @@ export function resolveOptions(options: PluginOptions) { const ssrBuild = !!options.ssr const allowDynamic = !!options.allowDynamic - const hmr = !!options.hmr + const hmr = options.hmr ?? true const appRootContainer = options.appRootContainer ?? '#app' const strictMessage = isBoolean(options.strictMessage) diff --git a/packages/unplugin-vue-i18n/src/index.ts b/packages/unplugin-vue-i18n/src/index.ts index 8b5d0bb4..3a1b0fba 100644 --- a/packages/unplugin-vue-i18n/src/index.ts +++ b/packages/unplugin-vue-i18n/src/index.ts @@ -29,7 +29,7 @@ export const unpluginFactory: UnpluginFactory = ( const resolvedOptions = resolveOptions(options) debug('plugin options (resolved):', resolvedOptions) - if (resolvedOptions.hmr && meta.framework === 'webpack') { + if (options.hmr && meta.framework === 'webpack') { warn(getWebpackNotSupportedMessage('hmr')) }