Skip to content

Commit

Permalink
feat: support server-side i18n integration (#2558)
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Dec 7, 2023
1 parent be4fdff commit 9bdad47
Show file tree
Hide file tree
Showing 47 changed files with 4,021 additions and 4,176 deletions.
91 changes: 91 additions & 0 deletions docs/content/2.guide/16.server-side-translations.md
Original file line number Diff line number Diff line change
@@ -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.
::
File renamed without changes.
11 changes: 9 additions & 2 deletions docs/content/3.options/10.misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
96 changes: 96 additions & 0 deletions docs/content/4.API/1.composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,32 @@ Note that if the value of `detectBrowserLanguage.useCookie` is `false`, an **emp
declare function useCookieLocale(): Ref<string>;
```

## `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<Schema extends Record<string, any> = {}, Event extends H3Event = H3Event>(event: Event): Promise<TranslationFunction<Schema, DefineLocaleMessage>>;
```

## `defineI18nConfig`

The `defineI18nConfig` defines a composable function to vue-i18n configuration.
Expand Down Expand Up @@ -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 `<NuxtLink>`. 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`.
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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": {
Expand All @@ -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",
Expand All @@ -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"
},
Expand Down
19 changes: 19 additions & 0 deletions playground/localeDetector.ts
Original file line number Diff line number Diff line change
@@ -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
})
4 changes: 2 additions & 2 deletions playground/locales/en-GB.js
Original file line number Diff line number Diff line change
@@ -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}`)
})
2 changes: 1 addition & 1 deletion playground/locales/ja.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default async function (locale) {
return $fetch(`/api/${locale}`)
return $fetch(`/api/locales/${locale}`)
}
3 changes: 3 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export default defineNuxtConfig({
// },
// debug: true,
i18n: {
experimental: {
localeDetector: './localeDetector.ts'
},
compilation: {
// jit: false,
strictMessage: false,
Expand Down
1 change: 1 addition & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ definePageMeta({
<nav>
<NuxtLink :to="localePath('/')">Home</NuxtLink> | <NuxtLink :to="localePath({ name: 'about' })">About</NuxtLink> |
<NuxtLink :to="localePath({ name: 'blog' })">Blog</NuxtLink> |
<NuxtLink :to="localePath({ name: 'server' })">Server</NuxtLink> |
<NuxtLink :to="localePath({ name: 'category-id', params: { id: 'foo' } })">Category</NuxtLink> |
<NuxtLinkLocale :to="{ name: 'history' }" class="history-link">History</NuxtLinkLocale> |
<NuxtLinkLocale :to="'/'" locale="ja" activeClass="link-active">Home (Japanese)</NuxtLinkLocale>
Expand Down
15 changes: 15 additions & 0 deletions playground/pages/server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
const { locale } = useI18n()
const { data } = await useFetch(`/api/server?locale=${locale.value}`)
console.log('client locale', locale.value)
console.log('fetch data from server-side', data.value)
</script>

<template>
<div>
<h1>fetch data from server-side</h1>
<div>
<code id="server-data">{{ data }}</code>
</div>
</div>
</template>
File renamed without changes.
6 changes: 6 additions & 0 deletions playground/server/api/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default defineEventHandler(async event => {
const t = await useTranslation(event)
return {
hello: t('hello')
}
})
3 changes: 3 additions & 0 deletions playground/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
4 changes: 1 addition & 3 deletions playground/vue-i18n.options.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import fr from './locales/fr.json'
import { useRuntimeConfig } from '#imports'

export default defineI18nConfig(() => {
// defineNuxtPlugin()
console.log(useRuntimeConfig())
const config = useRuntimeConfig()
return {
legacy: false,
locale: 'en',
Expand Down

0 comments on commit 9bdad47

Please sign in to comment.