Skip to content

Commit

Permalink
feat: support extending messages hook (nuxt-modules#1550)
Browse files Browse the repository at this point in the history
* feat: support extending messages hook

* add e2e spec
  • Loading branch information
kazupon authored Oct 10, 2022
1 parent a4c9b01 commit 173e3e0
Show file tree
Hide file tree
Showing 20 changed files with 317 additions and 66 deletions.
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This todo is based on [nuxt/i18n](https://i18n.nuxtjs.org/) docs.
- [x] Different domains
- [x] Locale fallback
- [ ] Per-component translations
- [ ] Extending messages hook
- [x] Extending messages hook

## API Reference

Expand Down
39 changes: 18 additions & 21 deletions docs/content/30.guide/13.extend-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ title: Extending messages hook
description: "Nuxt hook to extend app's messages"
---

::alert{type="warning"}
// TODO:
🚧 This feature is not implemented yet.
::

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.
Expand All @@ -16,24 +11,26 @@ This is particularly useful if your module use translated content and you want t

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'
```ts{}[my-module-exemple/module1.ts]
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
async setup(options, nuxt) {
nuxt.hook('i18n:extend-messages', (additionalMessages, localeCodes) => {
additionalMessages.push({
en: {
'my-module-exemple': {
hello: 'Hello from external module'
}
},
fr: {
'my-module-exemple': {
hello: 'Bonjour depuis le module externe'
}
}
}
})
})
})
}
}
```

Expand Down
40 changes: 39 additions & 1 deletion docs/content/50.API/7.nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ If you set `i18n.vueI18n.legacy` option to `false` in your `@nuxtjs/i18n` config

Example use:

```js
```ts
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.$i18n.onBeforeLanguageSwitch = (oldLocale, newLocale, isInitialSetup, nuxtApp) => {
console.log('onBeforeLanguageSwitch', oldLocale, newLocale, isInitialSetup)
Expand All @@ -35,3 +35,41 @@ export default defineNuxtPlugin(nuxtApp => {
### localeHead()

See more info about those in [Extension of Vue](./vue) section.


## Extension of NuxtHooks

### `i18n:extend-messages` Hook

- **Arguments**:
- additionalMessages (type: ` LocaleMessages<DefineLocaleMessage>[]`)
- localeCodes (type: `string[]`) - locale codes, which is resolved with `locales` option

The `additionalMessages` array can be pushed locale messages paired with the locale.

Example:

```ts
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
async setup(options, nuxt) {
nuxt.hook('i18n:extend-messages', (additionalMessages, localeCodes) => {
additionalMessages.push({
en: {
'my-module-exemple': {
hello: 'Hello from external module'
}
},
fr: {
'my-module-exemple': {
hello: 'Bonjour depuis le module externe'
}
}
})
})
}
}
```
See also [Extending messages hook](../guide/extend-messages)
20 changes: 20 additions & 0 deletions playground/module1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
async setup(options, nuxt) {
// @ts-ignore
await nuxt.hook('i18n:extend-messages', (messages, localeCodes) => {
messages.push({
en: {
foo: 'Foo'
},
fr: {
foo: 'Foo FR'
},
ja: {
foo: 'Foo JA'
}
})
})
}
})
7 changes: 4 additions & 3 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { defineNuxtConfig } from 'nuxt'
// @ts-ignore
import I18nModule from '../dist/module.mjs'
import Module1 from './module1'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
modules: [I18nModule],
modules: [Module1, I18nModule],

vite: {
build: {
Expand Down Expand Up @@ -45,10 +46,10 @@ export default defineNuxtConfig({
// strategy: 'no_prefix',
// strategy: 'prefix',
// strategy: 'prefix_and_default',
parsePages: false,
// parsePages: false,
pages: {
about: {
ja: false
ja: '/about-ja'
}
},
// differentDomains: true,
Expand Down
1 change: 1 addition & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ definePageMeta({
console.log('useBrowserLocale', useBrowserLocale())
console.log('localeProperties', localeProperties)
console.log('foo', t('foo'))
function getLocaleName(code: string) {
const locale = (locales.value as LocaleObject[]).find(i => i.code === code)
Expand Down
30 changes: 30 additions & 0 deletions specs/extend_messages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test, expect } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, url, createPage } from '@nuxt/test-utils'
import { getText } from './helper'

await setup({
rootDir: fileURLToPath(new URL(`./fixtures/basic`, import.meta.url)),
browser: true,
// overrides
nuxtConfig: {
i18n: {
defaultLocale: 'en'
}
}
})

test('extend message hook', async () => {
const home = url('/')
const page = await createPage()
const messages: string[] = []
page.on('console', msg => messages.push(msg.text()))
await page.goto(home)

expect(await getText(page, '#extend-message')).toEqual('Hello from external module')

// click `fr` lang switch link
await page.locator('#lang-switcher-with-nuxt-link a').click()

expect(await getText(page, '#extend-message')).toEqual('Bonjour depuis le module externe')
})
21 changes: 21 additions & 0 deletions specs/fixtures/basic/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
async setup(options, nuxt) {
// @ts-ignore
await nuxt.hook('i18n:extend-messages', (messages, localeCodes) => {
messages.push({
en: {
'my-module-exemple': {
hello: 'Hello from external module'
}
},
fr: {
'my-module-exemple': {
hello: 'Bonjour depuis le module externe'
}
}
})
})
}
})
3 changes: 2 additions & 1 deletion specs/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defineNuxtConfig } from 'nuxt'
import I18nModule from '../../..'
import CustomModule from './module'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
modules: [I18nModule],
modules: [CustomModule, I18nModule],

i18n: {
lazy: false,
Expand Down
3 changes: 3 additions & 0 deletions specs/fixtures/basic/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ useHead({
>:
<code id="home-use-locale-head">{{ i18nHead }}</code>
</section>
<section>
<code id="extend-message">{{ t('my-module-exemple.hello') }}</code>
</section>
<NuxtLink id="link-about" exact :to="localePath('about')">{{ $t('about') }}</NuxtLink>
<NuxtLink id="link-blog" :to="localePath('blog')">{{ $t('blog') }}</NuxtLink>
<NuxtLink id="link-ignore-disable" :to="localePath('/ignore-routes/disable')"
Expand Down
25 changes: 13 additions & 12 deletions src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,29 @@ import type { NuxtI18nOptions } from './types'
const debug = createDebug('@nuxtjs/i18n:bundler')

export async function extendBundler(
options: Required<NuxtI18nOptions>,
nuxt: Nuxt,
hasLocaleFiles: boolean,
langPath: string | null
options: {
nuxtOptions: Required<NuxtI18nOptions>
hasLocaleFiles: boolean
langPath: string | null
}
) {
const { nuxtOptions, hasLocaleFiles, langPath } = options

// setup nitro
if (nuxt.options.nitro.replace) {
nuxt.options.nitro.replace['__DEBUG__'] = options.debug
nuxt.options.nitro.replace['__DEBUG__'] = nuxtOptions.debug
} else {
nuxt.options.nitro.replace = {
__DEBUG__: options.debug
__DEBUG__: nuxtOptions.debug
}
}
debug('nitro.replace', nuxt.options.nitro.replace)

// extract macros from components
const macroOptions: TransformMacroPluginOptions = {
dev: nuxt.options.dev,
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
macros: {
defineI18nRoute: 'i18n'
}
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client
}

try {
Expand All @@ -58,7 +59,7 @@ export async function extendBundler(
__VUE_I18N_FULL_INSTALL__: 'true',
__VUE_I18N_LEGACY_API__: 'true',
__INTLIFY_PROD_DEVTOOLS__: 'false',
__DEBUG__: JSON.stringify(options.debug)
__DEBUG__: JSON.stringify(nuxtOptions.debug)
})
)
})
Expand All @@ -80,10 +81,10 @@ export async function extendBundler(

extendViteConfig(config => {
if (config.define) {
config.define['__DEBUG__'] = JSON.stringify(options.debug)
config.define['__DEBUG__'] = JSON.stringify(nuxtOptions.debug)
} else {
config.define = {
__DEBUG__: JSON.stringify(options.debug)
__DEBUG__: JSON.stringify(nuxtOptions.debug)
}
}
debug('vite.config.define', config.define)
Expand Down
20 changes: 20 additions & 0 deletions src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { genImport, genSafeVariableName, genDynamicImport } from 'knitwork'

import type { NuxtI18nOptions, NuxtI18nInternalOptions, LocaleInfo } from './types'
import type { NuxtI18nOptionsDefault } from './constants'
import type { AdditionalMessages } from './messages'

export type LoaderOptions = {
localeCodes?: string[]
localeInfo?: LocaleInfo[]
nuxtI18nOptions?: NuxtI18nOptions
nuxtI18nOptionsDefault?: NuxtI18nOptionsDefault
nuxtI18nInternalOptions?: NuxtI18nInternalOptions
additionalMessages?: AdditionalMessages
}

const debug = createDebug('@nuxtjs/i18n:gen')
Expand Down Expand Up @@ -93,6 +95,8 @@ export function generateLoaderOptions(
}
codes += `}\n`
return codes
} else if (rootKey === 'additionalMessages') {
return `export const ${rootKey} = ${generaeteAdditionalMessages(rootValue, dev)}\n`
} else {
return `export const ${rootKey} = ${toCode(rootValue)}\n`
}
Expand Down Expand Up @@ -122,6 +126,22 @@ function generateVueI18nOptions(options: Record<string, any>, dev: boolean): str
return genCode
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function generaeteAdditionalMessages(value: Record<string, any>, dev: boolean): string {
let genCode = 'Object({'
for (const [locale, messages] of Object.entries(value)) {
genCode += `${JSON.stringify(locale)}:[`
for (const [, p] of Object.entries(messages)) {
genCode += `() => Promise.resolve(${
generateJSON(JSON.stringify(p), { type: 'bare', env: dev ? 'development' : 'production' }).code
}),`
}
genCode += `],`
}
genCode += '})'
return genCode
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function stringifyObj(obj: Record<string, any>): string {
return `Object({${Object.entries(obj)
Expand Down
31 changes: 31 additions & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import createDebug from 'debug'

import type { Nuxt } from '@nuxt/schema'
import type { DefineLocaleMessage, Locale, LocaleMessages } from 'vue-i18n'

const debug = createDebug('@nuxtjs/i18n:messages')

export type AdditionalMessages = Record<Locale, DefineLocaleMessage[]>

export async function extendMessages(nuxt: Nuxt, localeCodes: string[]): Promise<AdditionalMessages> {
const additionalMessages: LocaleMessages<DefineLocaleMessage>[] = []
await nuxt.callHook('i18n:extend-messages', additionalMessages, localeCodes)
debug('i18n:extend-messages additional messages', additionalMessages)

return normalizeAdditionalMessages(additionalMessages, localeCodes)
}

async function normalizeAdditionalMessages(additional: LocaleMessages<DefineLocaleMessage>[], localeCodes: string[]) {
const additionalMessages: AdditionalMessages = {}
for (const localeCode of localeCodes) {
additionalMessages[localeCode] = []
}

for (const [, messages] of Object.entries(additional)) {
for (const [locale, message] of Object.entries(messages)) {
additionalMessages[locale].push(message)
}
}

return additionalMessages
}
Loading

0 comments on commit 173e3e0

Please sign in to comment.