From fdf54f4bf730151f84e16caabf52e465407dfc4e Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 17 Aug 2023 13:26:11 +0900 Subject: [PATCH] feat: pickup from unplugin-vue-i18n plugin options to nuxt i18n options (#2323) --- docs/content/3.options/6.misc.md | 41 ++++++++++++++++++----------- playground/nuxt.config.ts | 3 +++ playground/pages/index.vue | 1 + src/alias.ts | 15 ++++++++--- src/bundler.ts | 27 +++++++++++++------ src/constants.ts | 8 +++++- src/module.ts | 36 ++++++++++++++++--------- src/nitro.ts | 6 ++++- src/types.ts | 5 +++- test/__snapshots__/gen.test.ts.snap | 6 ++--- 10 files changed, 102 insertions(+), 46 deletions(-) diff --git a/docs/content/3.options/6.misc.md b/docs/content/3.options/6.misc.md index d7df058d9..3f1fec835 100644 --- a/docs/content/3.options/6.misc.md +++ b/docs/content/3.options/6.misc.md @@ -12,9 +12,7 @@ Miscellaneous options. Configure the flag for experimental features of the nuxt i18n module. ::alert{type="info"} - This module option setting is also set to the runtime config. - :: Supported properties: @@ -25,22 +23,28 @@ Supported properties: ## `bundle` - type: `object` -- default: `{ compositionOnly: true }` +- default: `{ compositionOnly: true, runtimeOnly: false, fullInstall: true }` Configure the bundling optimization for nuxt i18n module. Supported properties: -- `compositionOnly` (default: `true`) - Whether to make vue-i18n API only composition API. By default the legacy API is tree-shaken. For more details, See [here](https://vue-i18n.intlify.dev/guide/advanced/optimization.html#reduce-bundle-size-with-feature-build-flags) +- `compositionOnly` (default: `true`) - Whether to make vue-i18n API only composition API. By default the legacy API is tree-shaken. For more details, See [here](https://vue-i18n.intlify.dev/guide/advanced/optimization.html#reduce-bundle-size-with-feature-build-flags) ::alert{type="warning"} - If you would like to use Vue I18n's Legacy API, you must set `compositionOnly: false`. **Note that setting this value will disable Vue I18n Composition API**. Note that the Legacy API can also be used in hybrid by setting the Vue I18n option to `allowComposition: true` in i18n.config, but this is limited. See [here](https://vue-i18n.intlify.dev/guide/migration/vue3.html) for details. + :: +- `runtimeOnly` (default: `false`)a - Whether or not to automatically use Vue I18n runtime-only in build. + + ::alert{type="warning"} + When you will enable this option, vue-i18n message compiler is not bundled. This means that you will not be able to dynamically retrieve locale messages for use in your application from back-end APIs or DB via fetch, or programmatically compose the locale messages. That is to say, **you must be able to fully resolve locale messages at build time.** :: +- `fullInstall` (default: `true`) - Whether to install the full set of APIs, components, etc. By default, all of them will be installed. If `false` is specified, buld-in components (`i18n-t`, `i18n-d` and `i18n-n`) and directive (`v-t`) will not be installed in vue and will be tree-shaken. For more details, See [here](https://vue-i18n.intlify.dev/guide/advanced/optimization.html#reduce-bundle-size-with-feature-build-flags) + ## `compilation` @@ -54,27 +58,38 @@ Supported properties: - `jit` (default: `true`) Whether to use the JIT compilation of Vue I18n message compiler. ::alert{type="warning"} - Mean that necessary to pre-compile locale messages that are not managed by the nuxt i18n module (e.g. in the case of importing from a specific URL, you will need to precompile them yourself.) And also, you need to understand that you cannot support use cases where you dynamically compose locale messages from the back-end via an API. - :: - `strictMessage` (default: `true`) Strictly check that the locale message does not contain HTML tags. If HTML tags are included, an error is thrown. - ::alert{type="warning"} + ::alert{type="warning"} If you do not want the error to be thrown, you can work around it by setting it to false. However, **this means that the locale message might cause security issues with XSS**. In that case, we recommend setting the `escapeHtml` option to `true`. - :: - `escapeHtml` (default: `false`) - Determine whether to escape HTML tags if they are included in the locale message. - ::alert{type="warning"} + ::alert{type="warning"} If `strictMessage` is disabled by setting it to `false`, we recommend enabling this option. - :: + +## `customBlocks` + +- type: `object` +- default: `{ defaultSFCLang: 'json', globalSFCScope: false }` + +Configure the `i18n` custom blocks of SFC. + +Supported properties: + +- `defaultSFCLang` (default: `'json'`) - Specify the content for all your inlined i18n custom blocks on your SFC. about details, see [here](https://github.com/intlify/bundle-tools/blob/main/packages/unplugin-vue-i18n/README.md#defaultsfclang) + +- `globalSFCScope` (default: `false`) - Whether to include all `i18n` custom blocks on your SFC on global scope. about details, see [here](https://github.com/intlify/bundle-tools/blob/main/packages/unplugin-vue-i18n/README.md#globalsfcscope) + + ## `types` - type: `string` (`composition` or `legacy`) @@ -83,9 +98,7 @@ Supported properties: Enforces the type definition of the API style to be used. if you set `compostion`, Composition API types provided by Vue I18n and `@nuxtjs/i18n` are supported, else `legacy`, Options API types are supported. If you are running a dev server with `nuxi dev`, watching the Nuxt configuration will switch the type. ::alert{type="warning"} - If it can not detect Nuxt configuration changing, you need to run `nuxi prepare`. - :: @@ -97,11 +110,9 @@ If it can not detect Nuxt configuration changing, you need to run `nuxi prepare` Whether to use `@nuxtjs/i18n` debug mode. If `true`, logs will be output to the console. ::alert{type="warning"} - The purpose of this option is to help identify any problems with @nuxtjs/i18n. Don't enable this option for use in production. Performance will be decreased. - :: ## `parallelPlugin` diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 2babc6fbb..714c93cf6 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -147,6 +147,9 @@ export default defineNuxtConfig({ name: 'Français' } ], + // bundle: { + // runtimeOnly: true + // }, // trailingSlash: true, debug: true, defaultLocale: 'en', diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 7b1f3f25d..17cb6be24 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -98,6 +98,7 @@ definePageMeta({

{{ item }}

+
v-t directive:
diff --git a/src/alias.ts b/src/alias.ts index 24d414586..047203468 100644 --- a/src/alias.ts +++ b/src/alias.ts @@ -13,15 +13,16 @@ import { tryResolve, getLayerRootDirs, getPackageManagerType } from './utils' import type { Nuxt } from '@nuxt/schema' import type { PackageManager } from './utils' +import type { NuxtI18nOptions } from './types' const debug = createDebug('@nuxtjs/i18n:alias') -export async function setupAlias(nuxt: Nuxt) { +export async function setupAlias(nuxt: Nuxt, options: NuxtI18nOptions) { const pkgMgr = await getPackageManagerType() debug('setupAlias: pkgMgr', pkgMgr) // resolve vue-i18@v9 - nuxt.options.alias[VUE_I18N_PKG] = await resolveVueI18nAlias(pkgModulesDir, nuxt, pkgMgr) + nuxt.options.alias[VUE_I18N_PKG] = await resolveVueI18nAlias(pkgModulesDir, options, nuxt, pkgMgr) nuxt.options.build.transpile.push(VUE_I18N_PKG) debug('vue-i18n alias', nuxt.options.alias[VUE_I18N_PKG]) @@ -62,12 +63,18 @@ export async function setupAlias(nuxt: Nuxt) { * - `@intlify/vue-router-bridge` */ -export async function resolveVueI18nAlias(pkgModulesDir: string, nuxt: Nuxt, pkgMgr: PackageManager) { +export async function resolveVueI18nAlias( + pkgModulesDir: string, + options: NuxtI18nOptions, + nuxt: Nuxt, + pkgMgr: PackageManager +) { const { rootDir, workspaceDir } = nuxt.options + const runtimeOnly = options.bundle?.runtimeOnly const modulePath = nuxt.options.dev || nuxt.options._prepare ? `${VUE_I18N_PKG}/dist/vue-i18n.mjs` - : `${VUE_I18N_PKG}/dist/vue-i18n.runtime.mjs` + : `${VUE_I18N_PKG}/dist/vue-i18n${runtimeOnly ? '.runtime' : ''}.mjs` const targets = [ // for Nuxt layer ...getLayerRootDirs(nuxt).map(root => resolve(root, 'node_modules', modulePath)), diff --git a/src/bundler.ts b/src/bundler.ts index 0fc9605e0..e62dc6fe4 100644 --- a/src/bundler.ts +++ b/src/bundler.ts @@ -47,8 +47,9 @@ export async function extendBundler( const webpack = await import('webpack').then(m => m.default || m) const webpackPluginOptions: PluginOptions = { - runtimeOnly: true, allowDynamic: true, + runtimeOnly: nuxtOptions.bundle.runtimeOnly, + compositionOnly: nuxtOptions.bundle.compositionOnly, jitCompilation: nuxtOptions.compilation.jit, strictMessage: nuxtOptions.compilation.strictMessage, escapeHtml: nuxtOptions.compilation.escapeHtml @@ -66,9 +67,16 @@ export async function extendBundler( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- `config.plugins` is safe, so it's assigned with nuxt! config.plugins!.push( new webpack.DefinePlugin( - assign(getFeatureFlags(nuxtOptions.compilation.jit, nuxtOptions.bundle.compositionOnly), { - __DEBUG__: String(nuxtOptions.debug) - }) + assign( + getFeatureFlags({ + jit: nuxtOptions.compilation.jit, + compositionOnly: nuxtOptions.bundle.compositionOnly, + fullInstall: nuxtOptions.bundle.fullInstall + }), + { + __DEBUG__: String(nuxtOptions.debug) + } + ) ) ) }) @@ -81,12 +89,15 @@ export async function extendBundler( */ const vitePluginOptions: PluginOptions = { - runtimeOnly: true, allowDynamic: true, + runtimeOnly: nuxtOptions.bundle.runtimeOnly, compositionOnly: nuxtOptions.bundle.compositionOnly, + fullInstall: nuxtOptions.bundle.fullInstall, jitCompilation: nuxtOptions.compilation.jit, strictMessage: nuxtOptions.compilation.strictMessage, - escapeHtml: nuxtOptions.compilation.escapeHtml + escapeHtml: nuxtOptions.compilation.escapeHtml, + defaultSFCLang: nuxtOptions.customBlocks.defaultSFCLang, + globalSFCScope: nuxtOptions.customBlocks.globalSFCScope } if (hasLocaleFiles && localePaths.length > 0) { vitePluginOptions.include = localePaths.map(x => resolve(x, './**')) @@ -108,9 +119,9 @@ export async function extendBundler( }) } -export function getFeatureFlags(jit = true, compositionOnly = true) { +export function getFeatureFlags({ jit = true, compositionOnly = true, fullInstall = true }) { return { - __VUE_I18N_FULL_INSTALL__: 'true', + __VUE_I18N_FULL_INSTALL__: String(fullInstall), __VUE_I18N_LEGACY_API__: String(!compositionOnly), __INTLIFY_PROD_DEVTOOLS__: 'false', __INTLIFY_JIT_COMPILATION__: String(jit) diff --git a/src/constants.ts b/src/constants.ts index 5e2e8f770..fe55fdc94 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -36,13 +36,19 @@ export const DEFAULT_OPTIONS = { jsTsFormatResource: false }, bundle: { - compositionOnly: true + compositionOnly: true, + runtimeOnly: false, + fullInstall: true }, compilation: { jit: true, strictMessage: true, escapeHtml: false }, + customBlocks: { + defaultSFCLang: 'json', + globalSFCScope: false + }, vueI18n: '', locales: [] as string[], defaultLocale: '', diff --git a/src/module.ts b/src/module.ts index cd2c3be51..e41e602aa 100644 --- a/src/module.ts +++ b/src/module.ts @@ -100,25 +100,35 @@ export default defineNuxtModule({ if (options.bundle.compositionOnly && options.types === 'legacy') { throw new Error( formatMessage( - `'i18n.bundle.compositionOnly' option and 'i18n.types' option is conflicting: i18n.bundle.compositionOnly: ${ - options.bundle.compositionOnly - }, i18n.types: ${JSON.stringify(options.types)}` + '`bundle.compositionOnly` option and `types` option is conflicting: ' + + `bundle.compositionOnly: ${options.bundle.compositionOnly}, types: ${JSON.stringify(options.types)}` ) ) } - applyLayerOptions(options, nuxt) - await mergeI18nModules(options, nuxt) + if (options.bundle.runtimeOnly && options.compilation.jit) { + logger.warn( + '`bundle.runtimeOnly` option and `compilation.jit` option is conflicting: ' + + `bundle.runtimeOnly: ${options.bundle.runtimeOnly}, compilation.jit: ${JSON.stringify( + options.compilation.jit + )}` + ) + } if (options.strategy === 'no_prefix' && options.differentDomains) { - console.warn( - formatMessage( - 'The `differentDomains` option and `no_prefix` strategy are not compatible. ' + - 'Change strategy or disable `differentDomains` option.' - ) + logger.warn( + '`differentDomains` option and `no_prefix` strategy are not compatible. ' + + 'Change strategy or disable `differentDomains` option.' ) } + /** + * nuxt layers handling ... + */ + + applyLayerOptions(options, nuxt) + await mergeI18nModules(options, nuxt) + /** * setup runtime config */ @@ -161,7 +171,7 @@ export default defineNuxtModule({ const vueI18nConfigPathInfo = await resolveVueI18nConfigInfo(options, nuxt.options.buildDir, nuxt.options.rootDir) if (vueI18nConfigPathInfo.absolute == null) { - logger.warn(`Vue I18n configuration file does not exist at ${vueI18nConfigPathInfo.relative}. Skipping...`) + logger.info(`Vue I18n configuration file does not exist at ${vueI18nConfigPathInfo.relative}. Skipping...`) } debug('vueI18nConfigPathInfo', vueI18nConfigPathInfo) @@ -187,7 +197,7 @@ export default defineNuxtModule({ * setup module alias */ - await setupAlias(nuxt) + await setupAlias(nuxt, options) /** * add plugin and templates @@ -288,7 +298,7 @@ export default defineNuxtModule({ */ const pkgMgr = await getPackageManagerType() - const vueI18nPath = await resolveVueI18nAlias(pkgModulesDir, nuxt, pkgMgr) + const vueI18nPath = await resolveVueI18nAlias(pkgModulesDir, options, nuxt, pkgMgr) debug('vueI18nPath for auto-import', vueI18nPath) await addComponent({ diff --git a/src/nitro.ts b/src/nitro.ts index da001850f..d4f4159fe 100644 --- a/src/nitro.ts +++ b/src/nitro.ts @@ -17,7 +17,11 @@ export async function setupNitro(nuxt: Nuxt, nuxtOptions: Required {} +export interface BundleOptions extends Pick {} + +export interface CustomBlocksOptions extends Pick {} export interface LocaleMessageCompilationOptions { jit?: boolean @@ -75,6 +77,7 @@ export type NuxtI18nOptions = { experimental?: ExperimentalFeatures bundle?: BundleOptions compilation?: LocaleMessageCompilationOptions + customBlocks?: CustomBlocksOptions differentDomains?: boolean detectBrowserLanguage?: DetectBrowserLanguageOptions | false langDir?: string | null diff --git a/test/__snapshots__/gen.test.ts.snap b/test/__snapshots__/gen.test.ts.snap index 79c96e115..8cca63b2b 100644 --- a/test/__snapshots__/gen.test.ts.snap +++ b/test/__snapshots__/gen.test.ts.snap @@ -18,7 +18,7 @@ export const resolveNuxtI18nOptions = async (context) => { return nuxtI18nOptions } -export const nuxtI18nOptionsDefault = Object({experimental: Object({\\"jsTsFormatResource\\":false}),bundle: Object({\\"compositionOnly\\":true}),compilation: Object({\\"jit\\":true,\\"strictMessage\\":true,\\"escapeHtml\\":false}),vueI18n: \\"\\",locales: [],defaultLocale: \\"\\",defaultDirection: \\"ltr\\",routesNameSeparator: \\"___\\",trailingSlash: false,defaultLocaleRouteNameSuffix: \\"default\\",strategy: \\"prefix_except_default\\",lazy: false,langDir: null,rootRedirect: null,detectBrowserLanguage: Object({\\"alwaysRedirect\\":false,\\"cookieCrossOrigin\\":false,\\"cookieDomain\\":null,\\"cookieKey\\":\\"i18n_redirected\\",\\"cookieSecure\\":false,\\"fallbackLocale\\":\\"\\",\\"redirectOn\\":\\"root\\",\\"useCookie\\":true}),differentDomains: false,baseUrl: \\"\\",dynamicRouteParams: false,customRoutes: \\"page\\",pages: Object({}),skipSettingLocaleOnNavigate: false,types: \\"composition\\",debug: false,parallelPlugin: false}) +export const nuxtI18nOptionsDefault = Object({experimental: Object({\\"jsTsFormatResource\\":false}),bundle: Object({\\"compositionOnly\\":true,\\"runtimeOnly\\":false,\\"fullInstall\\":true}),compilation: Object({\\"jit\\":true,\\"strictMessage\\":true,\\"escapeHtml\\":false}),customBlocks: Object({\\"defaultSFCLang\\":\\"json\\",\\"globalSFCScope\\":false}),vueI18n: \\"\\",locales: [],defaultLocale: \\"\\",defaultDirection: \\"ltr\\",routesNameSeparator: \\"___\\",trailingSlash: false,defaultLocaleRouteNameSuffix: \\"default\\",strategy: \\"prefix_except_default\\",lazy: false,langDir: null,rootRedirect: null,detectBrowserLanguage: Object({\\"alwaysRedirect\\":false,\\"cookieCrossOrigin\\":false,\\"cookieDomain\\":null,\\"cookieKey\\":\\"i18n_redirected\\",\\"cookieSecure\\":false,\\"fallbackLocale\\":\\"\\",\\"redirectOn\\":\\"root\\",\\"useCookie\\":true}),differentDomains: false,baseUrl: \\"\\",dynamicRouteParams: false,customRoutes: \\"page\\",pages: Object({}),skipSettingLocaleOnNavigate: false,types: \\"composition\\",debug: false,parallelPlugin: false}) export const nuxtI18nInternalOptions = Object({__normalizedLocales: [Object({\\"code\\":\\"en\\"})]}) export const NUXT_I18N_MODULE_ID = \\"@nuxtjs/i18n\\" @@ -111,7 +111,7 @@ export const resolveNuxtI18nOptions = async (context) => { return nuxtI18nOptions } -export const nuxtI18nOptionsDefault = Object({experimental: Object({\\"jsTsFormatResource\\":false}),bundle: Object({\\"compositionOnly\\":true}),compilation: Object({\\"jit\\":true,\\"strictMessage\\":true,\\"escapeHtml\\":false}),vueI18n: \\"\\",locales: [],defaultLocale: \\"\\",defaultDirection: \\"ltr\\",routesNameSeparator: \\"___\\",trailingSlash: false,defaultLocaleRouteNameSuffix: \\"default\\",strategy: \\"prefix_except_default\\",lazy: false,langDir: null,rootRedirect: null,detectBrowserLanguage: Object({\\"alwaysRedirect\\":false,\\"cookieCrossOrigin\\":false,\\"cookieDomain\\":null,\\"cookieKey\\":\\"i18n_redirected\\",\\"cookieSecure\\":false,\\"fallbackLocale\\":\\"\\",\\"redirectOn\\":\\"root\\",\\"useCookie\\":true}),differentDomains: false,baseUrl: \\"\\",dynamicRouteParams: false,customRoutes: \\"page\\",pages: Object({}),skipSettingLocaleOnNavigate: false,types: \\"composition\\",debug: false,parallelPlugin: false}) +export const nuxtI18nOptionsDefault = Object({experimental: Object({\\"jsTsFormatResource\\":false}),bundle: Object({\\"compositionOnly\\":true,\\"runtimeOnly\\":false,\\"fullInstall\\":true}),compilation: Object({\\"jit\\":true,\\"strictMessage\\":true,\\"escapeHtml\\":false}),customBlocks: Object({\\"defaultSFCLang\\":\\"json\\",\\"globalSFCScope\\":false}),vueI18n: \\"\\",locales: [],defaultLocale: \\"\\",defaultDirection: \\"ltr\\",routesNameSeparator: \\"___\\",trailingSlash: false,defaultLocaleRouteNameSuffix: \\"default\\",strategy: \\"prefix_except_default\\",lazy: false,langDir: null,rootRedirect: null,detectBrowserLanguage: Object({\\"alwaysRedirect\\":false,\\"cookieCrossOrigin\\":false,\\"cookieDomain\\":null,\\"cookieKey\\":\\"i18n_redirected\\",\\"cookieSecure\\":false,\\"fallbackLocale\\":\\"\\",\\"redirectOn\\":\\"root\\",\\"useCookie\\":true}),differentDomains: false,baseUrl: \\"\\",dynamicRouteParams: false,customRoutes: \\"page\\",pages: Object({}),skipSettingLocaleOnNavigate: false,types: \\"composition\\",debug: false,parallelPlugin: false}) export const nuxtI18nInternalOptions = Object({__normalizedLocales: [Object({\\"code\\":\\"en\\"})]}) export const NUXT_I18N_MODULE_ID = \\"@nuxtjs/i18n\\" @@ -136,7 +136,7 @@ export const resolveNuxtI18nOptions = async (context) => { return nuxtI18nOptions } -export const nuxtI18nOptionsDefault = Object({experimental: Object({\\"jsTsFormatResource\\":false}),bundle: Object({\\"compositionOnly\\":true}),compilation: Object({\\"jit\\":true,\\"strictMessage\\":true,\\"escapeHtml\\":false}),vueI18n: \\"\\",locales: [],defaultLocale: \\"\\",defaultDirection: \\"ltr\\",routesNameSeparator: \\"___\\",trailingSlash: false,defaultLocaleRouteNameSuffix: \\"default\\",strategy: \\"prefix_except_default\\",lazy: false,langDir: null,rootRedirect: null,detectBrowserLanguage: Object({\\"alwaysRedirect\\":false,\\"cookieCrossOrigin\\":false,\\"cookieDomain\\":null,\\"cookieKey\\":\\"i18n_redirected\\",\\"cookieSecure\\":false,\\"fallbackLocale\\":\\"\\",\\"redirectOn\\":\\"root\\",\\"useCookie\\":true}),differentDomains: false,baseUrl: \\"\\",dynamicRouteParams: false,customRoutes: \\"page\\",pages: Object({}),skipSettingLocaleOnNavigate: false,types: \\"composition\\",debug: false,parallelPlugin: false}) +export const nuxtI18nOptionsDefault = Object({experimental: Object({\\"jsTsFormatResource\\":false}),bundle: Object({\\"compositionOnly\\":true,\\"runtimeOnly\\":false,\\"fullInstall\\":true}),compilation: Object({\\"jit\\":true,\\"strictMessage\\":true,\\"escapeHtml\\":false}),customBlocks: Object({\\"defaultSFCLang\\":\\"json\\",\\"globalSFCScope\\":false}),vueI18n: \\"\\",locales: [],defaultLocale: \\"\\",defaultDirection: \\"ltr\\",routesNameSeparator: \\"___\\",trailingSlash: false,defaultLocaleRouteNameSuffix: \\"default\\",strategy: \\"prefix_except_default\\",lazy: false,langDir: null,rootRedirect: null,detectBrowserLanguage: Object({\\"alwaysRedirect\\":false,\\"cookieCrossOrigin\\":false,\\"cookieDomain\\":null,\\"cookieKey\\":\\"i18n_redirected\\",\\"cookieSecure\\":false,\\"fallbackLocale\\":\\"\\",\\"redirectOn\\":\\"root\\",\\"useCookie\\":true}),differentDomains: false,baseUrl: \\"\\",dynamicRouteParams: false,customRoutes: \\"page\\",pages: Object({}),skipSettingLocaleOnNavigate: false,types: \\"composition\\",debug: false,parallelPlugin: false}) export const nuxtI18nInternalOptions = Object({__normalizedLocales: [Object({\\"code\\":\\"en\\"})]}) export const NUXT_I18N_MODULE_ID = \\"@nuxtjs/i18n\\"