Skip to content

Commit

Permalink
feat: expose hook for extending messages (#1319)
Browse files Browse the repository at this point in the history
Co-authored-by: existe_deja <[email protected]>
Co-authored-by: Rafal Chlodnicki <[email protected]>
  • Loading branch information
3 people committed Nov 4, 2021
1 parent afd70b0 commit 696bd12
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 5 deletions.
42 changes: 42 additions & 0 deletions docs/content/en/extend-messages.md
Original file line number Diff line number Diff line change
@@ -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')`.

<alert>
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.
</alert>
5 changes: 4 additions & 1 deletion src/core/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
Expand All @@ -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 => {
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { buildHook, createExtendRoutesHook } from './core/hooks'
import { formatMessage } from './templates/utils-common'

/** @type {import('@nuxt/types').Module<import('../types').Options>} */
export default function (moduleOptions) {
export default async function (moduleOptions) {
/** @type {import('../types/internal').ResolvedOptions} */
const options = merge({}, DEFAULT_OPTIONS, moduleOptions, this.options.i18n)

Expand Down Expand Up @@ -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')
Expand Down
9 changes: 8 additions & 1 deletion src/templates/plugin.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/templates/plugin.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
/* <% } %> */
Expand Down Expand Up @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions test/fixture/extend-locales/lang/en.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
home: 'langDir Homepage'
}
3 changes: 3 additions & 0 deletions test/fixture/extend-locales/lang/fr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
home: 'langDir Accueil'
}
19 changes: 19 additions & 0 deletions test/fixture/extend-locales/modules/externalModule/index.js
Original file line number Diff line number Diff line change
@@ -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'
}
}
})
})
}
19 changes: 19 additions & 0 deletions test/fixture/extend-locales/modules/externalModuleBis/index.js
Original file line number Diff line number Diff line change
@@ -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'
}
}
})
})
}
11 changes: 11 additions & 0 deletions test/fixture/extend-locales/nuxt.config.js
Original file line number Diff line number Diff line change
@@ -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
Empty file.
68 changes: 68 additions & 0 deletions test/module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
3 changes: 2 additions & 1 deletion types/internal.d.ts
Original file line number Diff line number Diff line change
@@ -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 '.'

Expand All @@ -12,6 +12,7 @@ export interface ResolvedOptions extends Omit<Required<Options>, 'detectBrowserL
detectBrowserLanguage: Required<DetectBrowserLanguageOptions> | false
localeCodes: readonly Locale[]
normalizedLocales: readonly LocaleObject[]
additionalMessages: LocaleMessages[]
vueI18n: I18nOptions | ((context: NuxtContext) => Promise<I18nOptions>)
vuex: Required<VuexOptions> | false
}
Expand Down

0 comments on commit 696bd12

Please sign in to comment.