diff --git a/docs/content/docs/4.api/0.options.md b/docs/content/docs/4.api/0.options.md index 5a6265515..0556562b3 100644 --- a/docs/content/docs/4.api/0.options.md +++ b/docs/content/docs/4.api/0.options.md @@ -492,6 +492,17 @@ This feature relies on [Nuxt's `experimental.typedRoutes`](https://nuxt.com/docs - default: `false`{lang="ts"} - Generate `vue-i18n` and message types used in translation functions and `vue-i18n` configuration. Can be configured to use the `defaultLocale` (better performance) or all locales for type generation. +### `generatedLocaleFilePathFormat` + +- type: `'absolute' | 'relative'`{lang="ts-type"} + - `'absolute'`{lang="ts-type"} - locale file and langDir paths contain the full absolute path + - `'relative'`{lang="ts-type"} - locale file and langDir paths are converted to be relative to the `rootDir` +- default: `'absolute'`{lang="ts"} +- This changes the generated locale file and langDir paths, these paths are absolute by default in v9 (and lower) and could expose sensitive path information to production. + +::callout{icon="i-heroicons-exclamation-triangle" color="amber"} +Changing this will also change the paths in `locales` returned by `useI18n()`{lang="ts"}. +:: ## customBlocks diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 06cfc071a..dabb186b6 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -41,7 +41,8 @@ export default defineNuxtConfig({ switchLocalePathLinkSSR: true, autoImportTranslationFunctions: true, typedPages: true, - typedOptionsAndMessages: 'default' + typedOptionsAndMessages: 'default', + generatedLocaleFilePathFormat: 'relative' }, compilation: { strictMessage: false, diff --git a/src/constants.ts b/src/constants.ts index 862df771a..ce141cac9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,7 +31,8 @@ export const DEFAULT_OPTIONS = { switchLocalePathLinkSSR: false, autoImportTranslationFunctions: false, typedPages: true, - typedOptionsAndMessages: false + typedOptionsAndMessages: false, + generatedLocaleFilePathFormat: 'absolute' }, bundle: { compositionOnly: true, diff --git a/src/gen.ts b/src/gen.ts index d5202b42a..20742f991 100644 --- a/src/gen.ts +++ b/src/gen.ts @@ -8,7 +8,7 @@ import { getLayerI18n, getLocalePaths, getNormalizedLocales, toCode } from './ut import type { Nuxt } from '@nuxt/schema' import type { PrerenderTarget } from './utils' -import type { NuxtI18nOptions, LocaleInfo, VueI18nConfigPathInfo, FileMeta, LocaleObject } from './types' +import type { NuxtI18nOptions, LocaleInfo, VueI18nConfigPathInfo, FileMeta, LocaleObject, LocaleFile } from './types' import type { Locale } from 'vue-i18n' export type LoaderOptions = { @@ -16,6 +16,7 @@ export type LoaderOptions = { localeInfo: LocaleInfo[] nuxtI18nOptions: NuxtI18nOptions isServer: boolean + normalizedLocales: LocaleObject[] } const debug = createDebug('@nuxtjs/i18n:gen') @@ -31,7 +32,10 @@ const generateVueI18nConfiguration = (config: Required, i ) } -export function simplifyLocaleOptions(nuxt: Nuxt, options: NuxtI18nOptions) { +export function simplifyLocaleOptions( + nuxt: Nuxt, + options: Pick +) { const isLocaleObjectsArray = (locales?: Locale[] | LocaleObject[]) => locales?.some(x => typeof x !== 'string') const hasLocaleObjects = @@ -39,6 +43,7 @@ export function simplifyLocaleOptions(nuxt: Nuxt, options: NuxtI18nOptions) { options?.i18nModules?.some(module => isLocaleObjectsArray(module?.locales)) const locales = (options.locales ?? []) as LocaleObject[] + const pathFormat = options.experimental?.generatedLocaleFilePathFormat ?? 'absolute' return locales.map(({ meta, ...locale }) => { if (!hasLocaleObjects) { @@ -47,6 +52,10 @@ export function simplifyLocaleOptions(nuxt: Nuxt, options: NuxtI18nOptions) { if (locale.file || (locale.files?.length ?? 0) > 0) { locale.files = getLocalePaths(locale) + + if (pathFormat === 'relative') { + locale.files = locale.files.map(x => relative(nuxt.options.rootDir, x)) + } } else { delete locale.files } @@ -58,7 +67,7 @@ export function simplifyLocaleOptions(nuxt: Nuxt, options: NuxtI18nOptions) { export function generateLoaderOptions( nuxt: Nuxt, - { nuxtI18nOptions, vueI18nConfigPaths, localeInfo, isServer }: LoaderOptions + { nuxtI18nOptions, vueI18nConfigPaths, localeInfo, isServer, normalizedLocales }: LoaderOptions ) { debug('generateLoaderOptions: lazy', nuxtI18nOptions.lazy) @@ -100,18 +109,49 @@ export function generateLoaderOptions( .map(config => generateVueI18nConfiguration(config, isServer)) const localeLoaders = localeInfo.map(locale => [locale.code, locale.meta?.map(meta => importMapper.get(meta.key))]) + const pathFormat = nuxtI18nOptions.experimental?.generatedLocaleFilePathFormat ?? 'absolute' const generatedNuxtI18nOptions = { ...nuxtI18nOptions, - locales: simplifyLocaleOptions(nuxt, nuxtI18nOptions) + locales: simplifyLocaleOptions(nuxt, nuxtI18nOptions), + i18nModules: + nuxtI18nOptions.i18nModules?.map(x => { + if (pathFormat === 'absolute') return x + if (x.langDir == null) return x + return { + ...x, + langDir: relative(nuxt.options.rootDir, x.langDir) + } + }) ?? [] } delete nuxtI18nOptions.vueI18n + /** + * Process locale file paths in `normalizedLocales` + */ + const processedNormalizedLocales = normalizedLocales.map(x => { + if (pathFormat === 'absolute') return x + if (x.files == null) return x + + return { + ...x, + files: x.files.map(f => { + if (typeof f === 'string') return relative(nuxt.options.rootDir, f) + + return { + ...f, + path: relative(nuxt.options.rootDir, f.path) + } + }) as string[] | LocaleFile[] + } + }) + const generated = { importStrings, localeLoaders, nuxtI18nOptions: generatedNuxtI18nOptions, - vueI18nConfigs: vueI18nConfigImports + vueI18nConfigs: vueI18nConfigImports, + normalizedLocales: processedNormalizedLocales } debug('generate code', generated) diff --git a/src/prepare/runtime.ts b/src/prepare/runtime.ts index e234cad49..9144533e8 100644 --- a/src/prepare/runtime.ts +++ b/src/prepare/runtime.ts @@ -31,10 +31,10 @@ export function prepareRuntime(ctx: I18nNuxtContext, nuxt: Nuxt) { vueI18nConfigPaths, localeInfo, nuxtI18nOptions, - isServer + isServer, + normalizedLocales }), localeCodes, - normalizedLocales, dev, isSSG, parallelPlugin: options.parallelPlugin diff --git a/src/types.ts b/src/types.ts index ebc7ae955..3a67267ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -117,6 +117,16 @@ export interface ExperimentalFeatures { * @remark `'all'` to generate types based on all locales */ typedOptionsAndMessages?: false | 'default' | 'all' + + /** + * Locale file and langDir paths can be formatted differently to prevent exposing sensitive paths in production. + * + * @defaultValue `'absolute'` + * + * @remark `'absolute'` locale file and langDir paths contain the full absolute path + * @remark `'relative'` locale file and langDir paths are converted to be relative to the `rootDir` + */ + generatedLocaleFilePathFormat?: 'absolute' | 'relative' } export interface BundleOptions diff --git a/src/utils.ts b/src/utils.ts index 95bc963da..76cc3b2f9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -490,7 +490,7 @@ export const getLocaleFiles = (locale: LocaleObject | LocaleInfo): LocaleFile[] return [] } -function resolveRelativeLocales(locale: LocaleObject, config: LocaleConfig) { +export function resolveRelativeLocales(locale: LocaleObject, config: LocaleConfig) { const fileEntries = getLocaleFiles(locale) return fileEntries.map(file => ({ diff --git a/test/__snapshots__/gen.test.ts.snap b/test/__snapshots__/gen.test.ts.snap index 6f75aace6..8f1afaaed 100644 --- a/test/__snapshots__/gen.test.ts.snap +++ b/test/__snapshots__/gen.test.ts.snap @@ -3,9 +3,9 @@ exports[`basic 1`] = ` { "importStrings": [ - "import locale__test_srcDir_en_json from "../srcDir/en.json";", - "import locale__test_srcDir_ja_json from "../srcDir/ja.json";", - "import locale__test_srcDir_fr_json from "../srcDir/fr.json";", + "import locale_srcDir_srcDir_en_json from "srcDir/en.json";", + "import locale_srcDir_srcDir_ja_json from "srcDir/ja.json";", + "import locale_srcDir_srcDir_fr_json from "srcDir/fr.json";", ], "localeLoaders": [ [ @@ -13,8 +13,8 @@ exports[`basic 1`] = ` [ { "cache": "true", - "key": ""../srcDir/en.json"", - "load": "() => Promise.resolve(locale__test_srcDir_en_json)", + "key": ""srcDir/en.json"", + "load": "() => Promise.resolve(locale_srcDir_srcDir_en_json)", }, ], ], @@ -23,8 +23,8 @@ exports[`basic 1`] = ` [ { "cache": "true", - "key": ""../srcDir/ja.json"", - "load": "() => Promise.resolve(locale__test_srcDir_ja_json)", + "key": ""srcDir/ja.json"", + "load": "() => Promise.resolve(locale_srcDir_srcDir_ja_json)", }, ], ], @@ -33,19 +33,175 @@ exports[`basic 1`] = ` [ { "cache": "true", - "key": ""../srcDir/fr.json"", - "load": "() => Promise.resolve(locale__test_srcDir_fr_json)", + "key": ""srcDir/fr.json"", + "load": "() => Promise.resolve(locale_srcDir_srcDir_fr_json)", }, ], ], ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "en.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "ja.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "fr.json", + }, + ], + }, + ], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": false, "locales": [], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", + ], +} +`; + +exports[`files with cache configuration (relative) 1`] = ` +{ + "importStrings": [], + "localeLoaders": [ + [ + "en", + [ + { + "cache": "true", + "key": ""srcDir/test/locales/en.json"", + "load": "() => import("srcDir/test/locales/en.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_en_json" */)", + }, + ], + ], + [ + "ja", + [ + { + "cache": "true", + "key": ""srcDir/test/locales/ja.json"", + "load": "() => import("srcDir/test/locales/ja.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_ja_json" */)", + }, + ], + ], + [ + "fr", + [ + { + "cache": "true", + "key": ""srcDir/test/locales/fr.json"", + "load": "() => import("srcDir/test/locales/fr.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_fr_json" */)", + }, + ], + ], + [ + "es", + [ + { + "cache": "false", + "key": ""srcDir/test/locales/es.json"", + "load": "() => import("srcDir/test/locales/es.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_es_json" */)", + }, + ], + ], + [ + "es-AR", + [ + { + "cache": "false", + "key": ""srcDir/test/locales/es.json"", + "load": "() => import("srcDir/test/locales/es.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_es_json" */)", + }, + { + "cache": "true", + "key": ""srcDir/test/locales/es-AR.json"", + "load": "() => import("srcDir/test/locales/es-AR.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_es_AR_json" */)", + }, + ], + ], + ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "locales/en.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "locales/ja.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "locales/fr.json", + }, + ], + }, + { + "code": "es", + "files": [ + { + "cache": false, + "path": "locales/es.json", + }, + ], + }, + { + "code": "es-AR", + "files": [ + { + "cache": false, + "path": "locales/es.json", + }, + { + "cache": true, + "path": "locales/es-AR.json", + }, + ], + }, + ], + "nuxtI18nOptions": { + "defaultLocale": "en", + "experimental": { + "generatedLocaleFilePathFormat": "relative", + }, + "i18nModules": [], + "lazy": true, + "locales": [], + }, + "vueI18nConfigs": [ + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -59,8 +215,8 @@ exports[`files with cache configuration 1`] = ` [ { "cache": "true", - "key": ""../srcDir/en.json"", - "load": "() => import("../srcDir/en.json" /* webpackChunkName: "locale__test_srcDir_en_json" */)", + "key": ""srcDir/test/locales/en.json"", + "load": "() => import("srcDir/test/locales/en.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_en_json" */)", }, ], ], @@ -69,8 +225,8 @@ exports[`files with cache configuration 1`] = ` [ { "cache": "true", - "key": ""../srcDir/ja.json"", - "load": "() => import("../srcDir/ja.json" /* webpackChunkName: "locale__test_srcDir_ja_json" */)", + "key": ""srcDir/test/locales/ja.json"", + "load": "() => import("srcDir/test/locales/ja.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_ja_json" */)", }, ], ], @@ -79,8 +235,8 @@ exports[`files with cache configuration 1`] = ` [ { "cache": "true", - "key": ""../srcDir/fr.json"", - "load": "() => import("../srcDir/fr.json" /* webpackChunkName: "locale__test_srcDir_fr_json" */)", + "key": ""srcDir/test/locales/fr.json"", + "load": "() => import("srcDir/test/locales/fr.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_fr_json" */)", }, ], ], @@ -89,8 +245,8 @@ exports[`files with cache configuration 1`] = ` [ { "cache": "false", - "key": ""../srcDir/es.json"", - "load": "() => import("../srcDir/es.json" /* webpackChunkName: "locale__test_srcDir_es_json" */)", + "key": ""srcDir/test/locales/es.json"", + "load": "() => import("srcDir/test/locales/es.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_es_json" */)", }, ], ], @@ -99,24 +255,76 @@ exports[`files with cache configuration 1`] = ` [ { "cache": "false", - "key": ""../srcDir/es.json"", - "load": "() => import("../srcDir/es.json" /* webpackChunkName: "locale__test_srcDir_es_json" */)", + "key": ""srcDir/test/locales/es.json"", + "load": "() => import("srcDir/test/locales/es.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_es_json" */)", }, { "cache": "true", - "key": ""../srcDir/es-AR.json"", - "load": "() => import("../srcDir/es-AR.json" /* webpackChunkName: "locale__test_srcDir_es_AR_json" */)", + "key": ""srcDir/test/locales/es-AR.json"", + "load": "() => import("srcDir/test/locales/es-AR.json" /* webpackChunkName: "locale_srcDir_srcDir_test_locales_es_AR_json" */)", }, ], ], ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "/test/locales/en.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "/test/locales/ja.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "/test/locales/fr.json", + }, + ], + }, + { + "code": "es", + "files": [ + { + "cache": false, + "path": "/test/locales/es.json", + }, + ], + }, + { + "code": "es-AR", + "files": [ + { + "cache": false, + "path": "/test/locales/es.json", + }, + { + "cache": true, + "path": "/test/locales/es-AR.json", + }, + ], + }, + ], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": true, "locales": [], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -130,8 +338,8 @@ exports[`lazy 1`] = ` [ { "cache": "true", - "key": ""../srcDir/en.json"", - "load": "() => import("../srcDir/en.json" /* webpackChunkName: "locale__test_srcDir_en_json" */)", + "key": ""srcDir/en.json"", + "load": "() => import("srcDir/en.json" /* webpackChunkName: "locale_srcDir_srcDir_en_json" */)", }, ], ], @@ -140,8 +348,8 @@ exports[`lazy 1`] = ` [ { "cache": "true", - "key": ""../srcDir/ja.json"", - "load": "() => import("../srcDir/ja.json" /* webpackChunkName: "locale__test_srcDir_ja_json" */)", + "key": ""srcDir/ja.json"", + "load": "() => import("srcDir/ja.json" /* webpackChunkName: "locale_srcDir_srcDir_ja_json" */)", }, ], ], @@ -150,19 +358,49 @@ exports[`lazy 1`] = ` [ { "cache": "true", - "key": ""../srcDir/fr.json"", - "load": "() => import("../srcDir/fr.json" /* webpackChunkName: "locale__test_srcDir_fr_json" */)", + "key": ""srcDir/fr.json"", + "load": "() => import("srcDir/fr.json" /* webpackChunkName: "locale_srcDir_srcDir_fr_json" */)", }, ], ], ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "en.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "ja.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "fr.json", + }, + ], + }, + ], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": true, "locales": [], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -176,8 +414,8 @@ exports[`locale file in nested 1`] = ` [ { "cache": "true", - "key": ""../srcDir/en/main.json"", - "load": "() => import("../srcDir/en/main.json" /* webpackChunkName: "locale__test_srcDir_en_main_json" */)", + "key": ""srcDir/en/main.json"", + "load": "() => import("srcDir/en/main.json" /* webpackChunkName: "locale_srcDir_srcDir_en_main_json" */)", }, ], ], @@ -186,8 +424,8 @@ exports[`locale file in nested 1`] = ` [ { "cache": "true", - "key": ""../srcDir/ja/main.json"", - "load": "() => import("../srcDir/ja/main.json" /* webpackChunkName: "locale__test_srcDir_ja_main_json" */)", + "key": ""srcDir/ja/main.json"", + "load": "() => import("srcDir/ja/main.json" /* webpackChunkName: "locale_srcDir_srcDir_ja_main_json" */)", }, ], ], @@ -196,19 +434,49 @@ exports[`locale file in nested 1`] = ` [ { "cache": "true", - "key": ""../srcDir/fr/main.json"", - "load": "() => import("../srcDir/fr/main.json" /* webpackChunkName: "locale__test_srcDir_fr_main_json" */)", + "key": ""srcDir/fr/main.json"", + "load": "() => import("srcDir/fr/main.json" /* webpackChunkName: "locale_srcDir_srcDir_fr_main_json" */)", }, ], ], ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "en/main.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "ja/main.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "fr/main.json", + }, + ], + }, + ], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": true, "locales": [], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -222,8 +490,8 @@ exports[`multiple files 1`] = ` [ { "cache": "true", - "key": ""../srcDir/en.json"", - "load": "() => import("../srcDir/en.json" /* webpackChunkName: "locale__test_srcDir_en_json" */)", + "key": ""srcDir/en.json"", + "load": "() => import("srcDir/en.json" /* webpackChunkName: "locale_srcDir_srcDir_en_json" */)", }, ], ], @@ -232,8 +500,8 @@ exports[`multiple files 1`] = ` [ { "cache": "true", - "key": ""../srcDir/ja.json"", - "load": "() => import("../srcDir/ja.json" /* webpackChunkName: "locale__test_srcDir_ja_json" */)", + "key": ""srcDir/ja.json"", + "load": "() => import("srcDir/ja.json" /* webpackChunkName: "locale_srcDir_srcDir_ja_json" */)", }, ], ], @@ -242,8 +510,8 @@ exports[`multiple files 1`] = ` [ { "cache": "true", - "key": ""../srcDir/fr.json"", - "load": "() => import("../srcDir/fr.json" /* webpackChunkName: "locale__test_srcDir_fr_json" */)", + "key": ""srcDir/fr.json"", + "load": "() => import("srcDir/fr.json" /* webpackChunkName: "locale_srcDir_srcDir_fr_json" */)", }, ], ], @@ -252,8 +520,8 @@ exports[`multiple files 1`] = ` [ { "cache": "true", - "key": ""../srcDir/es.json"", - "load": "() => import("../srcDir/es.json" /* webpackChunkName: "locale__test_srcDir_es_json" */)", + "key": ""srcDir/es.json"", + "load": "() => import("srcDir/es.json" /* webpackChunkName: "locale_srcDir_srcDir_es_json" */)", }, ], ], @@ -262,24 +530,76 @@ exports[`multiple files 1`] = ` [ { "cache": "true", - "key": ""../srcDir/es.json"", - "load": "() => import("../srcDir/es.json" /* webpackChunkName: "locale__test_srcDir_es_json" */)", + "key": ""srcDir/es.json"", + "load": "() => import("srcDir/es.json" /* webpackChunkName: "locale_srcDir_srcDir_es_json" */)", }, { "cache": "true", - "key": ""../srcDir/es-AR.json"", - "load": "() => import("../srcDir/es-AR.json" /* webpackChunkName: "locale__test_srcDir_es_AR_json" */)", + "key": ""srcDir/es-AR.json"", + "load": "() => import("srcDir/es-AR.json" /* webpackChunkName: "locale_srcDir_srcDir_es_AR_json" */)", }, ], ], ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "en.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "ja.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "fr.json", + }, + ], + }, + { + "code": "es", + "files": [ + { + "cache": true, + "path": "es.json", + }, + ], + }, + { + "code": "es-AR", + "files": [ + { + "cache": true, + "path": "es.json", + }, + { + "cache": true, + "path": "es-AR.json", + }, + ], + }, + ], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": true, "locales": [], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -288,8 +608,10 @@ exports[`toCode: function (arrow) 1`] = ` { "importStrings": [], "localeLoaders": [], + "normalizedLocales": [], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": false, "locales": [ { @@ -297,9 +619,6 @@ exports[`toCode: function (arrow) 1`] = ` "files": [ "en.json", ], - "paths": [ - "/path/to/en.json", - ], "testFunc": [Function], }, { @@ -307,9 +626,6 @@ exports[`toCode: function (arrow) 1`] = ` "files": [ "ja.json", ], - "paths": [ - "/path/to/ja.json", - ], "testFunc": [Function], }, { @@ -317,15 +633,12 @@ exports[`toCode: function (arrow) 1`] = ` "files": [ "fr.json", ], - "paths": [ - "/path/to/fr.json", - ], "testFunc": [Function], }, ], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -334,8 +647,10 @@ exports[`toCode: function (named) 1`] = ` { "importStrings": [], "localeLoaders": [], + "normalizedLocales": [], "nuxtI18nOptions": { "defaultLocale": "en", + "i18nModules": [], "lazy": false, "locales": [ { @@ -343,9 +658,6 @@ exports[`toCode: function (named) 1`] = ` "files": [ "en.json", ], - "paths": [ - "/path/to/en.json", - ], "testFunc": [Function], }, { @@ -353,9 +665,6 @@ exports[`toCode: function (named) 1`] = ` "files": [ "ja.json", ], - "paths": [ - "/path/to/ja.json", - ], "testFunc": [Function], }, { @@ -363,15 +672,12 @@ exports[`toCode: function (named) 1`] = ` "files": [ "fr.json", ], - "paths": [ - "/path/to/fr.json", - ], "testFunc": [Function], }, ], }, "vueI18nConfigs": [ - "() => import("../i18n.config.ts?hash=bffaebcb&config=1" /* webpackChunkName: "i18n_config_ts_bffaebcb" */)", + "() => import("/test/i18n.config.ts?hash=6354e9fa&config=1" /* webpackChunkName: "i18n_config_ts_6354e9fa" */)", ], } `; @@ -379,9 +685,9 @@ exports[`toCode: function (named) 1`] = ` exports[`vueI18n option 1`] = ` { "importStrings": [ - "import locale__test_srcDir_en_json from "../srcDir/en.json";", - "import locale__test_srcDir_ja_json from "../srcDir/ja.json";", - "import locale__test_srcDir_fr_json from "../srcDir/fr.json";", + "import locale_srcDir_srcDir_en_json from "srcDir/en.json";", + "import locale_srcDir_srcDir_ja_json from "srcDir/ja.json";", + "import locale_srcDir_srcDir_fr_json from "srcDir/fr.json";", ], "localeLoaders": [ [ @@ -389,8 +695,8 @@ exports[`vueI18n option 1`] = ` [ { "cache": "true", - "key": ""../srcDir/en.json"", - "load": "() => Promise.resolve(locale__test_srcDir_en_json)", + "key": ""srcDir/en.json"", + "load": "() => Promise.resolve(locale_srcDir_srcDir_en_json)", }, ], ], @@ -399,8 +705,8 @@ exports[`vueI18n option 1`] = ` [ { "cache": "true", - "key": ""../srcDir/ja.json"", - "load": "() => Promise.resolve(locale__test_srcDir_ja_json)", + "key": ""srcDir/ja.json"", + "load": "() => Promise.resolve(locale_srcDir_srcDir_ja_json)", }, ], ], @@ -409,21 +715,51 @@ exports[`vueI18n option 1`] = ` [ { "cache": "true", - "key": ""../srcDir/fr.json"", - "load": "() => Promise.resolve(locale__test_srcDir_fr_json)", + "key": ""srcDir/fr.json"", + "load": "() => Promise.resolve(locale_srcDir_srcDir_fr_json)", }, ], ], ], + "normalizedLocales": [ + { + "code": "en", + "files": [ + { + "cache": true, + "path": "en.json", + }, + ], + }, + { + "code": "ja", + "files": [ + { + "cache": true, + "path": "ja.json", + }, + ], + }, + { + "code": "fr", + "files": [ + { + "cache": true, + "path": "fr.json", + }, + ], + }, + ], "nuxtI18nOptions": { + "i18nModules": [], "lazy": false, "locales": [], "vueI18n": "vue-i18n.config.ts", }, "vueI18nConfigs": [ - "() => import("../foo/layer2/vue-i18n.options.js?hash=475488e5&config=1" /* webpackChunkName: "vue_i18n_options_js_475488e5" */)", - "() => import("../layer1/i18n.custom.ts?hash=0ca697e2&config=1" /* webpackChunkName: "i18n_custom_ts_0ca697e2" */)", - "() => import("../to/i18n.config.ts?hash=fb1304e8&config=1" /* webpackChunkName: "i18n_config_ts_fb1304e8" */)", + "() => import("/path/foo/layer2/vue-i18n.options.js?hash=84df1640&config=1" /* webpackChunkName: "vue_i18n_options_js_84df1640" */)", + "() => import("/path/layer1/i18n.custom.ts?hash=faa288dc&config=1" /* webpackChunkName: "i18n_custom_ts_faa288dc" */)", + "() => import("/path/to/i18n.config.ts?hash=42902d1e&config=1" /* webpackChunkName: "i18n_config_ts_42902d1e" */)", ], } `; diff --git a/test/gen.test.ts b/test/gen.test.ts index 66cbe2ee6..926dee17a 100644 --- a/test/gen.test.ts +++ b/test/gen.test.ts @@ -1,15 +1,32 @@ import { generateLoaderOptions } from '../src/gen' -import { resolveLocales, resolveVueI18nConfigInfo } from '../src/utils' +import { getNormalizedLocales, resolveLocales, resolveRelativeLocales, resolveVueI18nConfigInfo } from '../src/utils' import { vi, beforeEach, afterEach, test, expect } from 'vitest' -import type { LocaleInfo, NuxtI18nOptions, VueI18nConfigPathInfo } from '../src/types' +import type { LocaleInfo, LocaleObject, NuxtI18nOptions, VueI18nConfigPathInfo } from '../src/types' import type { Nuxt } from '@nuxt/schema' vi.mock('node:fs') vi.mock('pathe', async () => { const mod = await vi.importActual('pathe') - return { ...mod, resolve: vi.fn((...args: string[]) => mod.normalize(args.join('/'))) } + return { + ...mod, + resolve: vi.fn((...args: string[]) => mod.normalize(args.join('/'))), + relative: vi.fn((...args: string[]) => args[1].replace(args[0] + '/', '')) + } +}) + +vi.mock('@nuxt/kit', async () => { + const mod = await vi.importActual('@nuxt/kit') + return { + ...mod, + useNuxt: vi.fn(() => ({ + options: { + rootDir: '/test', + srcDir: '/test/srcDir' + } + })) + } }) beforeEach(async () => { @@ -20,23 +37,23 @@ afterEach(() => { vi.clearAllMocks() }) -const LOCALE_INFO = [ - { - code: 'en', - files: [{ path: 'en.json', cache: true }], - paths: ['/path/to/en.json'] - }, - { - code: 'ja', - files: [{ path: 'ja.json', cache: true }], - paths: ['/path/to/ja.json'] - }, - { - code: 'fr', - files: [{ path: 'fr.json', cache: true }], - paths: ['/path/to/fr.json'] - } -] +function getMockLocales(additionalLocales?: LocaleObject[]) { + return [ + { + code: 'en', + files: [{ path: 'en.json', cache: true }] + }, + { + code: 'ja', + files: [{ path: 'ja.json', cache: true }] + }, + { + code: 'fr', + files: [{ path: 'fr.json', cache: true }] + }, + ...(additionalLocales ? additionalLocales : []) + ] +} const NUXT_I18N_OPTIONS = { defaultLocale: 'en' @@ -52,6 +69,8 @@ const NUXT_I18N_VUE_I18N_CONFIG = { const makeNuxtOptions = (localeInfo: LocaleInfo[]) => { return { options: { + rootDir: '/test', + buildDir: '.nuxt', _layers: [ { config: { @@ -67,11 +86,13 @@ const makeNuxtOptions = (localeInfo: LocaleInfo[]) => { test('basic', async () => { const { generateLoaderOptions } = await import('../src/gen') - const localeInfo = await resolveLocales('/test/srcDir', LOCALE_INFO, '/test/.nuxt') - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + const locales = getMockLocales() + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), localeInfo, + normalizedLocales: getNormalizedLocales(locales), nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: false }, isServer: false }) @@ -80,11 +101,13 @@ test('basic', async () => { }) test('lazy', async () => { - const localeInfo = await resolveLocales('/test/srcDir', LOCALE_INFO, '/test/.nuxt') - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + const locales = getMockLocales() + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), localeInfo, + normalizedLocales: getNormalizedLocales(locales), nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: true }, isServer: false }) @@ -93,34 +116,30 @@ test('lazy', async () => { }) test('multiple files', async () => { - const localeInfo = await resolveLocales( - '/test/srcDir', - [ - ...LOCALE_INFO, - ...[ - { - code: 'es', - files: [{ path: 'es.json', cache: true }], - paths: ['/path/to/es.json'] - }, - { - code: 'es-AR', - files: [ - { path: 'es.json', cache: true }, - { path: 'es-AR.json', cache: true } - ], - paths: ['/path/to/es.json', '/path/to/es-AR.json'] - } - ] - ], - '/test/.nuxt' - ) - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + const locales = [ + ...getMockLocales([ + { + code: 'es', + files: [{ path: 'es.json', cache: true }] + }, + { + code: 'es-AR', + files: [ + { path: 'es.json', cache: true }, + { path: 'es-AR.json', cache: true } + ] + } + ]) + ] + + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), localeInfo, nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: true }, + normalizedLocales: getNormalizedLocales(locales), isServer: false }) @@ -128,33 +147,33 @@ test('multiple files', async () => { }) test('files with cache configuration', async () => { - const localeInfo = await resolveLocales( - '/test/srcDir', - [ - ...LOCALE_INFO, - ...[ - { - code: 'es', - files: [{ path: 'es.json', cache: false }], - paths: ['/path/to/es.json'] - }, - { - code: 'es-AR', - files: [ - { path: 'es.json', cache: false }, - { path: 'es-AR.json', cache: true } - ], - paths: ['/path/to/es.json', '/path/to/es-AR.json'] - } + const locales = getMockLocales([ + { + code: 'es', + files: [{ path: 'es.json', cache: false }] + }, + { + code: 'es-AR', + files: [ + { path: 'es.json', cache: false }, + { path: 'es-AR.json', cache: true } ] - ], - '/test/.nuxt' - ) - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + } + ]) + + for (const l of locales) { + // @ts-ignore + l.files = resolveRelativeLocales(l, { langDir: 'locales' }) + } + + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + console.log(getNormalizedLocales(locales)) const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), localeInfo, + normalizedLocales: getNormalizedLocales(locales), nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: true }, isServer: false }) @@ -162,42 +181,86 @@ test('files with cache configuration', async () => { expect(code).toMatchSnapshot() }) -test('locale file in nested', async () => { - const localeInfo = await resolveLocales( - '/test/srcDir', - [ - { - code: 'en', - files: [{ path: 'en/main.json', cache: true }], - paths: ['/path/to/en.json'] - }, - { - code: 'ja', - files: [{ path: 'ja/main.json', cache: true }], - paths: ['/path/to/ja.json'] +test('files with cache configuration (relative)', async () => { + const locales = getMockLocales([ + { + code: 'es', + files: [{ path: 'es.json', cache: false }] + }, + { + code: 'es-AR', + files: [ + { path: 'es.json', cache: false }, + { path: 'es-AR.json', cache: true } + ] + } + ]) + + for (const l of locales) { + // @ts-ignore + l.files = resolveRelativeLocales(l, { langDir: 'locales' }) + } + // console.log(JSON.stringify(getMockLocales(), null, 2)) + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + + console.log(getNormalizedLocales(locales)) + + const code = generateLoaderOptions( + { ...makeNuxtOptions(localeInfo), options: { ...makeNuxtOptions(localeInfo).options, rootDir: '/test' } }, + { + vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), + localeInfo, + normalizedLocales: getNormalizedLocales(locales), + nuxtI18nOptions: { + ...NUXT_I18N_OPTIONS, + lazy: true, + experimental: { + generatedLocaleFilePathFormat: 'relative' + } }, - { - code: 'fr', - files: [{ path: 'fr/main.json', cache: true }], - paths: ['/path/to/fr.json'] - } - ], - '/test/.nuxt' + isServer: false + } ) - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') - const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { - vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), - localeInfo, - nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: true }, - isServer: false - }) + expect(code).toMatchSnapshot() +}) + +test('locale file in nested', async () => { + const locales = [ + { + code: 'en', + files: [{ path: 'en/main.json', cache: true }] + }, + { + code: 'ja', + files: [{ path: 'ja/main.json', cache: true }] + }, + { + code: 'fr', + files: [{ path: 'fr/main.json', cache: true }] + } + ] + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') + + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + const code = generateLoaderOptions( + { ...makeNuxtOptions(localeInfo), options: { ...makeNuxtOptions(localeInfo).options, rootDir: '/test' } }, + { + vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), + localeInfo, + normalizedLocales: getNormalizedLocales(locales), + nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: true }, + isServer: false + } + ) expect(code).toMatchSnapshot() }) test('vueI18n option', async () => { - const localeInfo = await resolveLocales('/test/srcDir', LOCALE_INFO, '/test/.nuxt') + const locales = getMockLocales() + const localeInfo = await resolveLocales('srcDir', locales, '.nuxt') const vueI18nConfigs = await Promise.all( [ NUXT_I18N_VUE_I18N_CONFIG, @@ -213,11 +276,12 @@ test('vueI18n option', async () => { rootDir: '/path/foo/layer2', relativeBase: '../../..' } - ].map(x => resolveVueI18nConfigInfo(x.rootDir, x.relative, '/path/.nuxt')) + ].map(x => resolveVueI18nConfigInfo(x.rootDir, x.relative, '.nuxt')) ) const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { vueI18nConfigPaths: vueI18nConfigs as Required[], localeInfo, + normalizedLocales: getNormalizedLocales(locales), nuxtI18nOptions: { vueI18n: 'vue-i18n.config.ts', lazy: false @@ -229,16 +293,17 @@ test('vueI18n option', async () => { }) test('toCode: function (arrow)', async () => { - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') - const localeInfo = LOCALE_INFO.map(locale => ({ + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + const localeInfo = getMockLocales().map(locale => ({ ...locale, testFunc: (prop: string) => { return `Hello ${prop}` } })) - const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { + const code = generateLoaderOptions(makeNuxtOptions(localeInfo as LocaleInfo[]), { vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), localeInfo: [], + normalizedLocales: [], nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: false, @@ -251,16 +316,17 @@ test('toCode: function (arrow)', async () => { }) test('toCode: function (named)', async () => { - const vueI18nConfig = await resolveVueI18nConfigInfo('.', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') - const localeInfo = LOCALE_INFO.map(locale => ({ + const vueI18nConfig = await resolveVueI18nConfigInfo('/test', NUXT_I18N_VUE_I18N_CONFIG.relative, '.nuxt') + const localeInfo = getMockLocales().map(locale => ({ ...locale, testFunc(prop: string) { return `Hello ${prop}` } })) - const code = generateLoaderOptions(makeNuxtOptions(localeInfo), { + const code = generateLoaderOptions(makeNuxtOptions(localeInfo as LocaleInfo[]), { vueI18nConfigPaths: [vueI18nConfig].filter((x): x is Required => x != null), localeInfo: [], + normalizedLocales: [], nuxtI18nOptions: { ...NUXT_I18N_OPTIONS, lazy: false,