From d898a07e482bf118d58d0a7d032694bd38f70489 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Wed, 29 Mar 2023 16:24:55 +0200 Subject: [PATCH] feat: add basic module locale merging (#1955) * feat: add basic module locale merging * refactor: rewrite LocaleObject merging to be generic and reusable * fix: register module hook type * docs: add documentation describing registerModule hook * test: add register module hook test --- docs/content/2.guide/13.extend-messages.md | 88 +++++++++++--- docs/content/4.API/5.nuxt.md | 36 +++++- playground/layer-module/index.ts | 32 ++++++ playground/layer-module/locales/en.json | 3 + playground/layer-module/locales/fr.json | 3 + playground/layer-module/locales/nl.json | 3 + playground/layer-module/nuxt.config.ts | 34 ++++++ playground/nuxt.config.ts | 31 +++-- .../basic_layer/layer-module/index.ts | 32 ++++++ .../basic_layer/layer-module/locales/en.json | 3 + .../basic_layer/layer-module/locales/fr.json | 3 + .../basic_layer/layer-module/locales/nl.json | 3 + .../basic_layer/layer-module/nuxt.config.ts | 34 ++++++ specs/fixtures/basic_layer/nuxt.config.ts | 4 +- specs/fixtures/basic_layer/pages/index.vue | 3 + specs/register_module.spec.ts | 24 ++++ src/bundler.ts | 15 ++- src/layers.ts | 53 ++------- src/module.ts | 4 +- src/types.ts | 4 + src/utils.ts | 108 +++++++++++++++++- 21 files changed, 439 insertions(+), 81 deletions(-) create mode 100644 playground/layer-module/index.ts create mode 100644 playground/layer-module/locales/en.json create mode 100644 playground/layer-module/locales/fr.json create mode 100644 playground/layer-module/locales/nl.json create mode 100644 playground/layer-module/nuxt.config.ts create mode 100644 specs/fixtures/basic_layer/layer-module/index.ts create mode 100644 specs/fixtures/basic_layer/layer-module/locales/en.json create mode 100644 specs/fixtures/basic_layer/layer-module/locales/fr.json create mode 100644 specs/fixtures/basic_layer/layer-module/locales/nl.json create mode 100644 specs/fixtures/basic_layer/layer-module/nuxt.config.ts create mode 100644 specs/register_module.spec.ts diff --git a/docs/content/2.guide/13.extend-messages.md b/docs/content/2.guide/13.extend-messages.md index 31b963c7d..3f436d209 100644 --- a/docs/content/2.guide/13.extend-messages.md +++ b/docs/content/2.guide/13.extend-messages.md @@ -1,18 +1,22 @@ # Extending messages hook -Nuxt hook to extend app's messages. +Nuxt hooks to extend i18n messages in your project. --- -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. +For **module authors** that want to provide messages with their modules, there are now two options to add or merge them into the normally loaded messages. -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 uses translated content and you want to offer nice default translations. -This is particularly useful if your module use translated content and you want to offer to users nice default translations. +1. [The `i18n:extend-messages` hook](#i18nextend-messages) +2. [The `i18n:registerModule` hook](#i18nregistermodule) + +### `i18n:extend-messages` +In your module's setup file listen to the Nuxt `i18n:extend-messages` hook and push your messages. `@nuxtjs/i18n` will do the rest. Example: -```ts{}[my-module-exemple/module1.ts] +```ts{}[my-module-example/module1.ts] import { defineNuxtModule } from '@nuxt/kit' export default defineNuxtModule({ @@ -20,12 +24,12 @@ export default defineNuxtModule({ nuxt.hook('i18n:extend-messages', async (additionalMessages, localeCodes) => { additionalMessages.push({ en: { - 'my-module-exemple': { + 'my-module-example': { hello: 'Hello from external module' } }, fr: { - 'my-module-exemple': { + 'my-module-example': { hello: 'Bonjour depuis le module externe' } } @@ -36,29 +40,77 @@ export default defineNuxtModule({ ``` -Now the project has access to new messages and can use them through `$t('my-module-exemple.hello')`. +### `i18n:registerModule` +In your module's setup file listen to the Nuxt `i18n:registerModule` hook and register your i18n configuration, this is similar to how [lazy-load translations](./lazy-load-translations) are configured. -::alert{type="info"} +Translations added this way will be loaded after those added in your project, and before extended layers. + +Example: +::code-group + ::code-block{label="module.ts" active} + ```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) + nuxt.hook('i18n:registerModule', register => { + register({ + // langDir path needs to be resolved + langDir: resolve('./lang'), + locales: [ + { + code: 'en', + file: 'en.json', + }, + { + code: 'fr', + file: 'fr.json', + }, + ] + }) + }) + } + }) + ``` + :: + ::code-block{label="en.json"} + ```json + { + "my-module-example": { + "hello": "Hello from external module" + } + } + ``` + :: + ::code-block{label="fr.json"} + ```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')`. -The custom module that is use `i18n:extend-messages` hook should be inserted before nuxt i18n module. +::alert{type="info"} +These hooks will only work for modules registered before the `@nuxtjs/i18n` module. ```ts {}[nuxt.config.ts] -import CustomModule from './custom' // import your custom module +import ExampleModule from './my-module-example/module.ts' // import your custom module export default defineNuxtConfig({ modules: [ - CustomModule, + ExampleModule, '@nuxtjs/i18n', ], }) ``` - :: ::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. - +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/docs/content/4.API/5.nuxt.md b/docs/content/4.API/5.nuxt.md index 44556d19c..4b79181c5 100644 --- a/docs/content/4.API/5.nuxt.md +++ b/docs/content/4.API/5.nuxt.md @@ -73,4 +73,38 @@ export default defineNuxtModule({ } ``` -See also [Extending messages hook](/guide/extend-messages) +See also [Extending messages hook](/guide/extend-messages#i18nextend-messages) + +### `i18n:registerModule` Hook + +- **Arguments**: + - registerModule (type: `({ langDir: string, locales: LocaleObject[] }) => void`) + + +```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) + nuxt.hook('i18n:registerModule', register => { + register({ + // langDir path needs to be resolved + langDir: resolve('./lang'), + locales: [ + { + code: 'en', + file: 'en.json', + }, + { + code: 'fr', + file: 'fr.json', + }, + ] + }) + }) + } +}) +``` + +See also [Extending messages hook](/guide/extend-messages#i18nregistermodule) diff --git a/playground/layer-module/index.ts b/playground/layer-module/index.ts new file mode 100644 index 000000000..ba83b9a32 --- /dev/null +++ b/playground/layer-module/index.ts @@ -0,0 +1,32 @@ +import { createResolver, defineNuxtModule } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup(options, nuxt) { + const { resolve } = createResolver(import.meta.url) + nuxt.hook('i18n:registerModule', register => { + register({ + langDir: resolve('./locales'), + locales: [ + { + code: 'en', + iso: 'en-US', + file: 'en.json', + name: 'English' + }, + { + code: 'fr', + iso: 'fr-FR', + file: 'fr.json', + name: 'Francais' + }, + { + code: 'nl', + iso: 'nl-NL', + file: 'nl.json', + name: 'Nederlands' + } + ] + }) + }) + } +}) diff --git a/playground/layer-module/locales/en.json b/playground/layer-module/locales/en.json new file mode 100644 index 000000000..212faa58b --- /dev/null +++ b/playground/layer-module/locales/en.json @@ -0,0 +1,3 @@ +{ + "moduleLayerText": "This is a merged module layer locale key" +} \ No newline at end of file diff --git a/playground/layer-module/locales/fr.json b/playground/layer-module/locales/fr.json new file mode 100644 index 000000000..bc1f68593 --- /dev/null +++ b/playground/layer-module/locales/fr.json @@ -0,0 +1,3 @@ +{ + "moduleLayerText": "This is a merged module layer locale key in French" +} \ No newline at end of file diff --git a/playground/layer-module/locales/nl.json b/playground/layer-module/locales/nl.json new file mode 100644 index 000000000..f48bc663e --- /dev/null +++ b/playground/layer-module/locales/nl.json @@ -0,0 +1,3 @@ +{ + "moduleLayerText": "This is a merged module layer locale key in Dutch" +} \ No newline at end of file diff --git a/playground/layer-module/nuxt.config.ts b/playground/layer-module/nuxt.config.ts new file mode 100644 index 000000000..828fdbfba --- /dev/null +++ b/playground/layer-module/nuxt.config.ts @@ -0,0 +1,34 @@ +// import type { NuxtApp } from 'nuxt/dist/app/index' + +// https://nuxt.com/docs/guide/directory-structure/nuxt.config +export default defineNuxtConfig({ + modules: ['@nuxtjs/i18n'], + i18n: { + langDir: 'locales', + lazy: true, + baseUrl: 'http://localhost:3000', + locales: [ + { + code: 'en', + iso: 'en-US', + file: 'en.json', + // domain: 'localhost', + name: 'English' + }, + { + code: 'fr', + iso: 'fr-FR', + file: 'fr.json', + // domain: 'localhost', + name: 'Francais' + }, + { + code: 'nl', + iso: 'nl-NL', + file: 'nl.json', + // domain: 'localhost', + name: 'Nederlands' + } + ] + } +}) diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 987a7a0ba..7465d79de 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,10 +1,19 @@ import Module1 from './module1' +import LayerModule from './layer-module' import type { NuxtApp } from 'nuxt/dist/app/index' // https://nuxt.com/docs/guide/directory-structure/nuxt.config export default defineNuxtConfig({ extends: ['layers/i18n-layer'], - modules: [Module1, '@nuxtjs/i18n', '@nuxt/devtools'], + modules: [ + (_, nuxt) => { + console.log(nuxt.options._installedModules) + }, + Module1, + LayerModule, + '@nuxtjs/i18n', + '@nuxt/devtools' + ], vite: { build: { minify: false @@ -19,7 +28,7 @@ export default defineNuxtConfig({ // } // } // }, - + debug: false, i18n: { experimental: { jsTsFormatResource: true @@ -61,7 +70,7 @@ export default defineNuxtConfig({ } ], // trailingSlash: true, - debug: true, + debug: false, defaultLocale: 'en', // strategy: 'no_prefix', // strategy: 'prefix', @@ -80,14 +89,14 @@ export default defineNuxtConfig({ }, // differentDomains: true, // skipSettingLocaleOnNavigate: true, - detectBrowserLanguage: false, - // detectBrowserLanguage: { - // useCookie: true, - // // alwaysRedirect: true - // // cookieKey: 'i18n_redirected', - // // // cookieKey: 'my_custom_cookie_name', - // // redirectOn: 'root' - // }, + // detectBrowserLanguage: false, + detectBrowserLanguage: { + useCookie: true + // alwaysRedirect: true + // cookieKey: 'i18n_redirected', + // // cookieKey: 'my_custom_cookie_name', + // redirectOn: 'root' + }, // vueI18n: './vue-i18n.options.ts' vueI18n: { legacy: false, diff --git a/specs/fixtures/basic_layer/layer-module/index.ts b/specs/fixtures/basic_layer/layer-module/index.ts new file mode 100644 index 000000000..ba83b9a32 --- /dev/null +++ b/specs/fixtures/basic_layer/layer-module/index.ts @@ -0,0 +1,32 @@ +import { createResolver, defineNuxtModule } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup(options, nuxt) { + const { resolve } = createResolver(import.meta.url) + nuxt.hook('i18n:registerModule', register => { + register({ + langDir: resolve('./locales'), + locales: [ + { + code: 'en', + iso: 'en-US', + file: 'en.json', + name: 'English' + }, + { + code: 'fr', + iso: 'fr-FR', + file: 'fr.json', + name: 'Francais' + }, + { + code: 'nl', + iso: 'nl-NL', + file: 'nl.json', + name: 'Nederlands' + } + ] + }) + }) + } +}) diff --git a/specs/fixtures/basic_layer/layer-module/locales/en.json b/specs/fixtures/basic_layer/layer-module/locales/en.json new file mode 100644 index 000000000..212faa58b --- /dev/null +++ b/specs/fixtures/basic_layer/layer-module/locales/en.json @@ -0,0 +1,3 @@ +{ + "moduleLayerText": "This is a merged module layer locale key" +} \ No newline at end of file diff --git a/specs/fixtures/basic_layer/layer-module/locales/fr.json b/specs/fixtures/basic_layer/layer-module/locales/fr.json new file mode 100644 index 000000000..bc1f68593 --- /dev/null +++ b/specs/fixtures/basic_layer/layer-module/locales/fr.json @@ -0,0 +1,3 @@ +{ + "moduleLayerText": "This is a merged module layer locale key in French" +} \ No newline at end of file diff --git a/specs/fixtures/basic_layer/layer-module/locales/nl.json b/specs/fixtures/basic_layer/layer-module/locales/nl.json new file mode 100644 index 000000000..f48bc663e --- /dev/null +++ b/specs/fixtures/basic_layer/layer-module/locales/nl.json @@ -0,0 +1,3 @@ +{ + "moduleLayerText": "This is a merged module layer locale key in Dutch" +} \ No newline at end of file diff --git a/specs/fixtures/basic_layer/layer-module/nuxt.config.ts b/specs/fixtures/basic_layer/layer-module/nuxt.config.ts new file mode 100644 index 000000000..828fdbfba --- /dev/null +++ b/specs/fixtures/basic_layer/layer-module/nuxt.config.ts @@ -0,0 +1,34 @@ +// import type { NuxtApp } from 'nuxt/dist/app/index' + +// https://nuxt.com/docs/guide/directory-structure/nuxt.config +export default defineNuxtConfig({ + modules: ['@nuxtjs/i18n'], + i18n: { + langDir: 'locales', + lazy: true, + baseUrl: 'http://localhost:3000', + locales: [ + { + code: 'en', + iso: 'en-US', + file: 'en.json', + // domain: 'localhost', + name: 'English' + }, + { + code: 'fr', + iso: 'fr-FR', + file: 'fr.json', + // domain: 'localhost', + name: 'Francais' + }, + { + code: 'nl', + iso: 'nl-NL', + file: 'nl.json', + // domain: 'localhost', + name: 'Nederlands' + } + ] + } +}) diff --git a/specs/fixtures/basic_layer/nuxt.config.ts b/specs/fixtures/basic_layer/nuxt.config.ts index a936b7fef..164cd54aa 100644 --- a/specs/fixtures/basic_layer/nuxt.config.ts +++ b/specs/fixtures/basic_layer/nuxt.config.ts @@ -1,9 +1,9 @@ -import pathe from 'pathe' -import { resolveFiles } from '@nuxt/kit' +import LayerModule from './layer-module' // https://nuxt.com/docs/guide/directory-structure/nuxt.config export default defineNuxtConfig({ // extends: ['./layer'], modules: [ + LayerModule, '@nuxtjs/i18n' // async (_, nuxt) => { // const layers = nuxt.options._layers diff --git a/specs/fixtures/basic_layer/pages/index.vue b/specs/fixtures/basic_layer/pages/index.vue index 0bbe882f0..8e9ffd509 100644 --- a/specs/fixtures/basic_layer/pages/index.vue +++ b/specs/fixtures/basic_layer/pages/index.vue @@ -70,5 +70,8 @@ function onClick() {

localeRoute

+
+ {{ $t('moduleLayerText') }} +
diff --git a/specs/register_module.spec.ts b/specs/register_module.spec.ts new file mode 100644 index 000000000..04c904f9b --- /dev/null +++ b/specs/register_module.spec.ts @@ -0,0 +1,24 @@ +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_layer`, import.meta.url)), + browser: true, + // overrides + nuxtConfig: {} +}) + +test('register module hook', async () => { + const home = url('/') + const page = await createPage() + await page.goto(home) + + expect(await getText(page, '#register-module')).toEqual('This is a merged module layer locale key') + + // click `fr` lang switch link + await page.locator('.switch-to-fr a').click() + + expect(await getText(page, '#register-module')).toEqual('This is a merged module layer locale key in French') +}) diff --git a/src/bundler.ts b/src/bundler.ts index 60939845b..f09c017f6 100644 --- a/src/bundler.ts +++ b/src/bundler.ts @@ -25,6 +25,12 @@ export async function extendBundler( const { nuxtOptions, hasLocaleFiles } = options const langPaths = getLayerLangPaths(nuxt) debug('langPaths -', langPaths) + const i18nModulePaths = + nuxt.options._layers[0].config.i18n?.i18nModules?.map(module => + resolve(nuxt.options._layers[0].config.rootDir, module.langDir ?? '') + ) ?? [] + debug('i18nModulePaths -', i18nModulePaths) + const localePaths = [...langPaths, ...i18nModulePaths] /** * setup nitro @@ -66,8 +72,9 @@ export async function extendBundler( strictMessage: nuxtOptions.precompile.strictMessage, escapeHtml: nuxtOptions.precompile.escapeHtml } - if (hasLocaleFiles && langPaths.length > 0) { - webpackPluginOptions.include = langPaths.map(x => resolve(x, './**')) + + if (hasLocaleFiles && localePaths.length > 0) { + webpackPluginOptions.include = localePaths.map(x => resolve(x, './**')) } addWebpackPlugin(ResourceProxyPlugin.webpack(proxyOptions)) @@ -100,8 +107,8 @@ export async function extendBundler( strictMessage: nuxtOptions.precompile.strictMessage, escapeHtml: nuxtOptions.precompile.escapeHtml } - if (hasLocaleFiles && langPaths.length > 0) { - vitePluginOptions.include = langPaths.map(x => resolve(x, './**')) + if (hasLocaleFiles && localePaths.length > 0) { + vitePluginOptions.include = localePaths.map(x => resolve(x, './**')) } addVitePlugin(ResourceProxyPlugin.vite(proxyOptions)) diff --git a/src/layers.ts b/src/layers.ts index 25bd6bc56..a2da8bff6 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -3,26 +3,10 @@ import type { LocaleObject } from 'vue-i18n-routing' import type { NuxtI18nOptions } from './types' import createDebug from 'debug' import pathe from 'pathe' +import { getProjectPath, mergeConfigLocales } from './utils' const debug = createDebug('@nuxtjs/i18n:layers') -const getLocaleFiles = (locale: LocaleObject): string[] => { - if (locale.file != null) return [locale.file] - if (locale.files != null) return locale.files - return [] -} - -const localeFilesToRelative = (projectLangDir: string, layerLangDir: string, files: string[]) => { - const absoluteFiles = files.map(file => pathe.resolve(layerLangDir, file)) - const relativeFiles = absoluteFiles.map(file => pathe.relative(projectLangDir, file)) - return relativeFiles -} - -const getProjectPath = (nuxt: Nuxt, ...target: string[]) => { - const projectLayer = nuxt.options._layers[0] - return pathe.resolve(projectLayer.config.rootDir, ...target) -} - export const applyLayerOptions = (options: NuxtI18nOptions, nuxt: Nuxt) => { const project = nuxt.options._layers[0] const layers = nuxt.options._layers @@ -109,33 +93,16 @@ export const mergeLayerLocales = (nuxt: Nuxt) => { const projectLangDir = getProjectPath(nuxt, projectI18n.langDir) debug('project path', getProjectPath(nuxt)) - const mergedLocales: LocaleObject[] = [] - for (const layer of nuxt.options._layers) { - if (layer.config.i18n?.locales == null) continue - if (layer.config.i18n?.langDir == null) continue - - const layerLangDir = pathe.resolve(layer.config.rootDir, layer.config.i18n.langDir) - debug('layer langDir -', layerLangDir) + const configs = nuxt.options._layers + .filter(x => x.config.i18n?.locales != null && x.config.i18n?.langDir != null) + .map(x => ({ + ...x.config.i18n, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + langDir: pathe.resolve(x.config.rootDir, x.config.i18n!.langDir!), + projectLangDir + })) - for (const locale of layer.config.i18n.locales) { - if (typeof locale === 'string') continue - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { file, files, ...entry } = locale - const localeEntry = mergedLocales.find(x => x.code === locale.code) - - const fileEntries = getLocaleFiles(locale) - const relativeFiles = localeFilesToRelative(projectLangDir, layerLangDir, fileEntries) - - if (localeEntry == null) { - mergedLocales.push({ ...entry, files: relativeFiles }) - } else { - localeEntry.files = [...relativeFiles, ...(localeEntry?.files ?? [])] - } - } - } - - return mergedLocales + return mergeConfigLocales(configs) } return projectI18n.lazy ? mergeLazyLocales() : mergeSimpleLocales() diff --git a/src/module.ts b/src/module.ts index ca75c21a1..6ec0e70d6 100644 --- a/src/module.ts +++ b/src/module.ts @@ -29,7 +29,7 @@ import { NUXT_I18N_COMPOSABLE_DEFINE_ROUTE, NUXT_I18N_COMPOSABLE_DEFINE_LOCALE } from './constants' -import { formatMessage, getNormalizedLocales, resolveLocales, getPackageManagerType } from './utils' +import { formatMessage, getNormalizedLocales, resolveLocales, getPackageManagerType, mergeI18nModules } from './utils' import { distDir, runtimeDir, pkgModulesDir } from './dirs' import { applyLayerOptions } from './layers' @@ -80,6 +80,7 @@ export default defineNuxtModule({ throw new Error(formatMessage(`Cannot support nuxt version: ${getNuxtVersion(nuxt)}`)) } + await mergeI18nModules(options, nuxt) applyLayerOptions(options, nuxt) if (options.strategy === 'no_prefix' && options.differentDomains) { @@ -399,6 +400,7 @@ declare module '@nuxt/schema' { interface NuxtHooks { 'i18n:extend-messages': (messages: LocaleMessages[], localeCodes: string[]) => Promise + 'i18n:registerModule': (registerModule: (config: Pick) => void) => void } interface ConfigSchema { diff --git a/src/types.ts b/src/types.ts index 10a4a51d8..b0ea7b48e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,10 @@ export type NuxtI18nOptions = { lazy?: boolean pages?: CustomRoutePages customRoutes?: 'page' | 'config' + /** + * @internal + */ + i18nModules?: { langDir?: string | null; locales?: I18nRoutingOptions['locales'] }[] /** * @deprecated `'parsePages' option is deprecated. Please use 'customRoutes' option instead. We will remove it in v8 official release.` */ diff --git a/src/utils.ts b/src/utils.ts index 1d62a38b6..e6d8d2d08 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { promises as fs, constants as FS_CONSTANTS } from 'node:fs' import { resolveFiles } from '@nuxt/kit' -import { parse as parsePath, resolve } from 'pathe' +import { parse as parsePath, resolve, relative } from 'pathe' import { encodePath } from 'ufo' import { resolveLockfile } from 'pkg-types' import { isString } from '@intlify/shared' @@ -210,6 +210,112 @@ export function parseSegment(segment: string) { return tokens } +export const resolveRelativeLocales = ( + relativeFileResolver: (files: string[]) => string[], + locale: LocaleObject, + merged: LocaleObject | undefined +) => { + if (typeof locale === 'string') return merged + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { file, files, ...entry } = locale + + const fileEntries = getLocaleFiles(locale) + const relativeFiles = relativeFileResolver(fileEntries) + return { + ...entry, + ...merged, + files: [...relativeFiles, ...(merged?.files ?? [])] + } +} + +export const getLocaleFiles = (locale: LocaleObject): string[] => { + if (locale.file != null) return [locale.file] + if (locale.files != null) return locale.files + return [] +} + +export const localeFilesToRelative = (projectLangDir: string, layerLangDir: string, files: string[]) => { + const absoluteFiles = files.map(file => resolve(layerLangDir, file)) + const relativeFiles = absoluteFiles.map(file => relative(projectLangDir, file)) + + return relativeFiles +} + +export const getProjectPath = (nuxt: Nuxt, ...target: string[]) => { + const projectLayer = nuxt.options._layers[0] + return resolve(projectLayer.config.rootDir, ...target) +} + +export type LocaleConfig = { + projectLangDir?: string | null + langDir?: string | null + locales?: (string | LocaleObject)[] +} +/** + * Generically merge LocaleObject locales + * + * @param configs prepared configs to resolve locales relative to project + * @param baseLocales optional array of locale objects to merge configs into + */ +export const mergeConfigLocales = (configs: LocaleConfig[], baseLocales: LocaleObject[] = []) => { + const mergedLocales = new Map() + baseLocales.forEach(locale => mergedLocales.set(locale.code, locale)) + + for (const { locales, langDir, projectLangDir } of configs) { + if (locales == null) continue + if (langDir == null) continue + if (projectLangDir == null) continue + + for (const locale of locales) { + if (typeof locale === 'string') continue + + const filesResolver = (files: string[]) => localeFilesToRelative(projectLangDir, langDir, files) + const resolvedLocale = resolveRelativeLocales(filesResolver, locale, mergedLocales.get(locale.code)) + if (resolvedLocale != null) mergedLocales.set(locale.code, resolvedLocale) + } + } + + return Array.from(mergedLocales.values()) +} + +/** + * Merges project layer locales with registered i18n modules + */ +export const mergeI18nModules = async (options: NuxtI18nOptions, nuxt: Nuxt) => { + const projectLayer = nuxt.options._layers[0] + + if (projectLayer.config.i18n) projectLayer.config.i18n.i18nModules = [] + const registerI18nModule = (config: Pick) => { + if (config.langDir == null) return + projectLayer.config.i18n?.i18nModules?.push(config) + } + + await nuxt.callHook('i18n:registerModule', registerI18nModule) + const modules = projectLayer.config.i18n?.i18nModules ?? [] + const projectLangDir = getProjectPath(nuxt, projectLayer.config.i18n?.langDir ?? '') + + if (modules.length > 0) { + const baseLocales: LocaleObject[] = [] + const layerLocales = projectLayer.config.i18n?.locales ?? [] + + for (const locale of layerLocales) { + if (typeof locale !== 'object') continue + baseLocales.push({ ...locale, file: undefined, files: getLocaleFiles(locale) }) + } + + const mergedLocales = mergeConfigLocales( + modules.map(x => ({ ...x, projectLangDir })), + baseLocales + ) + + if (projectLayer.config.i18n) { + options.locales = mergedLocales + projectLayer.config.i18n.locales = mergedLocales + } + } +} + export function getRoutePath(tokens: SegmentToken[]): string { return tokens.reduce((path, token) => { // prettier-ignore