diff --git a/docs/content/en/extend-messages.md b/docs/content/en/extend-messages.md
new file mode 100644
index 000000000..79641062a
--- /dev/null
+++ b/docs/content/en/extend-messages.md
@@ -0,0 +1,42 @@
+---
+title: Extending messages hook
+description: "Nuxt hook to extend app's messages"
+position: 14
+category: Guide
+---
+If you're a **module author** and want that module to provide extra messages for your project, you can merge them into the normally loaded messages by using the `i18n:extend-messages` hook.
+
+To do this, in your module's setup file listen to the Nuxt hook and push your messages. `@nuxtjs/i18n` will do the rest.
+
+This is particularly useful if your module use translated content and you want to offer to users nice default translations.
+
+Example:
+
+```js{}[my-module-exemple/setup.js]
+export default function () {
+ const { nuxt } = this
+
+ nuxt.hook('i18n:extend-messages', function (additionalMessages) {
+ additionalMessages.push({
+ en: {
+ 'my-module-exemple': {
+ hello: 'Hello from external module'
+ }
+ },
+ fr: {
+ 'my-module-exemple': {
+ hello: 'Bonjour depuis le module externe'
+ }
+ }
+ })
+ })
+}
+```
+
+Now the project has access to new messages and can use them through `$t('my-module-exemple.hello')`.
+
+
+Because module's messages are merged with the project's ones, it's safer to prefix them.
+
+Main project messages **will always override** the module's ones.
+
diff --git a/src/core/hooks.js b/src/core/hooks.js
index 2acfc4ce3..a0728306b 100644
--- a/src/core/hooks.js
+++ b/src/core/hooks.js
@@ -41,7 +41,7 @@ export function createExtendRoutesHook (options) {
*
* @param {import('../../types/internal').ResolvedOptions} options
*/
-export function buildHook (options) {
+export async function buildHook (options) {
if (options.strategy === STRATEGIES.NO_PREFIX && options.differentDomains) {
// eslint-disable-next-line no-console
console.warn(formatMessage('The `differentDomains` option and `no_prefix` strategy are not compatible. Change strategy or disable `differentDomains` option.'))
@@ -52,6 +52,9 @@ export function buildHook (options) {
console.warn(formatMessage('The `forwardedHost` option is deprecated. You can safely remove it. See: https://github.com/nuxt-community/i18n-module/pull/630.'))
}
+ options.additionalMessages = []
+ await this.nuxt.callHook('i18n:extend-messages', options.additionalMessages)
+
// Add vue-i18n-loader if applicable
if (options.vueI18nLoader) {
this.extendBuild(config => {
diff --git a/src/index.js b/src/index.js
index 3cf47361e..e97d8e92a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,7 +8,7 @@ import { buildHook, createExtendRoutesHook } from './core/hooks'
import { formatMessage } from './templates/utils-common'
/** @type {import('@nuxt/types').Module} */
-export default function (moduleOptions) {
+export default async function (moduleOptions) {
/** @type {import('../types/internal').ResolvedOptions} */
const options = merge({}, DEFAULT_OPTIONS, moduleOptions, this.options.i18n)
@@ -87,7 +87,7 @@ export default function (moduleOptions) {
this.extendRoutes(createExtendRoutesHook.call(this, options))
}
- this.nuxt.hook('build:before', () => buildHook.call(this, options))
+ await this.nuxt.hook('build:before', () => buildHook.call(this, options))
this.options.alias['~i18n-klona'] = require.resolve('klona/full').replace(/\.js$/, '.mjs')
this.options.alias['~i18n-ufo'] = require.resolve('ufo').replace(/\.js$/, '.mjs')
diff --git a/src/templates/plugin.main.js b/src/templates/plugin.main.js
index c3fed64e0..74ac8c9aa 100644
--- a/src/templates/plugin.main.js
+++ b/src/templates/plugin.main.js
@@ -11,7 +11,12 @@ import {
parseAcceptLanguage,
setLocaleCookie
} from './utils-common'
-import { loadLanguageAsync, resolveBaseUrl, registerStore } from './plugin.utils'
+import {
+ loadLanguageAsync,
+ resolveBaseUrl,
+ registerStore,
+ mergeAdditionalMessages
+} from './plugin.utils'
// @ts-ignore
import { joinURL } from '~i18n-ufo'
// @ts-ignore
@@ -127,6 +132,8 @@ export default async (context) => {
// Load all locales.
await Promise.all(options.localeCodes.map(locale => loadLanguageAsync(context, locale)))
}
+ } else {
+ mergeAdditionalMessages(app.i18n, options.additionalMessages, options.localeCodes)
}
app.i18n.locale = newLocale
diff --git a/src/templates/plugin.utils.js b/src/templates/plugin.utils.js
index ef8d4151e..cf22f90db 100755
--- a/src/templates/plugin.utils.js
+++ b/src/templates/plugin.utils.js
@@ -51,6 +51,7 @@ export async function loadLanguageAsync (context, locale) {
}
if (messages) {
i18n.setLocaleMessage(locale, messages)
+ mergeAdditionalMessages(i18n, options.additionalMessages, options.localeCodes, [locale])
i18n.loadedLanguages.push(locale)
}
/* <% } %> */
@@ -195,6 +196,26 @@ export function validateRouteParams (routeParams, localeCodes) {
}
}
+/**
+ * Merge external additional messages
+ *
+ * @param {import('../../types').NuxtI18nInstance} i18n
+ * @param {ResolvedOptions['additionalMessages']} additionalMessages
+ * @param {ResolvedOptions['localeCodes']} localeCodes
+ * @param {string[] | null} [onlyLocales=null]
+ * @return {void}
+ */
+export function mergeAdditionalMessages (i18n, additionalMessages, localeCodes, onlyLocales) {
+ const locales = onlyLocales || localeCodes
+ for (const additionalEntry of additionalMessages) {
+ for (const locale of locales) {
+ const existingMessages = i18n.getLocaleMessage(locale)
+ i18n.mergeLocaleMessage(locale, additionalEntry[locale])
+ i18n.mergeLocaleMessage(locale, existingMessages)
+ }
+ }
+}
+
/**
* @param {any} value
* @return {boolean}
diff --git a/test/fixture/extend-locales/lang/en.js b/test/fixture/extend-locales/lang/en.js
new file mode 100644
index 000000000..76159f260
--- /dev/null
+++ b/test/fixture/extend-locales/lang/en.js
@@ -0,0 +1,3 @@
+export default {
+ home: 'langDir Homepage'
+}
diff --git a/test/fixture/extend-locales/lang/fr.js b/test/fixture/extend-locales/lang/fr.js
new file mode 100644
index 000000000..d7087b993
--- /dev/null
+++ b/test/fixture/extend-locales/lang/fr.js
@@ -0,0 +1,3 @@
+export default {
+ home: 'langDir Accueil'
+}
diff --git a/test/fixture/extend-locales/modules/externalModule/index.js b/test/fixture/extend-locales/modules/externalModule/index.js
new file mode 100644
index 000000000..519e548ef
--- /dev/null
+++ b/test/fixture/extend-locales/modules/externalModule/index.js
@@ -0,0 +1,19 @@
+/** @type {import('@nuxt/types').Module} */
+export default function () {
+ const { nuxt } = this
+
+ nuxt.hook('i18n:extend-messages', function (additionalMessages) {
+ additionalMessages.push({
+ en: {
+ 'external-module': {
+ hello: 'Hello external module'
+ }
+ },
+ fr: {
+ 'external-module': {
+ hello: 'Bonjour module externe'
+ }
+ }
+ })
+ })
+}
diff --git a/test/fixture/extend-locales/modules/externalModuleBis/index.js b/test/fixture/extend-locales/modules/externalModuleBis/index.js
new file mode 100644
index 000000000..a78a45152
--- /dev/null
+++ b/test/fixture/extend-locales/modules/externalModuleBis/index.js
@@ -0,0 +1,19 @@
+/** @type {import('@nuxt/types').Module} */
+export default function () {
+ const { nuxt } = this
+
+ nuxt.hook('i18n:extend-messages', function (additionalMessages) {
+ additionalMessages.push({
+ en: {
+ 'external-module-bis': {
+ hello: 'Hello external module bis'
+ }
+ },
+ fr: {
+ 'external-module-bis': {
+ hello: 'Bonjour module externe bis'
+ }
+ }
+ })
+ })
+}
diff --git a/test/fixture/extend-locales/nuxt.config.js b/test/fixture/extend-locales/nuxt.config.js
new file mode 100644
index 000000000..c6b2135d6
--- /dev/null
+++ b/test/fixture/extend-locales/nuxt.config.js
@@ -0,0 +1,11 @@
+import { resolve } from 'path'
+import BaseConfig from '../base.config'
+
+/** @type {import('@nuxt/types').NuxtConfig} */
+const config = {
+ ...BaseConfig,
+ buildDir: resolve(__dirname, '.nuxt'),
+ srcDir: __dirname
+}
+
+module.exports = config
diff --git a/test/fixture/extend-locales/store/index.js b/test/fixture/extend-locales/store/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/module.test.js b/test/module.test.js
index f4de000e2..6e5d1277c 100644
--- a/test/module.test.js
+++ b/test/module.test.js
@@ -2511,3 +2511,71 @@ describe('Store', () => {
expect(dom.querySelector('#store-path-fr')?.textContent).toBe('/fr/a-propos')
})
})
+
+describe('Extend Locale with additionalMessages', () => {
+ /** @type {Nuxt} */
+ let nuxt
+ afterEach(async () => {
+ await nuxt.close()
+ })
+
+ test('should define additionalMessages from i18n:extend-messages hook', async () => {
+ const override = {
+ buildModules: [
+ '~/modules/externalModule'
+ ]
+ }
+ const localConfig = loadConfig(__dirname, 'extend-locales', override, { merge: true })
+ nuxt = (await setup(localConfig)).nuxt
+ const window = await nuxt.renderAndGetWindow(url('/'))
+ expect(window.$nuxt.$i18n.messages.en['external-module'].hello).toEqual('Hello external module')
+ })
+
+ test('should merge multiple additionalMessages', async () => {
+ const override = {
+ buildModules: [
+ '~/modules/externalModule'
+ ]
+ }
+ const localConfig = loadConfig(__dirname, 'extend-locales', override, { merge: true })
+ nuxt = (await setup(localConfig)).nuxt
+ const window = await nuxt.renderAndGetWindow(url('/'))
+ expect(window.$nuxt.$i18n.messages.en['external-module'].hello).toEqual('Hello external module')
+ })
+
+ test('should merge additionalMessages from different modules through i18n:extend-messages hook', async () => {
+ const override = {
+ buildModules: [
+ '~/modules/externalModule',
+ '~/modules/externalModuleBis'
+ ]
+ }
+ const localConfig = loadConfig(__dirname, 'extend-locales', override, { merge: true })
+ nuxt = (await setup(localConfig)).nuxt
+ const window = await nuxt.renderAndGetWindow(url('/'))
+ expect(window.$nuxt.$i18n.messages.en['external-module'].hello).toEqual('Hello external module')
+ expect(window.$nuxt.$i18n.messages.en['external-module-bis'].hello).toEqual('Hello external module bis')
+ })
+
+ test('should override translations from additionalMessages', async () => {
+ const override = {
+ i18n: {
+ vueI18n: {
+ messages: {
+ en: {
+ 'external-module': {
+ hello: 'Hello from project'
+ }
+ }
+ }
+ }
+ },
+ buildModules: [
+ '~/modules/externalModule'
+ ]
+ }
+ nuxt = (await setup(loadConfig(__dirname, 'extend-locales', override, { merge: true }))).nuxt
+ const window = await nuxt.renderAndGetWindow(url('/'))
+ expect(window.$nuxt.$i18n.messages.en['external-module'].hello).toEqual('Hello from project')
+ })
+})
diff --git a/types/internal.d.ts b/types/internal.d.ts
index 0bddf7fb2..d04e959cb 100644
--- a/types/internal.d.ts
+++ b/types/internal.d.ts
@@ -1,7 +1,7 @@
import { IncomingMessage } from 'http'
import { Context as NuxtContext } from '@nuxt/types'
import { Route } from 'vue-router'
-import { LocaleMessageObject, I18nOptions, Locale } from 'vue-i18n'
+import { LocaleMessageObject, I18nOptions, Locale, LocaleMessages } from 'vue-i18n'
import Vue from 'vue'
import { DetectBrowserLanguageOptions, VuexOptions, Options, LocaleObject } from '.'
@@ -12,6 +12,7 @@ export interface ResolvedOptions extends Omit, 'detectBrowserL
detectBrowserLanguage: Required | false
localeCodes: readonly Locale[]
normalizedLocales: readonly LocaleObject[]
+ additionalMessages: LocaleMessages[]
vueI18n: I18nOptions | ((context: NuxtContext) => Promise)
vuex: Required | false
}