Skip to content

Commit

Permalink
fix: options passed with installModule are overwritten (#2882)
Browse files Browse the repository at this point in the history
* fix: locales provided via `installModule` being ignored

* fix: use `locales` and `vueI18n` passed by `installModule`

* test: add `installModule` options tests

* docs: add `installModule` documentation
  • Loading branch information
BobbieGoede committed Mar 27, 2024
1 parent 86d847c commit 860dca9
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 9 deletions.
2 changes: 1 addition & 1 deletion docs/content/docs/2.guide/12.extend-messages.md
Expand Up @@ -15,7 +15,7 @@ Translations added this way will be loaded after those added in your project, an
Example:
::code-group

```ts[my-module-example/module.ts]
```ts [my-module-example/module.ts]
import { createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/2.guide/14.layers.md
@@ -1,6 +1,6 @@
---
title: Layers pages
description: How Nuxt i18n handles layers.
title: Layers
description: Using layers to extends projects with Nuxt i18n.
---

Nuxt i18n module supports layers and will automatically combine i18n configuration of all extended layers. [Read more about layers here](https://nuxt.com/docs/getting-started/layers)
Expand Down
66 changes: 66 additions & 0 deletions docs/content/docs/2.guide/16.install-module.md
@@ -0,0 +1,66 @@
---
title: Installing from a module
description: How to install Nuxt i18n using `installModule` inside of a module.
---

If you're a **module author** and want your module to install Nuxt i18n, you can do so using `installModule` but you will have to resolve paths used for `vueI18n`, `langDir` and those configured in `locales`.

::callout
We strongly recommend using [layers](/docs/guide/layers) for complete module installations over using `installModule`, layers are merged by priority which allows projects to overwrite options as desired and will not cause conflicts if more than one layer provides options for the Nuxt i18n module.

:br :br

If you would only like your module to provide translations, consider using the hook described in [extend-messages](/docs/guide/extend-messages) instead.
::

Note that when using `installModule`, the options passed will essentially have a higher priority than any layer (including the project layer), options are merged when possible and applicable but will otherwise override configurations.

Example:
::code-group

```ts [my-module-example/module.ts]
import { createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
async setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)

// paths needs to be resolved so absolute paths are used
await installModule('@nuxtjs/i18n', {
vueI18n: resolve('./i18n.config.ts'),
langDir: resolve('./lang'),
locales: [
{
code: 'en',
file: resolve('./lang/en.json'),
},
{
code: 'fr',
file: resolve('./lang/fr.json'),
},
]
})
}
})
```

```json [lang/en.json]
{
"my-module-example": {
"hello": "Hello from external module"
}
}
```

```json [lang/fr.json]
{
"my-module-example": {
"hello": "Bonjour depuis le module externe"
}
}
```

::

Now the project has access to new messages and can use them through `$t('my-module-example.hello')`.

File renamed without changes.
7 changes: 7 additions & 0 deletions specs/basic_usage.spec.ts
Expand Up @@ -617,4 +617,11 @@ describe('basic usage', async () => {
expect(dom.querySelector('head #switch-locale-path').content).toEqual('/fr/composables')
expect(dom.querySelector('head #route-base-name').content).toEqual('nested-test-route')
})

test('(#2874) options `locales` and `vueI18n` passed using `installModule` are not overridden', async () => {
const { page } = await renderPage('/')

expect(await getText(page, '#install-module-locale')).toEqual('Installer module locale works!')
expect(await getText(page, '#install-module-vue-i18n')).toEqual('Installer module vue-i18n works!')
})
})
9 changes: 9 additions & 0 deletions specs/fixtures/basic_usage/installer-module/i18n.config.ts
@@ -0,0 +1,9 @@
import type { I18nOptions } from 'vue-i18n'

export default {
messages: {
en: {
installerModuleVueI18nMessage: 'Installer module vue-i18n works!'
}
}
} as I18nOptions
20 changes: 20 additions & 0 deletions specs/fixtures/basic_usage/installer-module/index.ts
@@ -0,0 +1,20 @@
import { createResolver, defineNuxtModule, installModule } from '@nuxt/kit'

export default defineNuxtModule({
async setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)

installModule('@nuxtjs/i18n', {
langDir: resolve('./locales'),
vueI18n: resolve('./i18n.config.ts'),
locales: [
{
code: 'en',
iso: 'en',
files: [resolve('./locales/en.json')],
name: 'English'
}
]
})
}
})
3 changes: 3 additions & 0 deletions specs/fixtures/basic_usage/installer-module/locales/en.json
@@ -0,0 +1,3 @@
{
"installerModuleLocaleMessage": "Installer module locale works!"
}
2 changes: 1 addition & 1 deletion specs/fixtures/basic_usage/nuxt.config.ts
@@ -1,7 +1,7 @@
// https://nuxt.com/docs/guide/directory-structure/nuxt.config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['./layer-module', '@nuxtjs/i18n'],
modules: ['./layer-module', './installer-module', '@nuxtjs/i18n'],
runtimeConfig: {
public: {
runtimeValue: 'Hello from runtime config!',
Expand Down
4 changes: 4 additions & 0 deletions specs/fixtures/basic_usage/pages/index.vue
Expand Up @@ -239,5 +239,9 @@ useHead({
<code id="global-scope-properties">{{ localeProperties }}</code>
<LocalScope />
</section>
<section>
<div id="install-module-locale">{{ $t('installerModuleLocaleMessage') }}</div>
<div id="install-module-vue-i18n">{{ $t('installerModuleVueI18nMessage') }}</div>
</section>
</div>
</template>
62 changes: 58 additions & 4 deletions src/layers.ts
@@ -1,14 +1,21 @@
import createDebug from 'debug'
import { getLayerI18n, getProjectPath, mergeConfigLocales, resolveVueI18nConfigInfo, formatMessage } from './utils'
import {
getLayerI18n,
getProjectPath,
mergeConfigLocales,
resolveVueI18nConfigInfo,
formatMessage,
getLocaleFiles
} from './utils'

import { useLogger } from '@nuxt/kit'
import { isAbsolute, resolve } from 'pathe'
import { isAbsolute, parse, resolve } from 'pathe'
import { isString } from '@intlify/shared'
import { NUXT_I18N_MODULE_ID } from './constants'

import type { LocaleConfig } from './utils'
import type { Nuxt, NuxtConfigLayer } from '@nuxt/schema'
import type { NuxtI18nOptions, VueI18nConfigPathInfo } from './types'
import type { LocaleObject, NuxtI18nOptions, VueI18nConfigPathInfo } from './types'

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

Expand Down Expand Up @@ -60,6 +67,11 @@ export const checkLayerOptions = (options: NuxtI18nOptions, nuxt: Nuxt) => {
}
}

/**
* Merges `locales` configured by each layer and resolves the locale `files` to absolute paths.
*
* This overwrites `options.locales`
*/
export const applyLayerOptions = (options: NuxtI18nOptions, nuxt: Nuxt) => {
const project = nuxt.options._layers[0]
const layers = nuxt.options._layers
Expand Down Expand Up @@ -107,6 +119,43 @@ export const mergeLayerLocales = (options: NuxtI18nOptions, nuxt: Nuxt) => {
}
})

const localeObjects = options.locales.filter((x): x is LocaleObject => x !== 'string')
const absoluteConfigMap = new Map<string, LocaleConfig>()
// locale `files` use absolute paths installed using `installModule`
const absoluteLocaleObjects = localeObjects.filter(localeObject => {
const files = getLocaleFiles(localeObject)
if (files.length === 0) return false
return files.every(
file => isAbsolute(file.path) && configs.find(config => config.langDir === parse(file.path).dir) == null
)
})

// filter layer locales
for (const absoluteLocaleObject of absoluteLocaleObjects) {
const files = getLocaleFiles(absoluteLocaleObject)
if (files.length === 0) continue
const langDir = parse(files[0].path).dir

if (absoluteConfigMap.has(langDir)) {
const entry = absoluteConfigMap.get(langDir)

absoluteConfigMap.set(langDir, {
langDir,
projectLangDir,
locales: [...(entry!.locales! as LocaleObject[]), absoluteLocaleObject]
})
continue
}

absoluteConfigMap.set(langDir, {
langDir,
projectLangDir,
locales: [absoluteLocaleObject]
})
}

configs.unshift(...Array.from(absoluteConfigMap.values()))

return mergeConfigLocales(configs)
}

Expand All @@ -126,7 +175,7 @@ export const getLayerLangPaths = (nuxt: Nuxt) => {
}) as string[]
}

export async function resolveLayerVueI18nConfigInfo(nuxt: Nuxt, buildDir: string) {
export async function resolveLayerVueI18nConfigInfo(options: NuxtI18nOptions, nuxt: Nuxt, buildDir: string) {
const logger = useLogger(NUXT_I18N_MODULE_ID)
const layers = [...nuxt.options._layers]
const project = layers.shift() as NuxtConfigLayer
Expand Down Expand Up @@ -158,5 +207,10 @@ export async function resolveLayerVueI18nConfigInfo(nuxt: Nuxt, buildDir: string
})
)

// use `vueI18n` passed by `installModule`
if (options.vueI18n && isAbsolute(options.vueI18n)) {
resolved.unshift(await resolveVueI18nConfigInfo({ vueI18n: options.vueI18n }, buildDir, parse(options.vueI18n).dir))
}

return resolved.filter((x): x is Required<VueI18nConfigPathInfo> => x != null)
}
2 changes: 1 addition & 1 deletion src/module.ts
Expand Up @@ -186,7 +186,7 @@ export default defineNuxtModule<NuxtI18nOptions>({
* resolve vue-i18n config path
*/

const vueI18nConfigPaths = await resolveLayerVueI18nConfigInfo(nuxt, nuxt.options.buildDir)
const vueI18nConfigPaths = await resolveLayerVueI18nConfigInfo(options, nuxt, nuxt.options.buildDir)
debug('VueI18nConfigPaths', vueI18nConfigPaths)

/**
Expand Down

0 comments on commit 860dca9

Please sign in to comment.