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 }