From 9bdad474324d8950e981960d80f26082f9f63b7a Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 7 Dec 2023 09:06:07 +0900 Subject: [PATCH] feat: support server-side i18n integration (#2558) --- .../2.guide/16.server-side-translations.md | 91 + .../{16.migrating.md => 19.migrating.md} | 0 docs/content/3.options/10.misc.md | 11 +- docs/content/4.API/1.composables.md | 96 + package.json | 15 +- playground/localeDetector.ts | 19 + playground/locales/en-GB.js | 4 +- playground/locales/ja.mjs | 2 +- playground/nuxt.config.ts | 3 + playground/pages/index.vue | 1 + playground/pages/server.vue | 15 + .../server/api/{ => locales}/[locale].ts | 0 playground/server/api/server.ts | 6 + playground/server/tsconfig.json | 3 + playground/vue-i18n.options.ts | 4 +- pnpm-lock.yaml | 6831 ++++++++--------- specs/basic_usage.spec.ts | 25 +- specs/fixtures/basic_usage/nuxt.config.ts | 2 + specs/fixtures/basic_usage/package.json | 3 + .../layers/layer-server/localeDetector.ts | 19 + .../layers/layer-server/locales/en.json | 3 + .../layers/layer-server/locales/ja.json | 3 + .../layers/layer-server/nuxt.config.ts | 23 + .../layers/layer-server/server/api/server.ts | 8 + .../layers/layer-server/server/tsconfig.json | 3 + src/alias.ts | 5 + src/constants.ts | 9 +- src/gen.ts | 57 +- src/module.ts | 30 +- src/nitro.ts | 127 +- src/options.d.ts | 4 +- .../{composables.ts => composables/index.ts} | 59 +- src/runtime/composables/server.ts | 46 + src/runtime/composables/shared.ts | 55 + src/runtime/internal.ts | 63 +- src/runtime/messages.ts | 147 + src/runtime/plugins/i18n.ts | 28 +- src/runtime/server/plugin.ts | 66 + src/runtime/templates/options.template.mjs | 25 - src/runtime/utils.ts | 64 +- src/template.ts | 51 + src/types.ts | 8 +- src/utils.ts | 10 +- test/__snapshots__/gen.test.ts.snap | 116 +- test/gen.test.ts | 24 +- tsconfig.json | 5 +- vitest.config.ts | 8 +- 47 files changed, 4021 insertions(+), 4176 deletions(-) create mode 100644 docs/content/2.guide/16.server-side-translations.md rename docs/content/2.guide/{16.migrating.md => 19.migrating.md} (100%) create mode 100644 playground/localeDetector.ts create mode 100644 playground/pages/server.vue rename playground/server/api/{ => locales}/[locale].ts (100%) create mode 100644 playground/server/api/server.ts create mode 100644 playground/server/tsconfig.json create mode 100644 specs/fixtures/layers/layer-server/localeDetector.ts create mode 100644 specs/fixtures/layers/layer-server/locales/en.json create mode 100644 specs/fixtures/layers/layer-server/locales/ja.json create mode 100644 specs/fixtures/layers/layer-server/nuxt.config.ts create mode 100644 specs/fixtures/layers/layer-server/server/api/server.ts create mode 100644 specs/fixtures/layers/layer-server/server/tsconfig.json rename src/runtime/{composables.ts => composables/index.ts} (82%) create mode 100644 src/runtime/composables/server.ts create mode 100644 src/runtime/composables/shared.ts create mode 100644 src/runtime/messages.ts create mode 100644 src/runtime/server/plugin.ts delete mode 100644 src/runtime/templates/options.template.mjs create mode 100644 src/template.ts diff --git a/docs/content/2.guide/16.server-side-translations.md b/docs/content/2.guide/16.server-side-translations.md new file mode 100644 index 000000000..7d47d0e0c --- /dev/null +++ b/docs/content/2.guide/16.server-side-translations.md @@ -0,0 +1,91 @@ +# Server-side Translations + +The locale messages defined in nuxt i18n module options are integrated, so all you need to do is configure the locale detector. + +You can do the translation on the server side and return it as a response. + +--- + +::alert{type="warning"} +**This feature is experimental,** that is supported from v8 RC8. +:: + +## Define locale detector + +For server-side translation, you need to define locale detector. + +Nuxt i18n export the `defineI18nLocaleDetector` composable function to define it. +You can use it to define the locale detector. + +The following is an example of defining a detector that detects locale using query, cookie, and header: +```ts {}[localeDetector.ts] +// Detect based on query, cookie, header +export default defineI18nLocaleDetector((event, config) => { + // try to get locale from query + const query = tryQueryLocale(event, { lang: '' }) // disable locale default value with `lang` option + if (query) { + return query.toString() + } + + // try to get locale from cookie + const cookie = tryCookieLocale(event, { lang: '', name: 'i18n_locale' }) // disable locale default value with `lang` option + if (cookie) { + return cookie.toString() + } + + // try to get locale from header (`accept-header`) + const header = tryHeaderLocale(event, { lang: '' }) // disable locale default value with `lang` option + if (header) { + return header.toString() + } + + // If the locale cannot be resolved up to this point, it is resolved with the value `defaultLocale` of the locale config passed to the function + return config.defaultLocale +}) +``` + +The locale detector function is used to detect the locale on server-side. It's called per request on the server. + +when you will define the locale detector, you need to pass the path to the locale detector to `experimental.localeDetector` option. + +The following is an example of locale detector configuration defined directly under Nuxt application: + +```ts {}[nuxt.config.ts] +export default defineNuxtConfig({ + // ... + + i18n: { + experimental: { + localeDetector: './localeDetector.ts' + }, + // ... + }, + + // ... +}) +``` + +For details on the locale detector function defined by `defineI18nLocaleDetector`, see [here](../api/composables#definei18nlocaledetector). + + +## `useTranslation` on eventHandler + +To translate on the server side, you need to call `useTranslation`. + +Example: +```ts +// you need to define `async` event handler +export default defineEventHandler(async event => { + // call `useTranslation`, so it retrun the translation function + const t = await useTranslation(event) + return { + // call translation function with key of locale messages, + // and translation function has some overload + hello: t('hello') + } +}) +``` + +::alert{type="info"} +For the key of translation function, you can specify the locale messages set in nuxt i18n options in nuxt.config, or the locale loaded in i18n.config messages loaded in i18n.config. +:: diff --git a/docs/content/2.guide/16.migrating.md b/docs/content/2.guide/19.migrating.md similarity index 100% rename from docs/content/2.guide/16.migrating.md rename to docs/content/2.guide/19.migrating.md diff --git a/docs/content/3.options/10.misc.md b/docs/content/3.options/10.misc.md index a73c8be60..bd80eb853 100644 --- a/docs/content/3.options/10.misc.md +++ b/docs/content/3.options/10.misc.md @@ -7,9 +7,16 @@ Miscellaneous options. ## `experimental` - type: `object` -- default: `{}` +- default: `{ localeDetector: '' }` + +Supported properties: + +- `localeDetector` (default: `''`) - Specify the locale detector to be called per request on the server side. You need to specify the filepath where the locale detector is defined. + +::alert{type="warning"} +About how to define the locale detector, see the [`defineI18nLocaleDetector` API](../api/composables#definei18nlocaledetector) +:: -Currently no experimental options are available. ## `customBlocks` - type: `object` diff --git a/docs/content/4.API/1.composables.md b/docs/content/4.API/1.composables.md index 9f8dfb1c8..12f85d3c0 100644 --- a/docs/content/4.API/1.composables.md +++ b/docs/content/4.API/1.composables.md @@ -177,6 +177,32 @@ Note that if the value of `detectBrowserLanguage.useCookie` is `false`, an **emp declare function useCookieLocale(): Ref; ``` +## `useTranslation` + +The `useTranslation` composable returns the translation function. + +::alert{type="warning"} +**This composable is experimental and server-side only.** +:: + +Example: +```ts +export default defineEventHandler(async event => { + const t = await useTranslation(event) + return { + hello: t('hello') + } +}) +``` + +The locale is use by the translation function is the locale detected by the function defined in [`experimental.localeDetector` option](../options/misc#experimental). + +### Type + +```ts +declare function useTranslation = {}, Event extends H3Event = H3Event>(event: Event): Promise>; +``` + ## `defineI18nConfig` The `defineI18nConfig` defines a composable function to vue-i18n configuration. @@ -253,3 +279,73 @@ A function that is the dynamic locale messages loading, that has the following p - when you switch the locale with `setLocale`. - when the locale is switched with ``. for example, the route path resolved by `useSwitchLocalePath` or `$switchLocalePath`. + + +## `defineI18nLocaleDetector` + +The `defineI18nLocaleDetector` defines a composable function to detect the locale on the server-side. + +The locale detector fucntion is used to detect the locale on server-side. It's called per request on the server. + +You need return locale string. + +::alert{type="warning"} +**This composable is experimental.** You need to configure filepath to [`experimental.localeDetector` option](../options/misc#experimental). +:: + +### Type + +```ts +type LocaleConfig = { + defaultLocale: Locale + fallbackLocale: FallbackLocale +} +declare function defineI18nLocaleDetector(detector: (event: H3Event, config: LocaleConfig) => string): (event: H3Event, config: LocaleConfig) => string; +``` + +An example of a locale detector: +```ts +// Detect based on query, cookie, header +export default defineI18nLocaleDetector((event, config) => { + const query = tryQueryLocale(event, { lang: '' }) + if (query) { + return query.toString() + } + + const cookie = tryCookieLocale(event, { lang: '', name: 'i18n_locale' }) + if (cookie) { + return cookie.toString() + } + + const header = tryHeaderLocale(event, { lang: '' }) + if (header) { + return header.toString() + } + + return config.defaultLocale +}) +``` + +In the locale detector function, you can use [`@intlify/h3` utilites](https://github.com/intlify/h3#%EF%B8%8F-utilites--helpers), these are auto imported. + +### Parameters + +#### `detector` + +A function that is the locale detector, that has the following parameters: + +- `event` + + **Type**: `H3Event` + + An H3 event. see details [H3 API docs](https://www.jsdocs.io/package/h3#H3Event) + +- `config` + + **Type**: `{ defaultLocale: Locale; fallbackLocale: FallbackLocale; }` + + A locale config that is passed from Nitro. That has the below values: + + - `defaultLocale`: This value is set to the `defaultLocale` option of Nuxt i18n. If unset, it is set to the `locale` option loaded from the Vue I18n configuration (`i18n.config` file set on the `vueI18n` option). If neither of these are set, the default value of `'en-US'` is used. + + - `fallbackLocale`: This value is set to the `fallbackLocale` option loaded from the Vue I18n configuration (`i18n.config` file set on the `vueI18n` option). If no fallback locale has been configured this will default to `false`. diff --git a/package.json b/package.json index ae9656e21..273c331f4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "./package.json": "./package.json" }, "imports": { - "#i18n": "./dist/runtime/composables.mjs" + "#i18n": "./dist/runtime/composables/index.mjs" }, "main": "./dist/module.cjs", "module": "./dist/module.mjs", @@ -71,12 +71,14 @@ "pnpm": { "overrides": { "@nuxtjs/i18n": "link:.", - "nuxt": "^3.7.0", + "nuxt": "^3.8.0", "consola": "^3" } }, "dependencies": { - "@intlify/shared": "^9.7.0", + "@intlify/h3": "^0.5.0", + "@intlify/utils": "^0.12.0", + "@intlify/shared": "^9.8.0", "@intlify/unplugin-vue-i18n": "^1.4.0", "@nuxt/kit": "^3.7.4", "@vue/compiler-sfc": "^3.3.4", @@ -86,11 +88,12 @@ "is-https": "^4.0.0", "knitwork": "^1.0.0", "magic-string": "^0.30.4", + "mlly": "^1.4.2", "pathe": "^1.1.1", "sucrase": "^3.34.0", "ufo": "^1.3.1", "unplugin": "^1.5.0", - "vue-i18n": "^9.7.1", + "vue-i18n": "^9.8.0", "vue-i18n-routing": "^1.2.0" }, "devDependencies": { @@ -112,9 +115,11 @@ "get-port-please": "^3.1.1", "gh-changelogen": "^0.2.8", "globby": "^14.0.0", + "h3": "^1.8.2", "jiti": "^1.20.0", "jsdom": "^23.0.1", "lint-staged": "^15.0.2", + "nitropack": "^2.8.0", "npm-run-all": "^4.1.5", "nuxt": "^3.7.4", "ofetch": "^1.3.3", @@ -124,7 +129,7 @@ "typescript": "^5.2.2", "unbuild": "^2.0.0", "undici": "^5.27.2", - "vitest": "^0.34.6", + "vitest": "^1.0.0", "vue": "^3.3.4", "vue-router": "^4.2.5" }, diff --git a/playground/localeDetector.ts b/playground/localeDetector.ts new file mode 100644 index 000000000..241625541 --- /dev/null +++ b/playground/localeDetector.ts @@ -0,0 +1,19 @@ +// Detect based on query, cookie, header +export default defineI18nLocaleDetector((event, config) => { + const query = tryQueryLocale(event, { lang: '' }) + if (query) { + return query.toString() + } + + const cookie = tryCookieLocale(event, { lang: '', name: 'i18n_locale' }) + if (cookie) { + return cookie.toString() + } + + const header = tryHeaderLocale(event, { lang: '' }) + if (header) { + return header.toString() + } + + return config.defaultLocale +}) diff --git a/playground/locales/en-GB.js b/playground/locales/en-GB.js index 8665fd884..279c60311 100644 --- a/playground/locales/en-GB.js +++ b/playground/locales/en-GB.js @@ -1,4 +1,4 @@ export default defineI18nLocale(async function (locale) { - console.log('Loading locale', locale) - return $fetch(`/api/${locale}`) + console.log('Loading locale ...', locale) + return $fetch(`/api/locales/${locale}`) }) diff --git a/playground/locales/ja.mjs b/playground/locales/ja.mjs index 086c57dda..8e2f01754 100644 --- a/playground/locales/ja.mjs +++ b/playground/locales/ja.mjs @@ -1,3 +1,3 @@ export default async function (locale) { - return $fetch(`/api/${locale}`) + return $fetch(`/api/locales/${locale}`) } diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 894c15b11..caeab8af7 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -107,6 +107,9 @@ export default defineNuxtConfig({ // }, // debug: true, i18n: { + experimental: { + localeDetector: './localeDetector.ts' + }, compilation: { // jit: false, strictMessage: false, diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 9e7120661..accf9d39a 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -76,6 +76,7 @@ definePageMeta({