Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/unplugin-vue-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,22 @@ 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.

### `hmr`

- **Type:** `boolean`
- **Default:** `true`

HMR without page reload for bundled virtual messages `@intlify/unplugin-vue-i18n/messages`.

> [!IMPORTANT] > Vite support only

### `appRootContainer`

- **Type:** `string`
- **Default:** `'#app'`

The selector used find the Vue app root container during HMR.

### `useVueI18nImportName` (Experimental)

- **Type:** `boolean`
Expand Down
4 changes: 4 additions & 0 deletions packages/unplugin-vue-i18n/src/core/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export function resolveOptions(options: PluginOptions) {
const ssrBuild = !!options.ssr

const allowDynamic = !!options.allowDynamic
const hmr = options.hmr ?? true
const appRootContainer = options.appRootContainer ?? '#app'

const strictMessage = isBoolean(options.strictMessage)
? options.strictMessage
Expand All @@ -83,6 +85,8 @@ export function resolveOptions(options: PluginOptions) {
return {
include,
exclude,
hmr,
appRootContainer,
module: moduleType,
onlyLocales,
forceStringify,
Expand Down
60 changes: 57 additions & 3 deletions packages/unplugin-vue-i18n/src/core/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function resourcePlugin(
exclude,
module,
forceStringify,
appRootContainer,
defaultSFCLang,
globalSFCScope,
runtimeOnly,
Expand All @@ -61,6 +62,7 @@ export function resourcePlugin(
strictMessage,
allowDynamic,
escapeHtml,
hmr,
transformI18nBlock
}: ResolvedOptions,
meta: UnpluginContextMeta
Expand Down Expand Up @@ -230,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)) {
Expand Down Expand Up @@ -326,7 +338,9 @@ export function resourcePlugin(
{
forceStringify,
strictMessage,
escapeHtml
escapeHtml,
hmr,
appRootContainer
}
)
// TODO: support virtual import identifier
Expand Down Expand Up @@ -526,6 +540,8 @@ async function generateBundleResources(
strictMessage = true,
escapeHtml = false,
jit = true,
hmr = false,
appRootContainer,
transformI18nBlock = undefined
}: {
forceStringify?: boolean
Expand All @@ -534,6 +550,8 @@ async function generateBundleResources(
strictMessage?: boolean
escapeHtml?: boolean
jit?: boolean
hmr?: boolean
appRootContainer: string
transformI18nBlock?: PluginOptions['transformI18nBlock']
}
) {
Expand Down Expand Up @@ -562,6 +580,38 @@ 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('${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)

// 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) => {
Expand All @@ -582,9 +632,13 @@ const mergeDeep = (target, ...sources) => {
return mergeDeep(target, ...sources);
}

export default mergeDeep({},
const merged = mergeDeep({},
${codes.map(code => `{${code}}`).join(',\n')}
);`
);
export default merged

${isProduction || !hmr ? '' : hmrCode}
`
}

async function getCode(
Expand Down
16 changes: 11 additions & 5 deletions packages/unplugin-vue-i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -24,13 +29,14 @@ export const unpluginFactory: UnpluginFactory<PluginOptions | undefined> = (
const resolvedOptions = resolveOptions(options)
debug('plugin options (resolved):', resolvedOptions)

if (options.hmr && meta.framework === 'webpack') {
warn(getWebpackNotSupportedMessage('hmr'))
}

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))
}
Expand Down
2 changes: 2 additions & 0 deletions packages/unplugin-vue-i18n/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface PluginOptions {
runtimeOnly?: boolean
compositionOnly?: boolean
ssr?: boolean
hmr?: boolean
appRootContainer?: string
fullInstall?: boolean
forceStringify?: boolean
defaultSFCLang?: SFCLangFormat
Expand Down
8 changes: 8 additions & 0 deletions packages/unplugin-vue-i18n/src/utils/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 🙂.`
)
}