Skip to content

Commit

Permalink
feat: project relative layer locale resolution (nuxt-modules#2290)
Browse files Browse the repository at this point in the history
* test: add `overrides` property for overriding project layer options in tests

* feat: relative locale resolution

* feat: validate layer options

* test: fix incorrect test fixtures

* fix: use defu to merge inline options

* refactor: remove unused `langDir`
  • Loading branch information
BobbieGoede authored Aug 11, 2023
1 parent 32d571e commit 5c46d22
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 705 deletions.
856 changes: 405 additions & 451 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions specs/fixtures/basic_pages/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ export default defineNuxtConfig({
{
code: 'en',
iso: 'en',
name: 'English',
file: 'en-US.json'
name: 'English'
},
{
code: 'fr',
iso: 'fr-FR',
name: 'Français',
file: 'fr-FR.json'
name: 'Français'
}
],
defaultLocale: 'en',
Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/different_domains/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineNuxtConfig({
{
code: 'nl',
iso: 'nl-NL',
file: 'nl.json',
// file: 'nl.json',
domain: undefined,
name: 'Nederlands'
}
Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/layers/layer-domain/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default defineNuxtConfig({
differentDomains: true,
defaultLocale: 'en',
strategy: 'prefix_except_default',
// langDir: 'locales',
langDir: 'locales',
locales: [
{
code: 'en',
Expand Down
9 changes: 3 additions & 6 deletions specs/fixtures/layers/layer-pages/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ export default defineNuxtConfig({
{
code: 'en',
iso: 'en-US',
name: 'English',
file: 'en-US.json'
name: 'English'
},
{
code: 'fr',
iso: 'fr-FR',
name: 'Français',
file: 'fr-FR.json'
name: 'Français'
},
{
code: 'nl',
iso: 'nl-NL',
name: 'Nederlands',
file: 'nl-NL.json'
name: 'Nederlands'
}
]
}
Expand Down
12 changes: 11 additions & 1 deletion specs/utils/setup/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createTestContext, setTestContext } from '../context'
import { buildFixture, loadFixture, clearDir } from '../nuxt'
import { buildFixture, loadFixture } from '../nuxt'
import { startServer, stopServer } from '../server'
import { createBrowser } from '../browser'
import type { TestHooks, TestOptions } from '../types'
Expand Down Expand Up @@ -68,6 +68,16 @@ export function createTest(options: Partial<TestOptions>): TestHooks {
}

export async function setup(options: Partial<TestOptions> = {}) {
// Our layer support handles each layer individually (ignoring merged options)
// `nuxtConfig` overrides are not applied to `_layers` but passed to merged options
// `i18n.overrides` are applied at project layer to support overrides from tests

// @ts-ignore
if (options?.nuxtConfig?.i18n != null) {
// @ts-ignore
options.nuxtConfig.i18n = { ...options.nuxtConfig.i18n, overrides: options.nuxtConfig.i18n }
}

const hooks = createTest(options)

const setupFn = setupMaps[hooks.ctx.options.runner]
Expand Down
5 changes: 1 addition & 4 deletions src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@ export async function extendBundler(
options: {
nuxtOptions: Required<NuxtI18nOptions>
hasLocaleFiles: boolean
langPath: string | null
}
) {
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 ?? '')
) ?? []
nuxtOptions?.i18nModules?.map(module => resolve(nuxt.options._layers[0].config.rootDir, module.langDir ?? '')) ?? []
debug('i18nModulePaths -', i18nModulePaths)
const localePaths = [...langPaths, ...i18nModulePaths]

Expand Down
32 changes: 12 additions & 20 deletions src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const debug = createDebug('@nuxtjs/i18n:gen')

export function generateLoaderOptions(
lazy: NonNullable<NuxtI18nOptions['lazy']>,
langDir: NuxtI18nOptions['langDir'],
localesRelativeBase: string,
vueI18nConfigPathInfo: VueI18nConfigPathInfo,
vueI18nConfigPaths: VueI18nConfigPathInfo[],
Expand Down Expand Up @@ -68,10 +67,7 @@ export function generateLoaderOptions(
const { root, dir, base, ext } = parsePath(relativePath)
const key = makeImportKey(root, dir, base)
if (!generatedImports.has(key)) {
let loadPath = relativePath
if (langDir) {
loadPath = resolveLocaleRelativePath(localesRelativeBase, langDir, relativePath)
}
const loadPath = resolveLocaleRelativePath(localesRelativeBase, relativePath)
const assertFormat = ext.slice(1)
const variableName = genSafeVariableName(`locale_${convertToImportId(key)}`)
gen += `${genImport(
Expand All @@ -98,11 +94,9 @@ export function generateLoaderOptions(
/**
* Prepare locale files for synthetic or asynthetic
*/
if (langDir) {
for (const locale of localeInfo) {
if (!syncLocaleFiles.has(locale) && !asyncLocaleFiles.has(locale)) {
;(lazy ? asyncLocaleFiles : syncLocaleFiles).add(locale)
}
for (const locale of localeInfo) {
if (!syncLocaleFiles.has(locale) && !asyncLocaleFiles.has(locale)) {
;(lazy ? asyncLocaleFiles : syncLocaleFiles).add(locale)
}
}

Expand Down Expand Up @@ -236,7 +230,6 @@ export function generateLoaderOptions(
}).join(`,`)}})\n`
} else if (rootKey === 'localeInfo') {
let codes = `export const localeMessages = {\n`
if (langDir) {
for (const { code, file, files} of syncLocaleFiles) {
const syncPaths = file ? [file] : files|| []
codes += ` ${toCode(code)}: [${syncPaths.map(filepath => {
Expand All @@ -249,16 +242,15 @@ export function generateLoaderOptions(
codes += ` ${toCode(localeInfo.code)}: [${convertToPairs(localeInfo).map(({ file, path, hash, type }) => {
const { root, dir, base, ext } = parsePath(file)
const key = makeImportKey(root, dir, base)
const loadPath = resolveLocaleRelativePath(localesRelativeBase, langDir, file)
const loadPath = resolveLocaleRelativePath(localesRelativeBase, file)
return `{ key: ${toCode(loadPath)}, load: ${genDynamicImport(genImportSpecifier(loadPath, ext, path, type, { hash, query: { locale: localeInfo.code } }), { comment: `webpackChunkName: "lang_${normalizeWithUnderScore(key)}"` })} }`
})}],\n`
}
}
codes += `}\n`
return codes
} else {
return `export const ${rootKey} = ${toCode(rootValue)}\n`
}
} else {
return `export const ${rootKey} = ${toCode(rootValue)}\n`
}
}).join('\n')}`

/**
Expand Down Expand Up @@ -314,15 +306,15 @@ function convertToImportId(file: string) {
return IMPORT_ID_CACHES.get(file)
}

const { name } = parsePath(file)
const id = normalizeWithUnderScore(name)
const { name, dir } = parsePath(file)
const id = normalizeWithUnderScore(`${dir}/${name}`)
IMPORT_ID_CACHES.set(file, id)

return id
}

function resolveLocaleRelativePath(relativeBase: string, langDir: string, file: string) {
return normalize(`${relativeBase}/${langDir}/${file}`)
function resolveLocaleRelativePath(relativeBase: string, file: string) {
return normalize(`${relativeBase}/${file}`)
}

/* eslint-enable @typescript-eslint/no-explicit-any */
166 changes: 77 additions & 89 deletions src/layers.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,79 @@
import createDebug from 'debug'
import { resolve } from 'pathe'
import { getLayerI18n, getProjectPath, mergeConfigLocales, resolveVueI18nConfigInfo } from './utils'
import {
getLayerI18n,
getProjectPath,
mergeConfigLocales,
resolveVueI18nConfigInfo,
LocaleConfig,
formatMessage
} from './utils'

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

import type { Nuxt } from '@nuxt/schema'
import type { LocaleObject } from 'vue-i18n-routing'
import type { NuxtI18nOptions } from './types'

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

export const applyLayerOptions = (options: NuxtI18nOptions, nuxt: Nuxt) => {
export const checkLayerOptions = (options: NuxtI18nOptions, nuxt: Nuxt) => {
const logger = useLogger(NUXT_I18N_MODULE_ID)
const project = nuxt.options._layers[0]
const layers = nuxt.options._layers

// No layers to merge
if (layers.length === 1) return
for (const layer of layers) {
const layerI18n = getLayerI18n(layer)
if (layerI18n == null) continue

const configLocation = project.config.rootDir === layer.config.rootDir ? 'project layer' : 'extended layer'
const layerHint = `In ${configLocation} (\`${resolve(project.config.rootDir, layer.configFile)}\`) -`

try {
// check `lazy` and `langDir` option
if (layerI18n.lazy && !layerI18n.langDir) {
throw new Error('When using the `lazy`option you must also set the `langDir` option.')
}

// check `langDir` option
if (layerI18n.langDir) {
const locales = layerI18n.locales || []
if (!locales.length || locales.some(locale => isString(locale))) {
throw new Error('When using the `langDir` option the `locales` must be a list of objects.')
}

if (isString(layerI18n.langDir) && isAbsolute(layerI18n.langDir)) {
logger.warn(
`${layerHint} \`langdir\` is set to an absolute path (\`${layerI18n.langDir}\`) but should be set a path relative to \`srcDir\` (\`${layer.config.srcDir}\`). ` +
`Absolute paths will not work in production, see https://v8.i18n.nuxtjs.org/options/lazy#langdir for more details.`
)
}

for (const locale of locales) {
if (isString(locale) || !(locale.file || locale.files)) {
throw new Error(
'All locales must have the `file` or `files` property set when using `langDir`.\n' +
`Found none in:\n${JSON.stringify(locale, null, 2)}.`
)
}
}
}
} catch (err) {
if (!(err instanceof Error)) throw err
throw new Error(formatMessage(`${layerHint} ${err.message}`))
}
}
}

export const applyLayerOptions = (options: NuxtI18nOptions, nuxt: Nuxt) => {
const project = nuxt.options._layers[0]
const layers = nuxt.options._layers

const resolvedLayerPaths = layers.map(l => resolve(project.config.rootDir, l.config.rootDir))
debug('using layers at paths', resolvedLayerPaths)

const mergedLocales = mergeLayerLocales(nuxt)
const mergedLocales = mergeLayerLocales(options, nuxt)
debug('merged locales', mergedLocales)

options.locales = mergedLocales
Expand All @@ -38,94 +93,27 @@ export const mergeLayerPages = (analyzer: (pathOverride: string) => void, nuxt:
}
}

export const mergeLayerLocales = (nuxt: Nuxt) => {
const projectLayer = nuxt.options._layers[0]
const projectI18n = getLayerI18n(projectLayer)

if (projectI18n == null) {
debug('project layer `i18n` configuration is required')
return []
}
debug('project layer `lazy` option', projectI18n.lazy)
export const mergeLayerLocales = (options: NuxtI18nOptions, nuxt: Nuxt) => {
debug('project layer `lazy` option', options.lazy)
const projectLangDir = getProjectPath(nuxt, nuxt.options.srcDir)
options.locales ??= []

/**
* Merge locales when `lazy: false`
*/
const mergeSimpleLocales = () => {
if (projectI18n.locales == null) return []

const firstI18nLayer = nuxt.options._layers.find(layer => {
const configs: LocaleConfig[] = nuxt.options._layers
.filter(layer => {
const i18n = getLayerI18n(layer)
return i18n?.locales && i18n?.locales?.length > 0
return i18n?.locales != null
})
if (firstI18nLayer == null) return []

const localeType = typeof getLayerI18n(firstI18nLayer)?.locales?.at(0)
const isStringLocales = (val: unknown): val is string[] => localeType === 'string'

const mergedLocales: string[] | LocaleObject[] = []

/*
Layers need to be reversed to ensure that the original first layer (project)
has the highest priority in merging (because in the reversed array it gets merged last)
*/
const reversedLayers = [...nuxt.options._layers].reverse()
for (const layer of reversedLayers) {
.map(layer => {
const i18n = getLayerI18n(layer)
debug('layer.config.i18n.locales', i18n?.locales)
if (i18n?.locales == null) continue

for (const locale of i18n.locales) {
if (isStringLocales(mergedLocales)) {
if (typeof locale !== 'string') continue
if (mergedLocales.includes(locale)) continue

mergedLocales.push(locale)
continue
}

if (typeof locale === 'string') continue
const localeEntry = mergedLocales.find(x => x.code === locale.code)

if (localeEntry == null) {
mergedLocales.push(locale)
} else {
Object.assign(localeEntry, locale, localeEntry)
}
return {
...i18n,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
langDir: resolve(layer.config.srcDir, i18n?.langDir ?? layer.config.srcDir),
projectLangDir
}
}

return mergedLocales
}

const mergeLazyLocales = () => {
if (projectI18n.langDir == null) {
debug('project layer `i18n.langDir` is required')
return []
}

const projectLangDir = getProjectPath(nuxt, projectI18n.langDir)
debug('project path', getProjectPath(nuxt))

const configs = nuxt.options._layers
.filter(layer => {
const i18n = getLayerI18n(layer)
return i18n?.locales != null && i18n?.langDir != null
})
.map(layer => {
const i18n = getLayerI18n(layer)
return {
...i18n,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
langDir: resolve(layer.config.rootDir, i18n!.langDir!),
projectLangDir
}
})

return mergeConfigLocales(configs)
}
})

return projectI18n.lazy ? mergeLazyLocales() : mergeSimpleLocales()
return mergeConfigLocales(configs)
}

/**
Expand Down
Loading

0 comments on commit 5c46d22

Please sign in to comment.