Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: project relative layer locale resolution #2290

Merged
merged 6 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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