@intlify/h3
Version:
Internationalization middleware & utilities for h3
354 lines (259 loc) โข 10.4 kB
Markdown
# /h3
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![CI][ci-src]][ci-href]
Internationalization middleware & utilities for h3 (and therefore also for Nitro, which is using h3)
## ๐ Features
โ
๏ธ **Translation:** Simple API like
[vue-i18n](https://vue-i18n.intlify.dev/)
โ
**Custom locale detector:** You can implement your own locale detector
on server-side
โ
๏ธ๏ธ **Useful utilities:** support internationalization composables
utilities via [/utils](https://github.com/intlify/utils)
## ๐ฟ Installation
```sh
# Using npm
npm install /h3
# Using yarn
yarn add /h3
# Using pnpm
pnpm add /h3
# Using bun
bun add /h3
```
## ๐ Usage (h3)
```ts
import { createServer } from 'node:http'
import { createApp, createRouter, eventHandler, toNodeListener } from 'h3'
import {
defineI18nMiddleware,
detectLocaleFromAcceptLanguageHeader,
useTranslation
} from '/h3'
// define middleware with vue-i18n like options
const middleware = defineI18nMiddleware({
// detect locale with `accept-language` header
locale: detectLocaleFromAcceptLanguageHeader,
// resource messages
messages: {
en: {
hello: 'Hello {name}!'
},
ja: {
hello: 'ใใใซใกใฏใ{name}๏ผ'
}
}
// something options
// ...
})
// install middleware with `createApp` option
const app = createApp({ ...middleware })
const router = createRouter()
router.get(
'/',
eventHandler(async event => {
// use `useTranslation` in event handler
const t = await useTranslation(event)
return t('hello', { name: 'h3' })
})
)
app.use(router)
createServer(toNodeListener(app)).listen(3000)
```
## ๐ Usage (Nitro)
For usage with [Nitro](https://nitro.build/) you need to create a plugin instead, create file `plugins/i18n.ts`:
```ts
import { defineNitroPlugin } from 'nitropack/runtime'
import { defineI18nMiddleware, detectLocaleFromAcceptLanguageHeader } from '/h3'
export default defineNitroPlugin(nitroApp => {
const { onRequest, onAfterResponse } = defineI18nMiddleware({
// detect locale with `accept-language` header
locale: detectLocaleFromAcceptLanguageHeader,
// resource messages
messages: {
en: {
hello: 'Hello {name}!'
},
ja: {
hello: 'ใใใซใกใฏใ{name}๏ผ'
}
}
})
nitroApp.hooks.hook('request', onRequest)
nitroApp.hooks.hook('afterResponse', onAfterResponse)
})
```
## ๐ ๏ธ Custom locale detection
You can detect locale with your custom logic from current `H3Event`.
example for detecting locale from url query:
```ts
import { defineI18nMiddleware, getQueryLocale } from '/h3'
import type { H3Event } from 'h3'
// define custom locale detector
const localeDetector = (event: H3Event): string => {
return getQueryLocale(event).toString()
}
const middleware = defineI18nMiddleware({
// set your custom locale detector
locale: localeDetector
// something options
// ...
})
```
You can make that function asynchronous. This is useful when loading resources along with locale detection.
<!-- eslint-disable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
> [!NOTE]
> The case which a synchronous function returns a promise is not supported. you need to use `async function`.
<!-- eslint-enable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
```ts
import { defineI18nMiddleware } from '/h3'
import type { DefineLocaleMessage } from '/h3'
import type { H3Event } from 'h3'
const loader = (path: string) => import(path).then(m => m.default || m)
const messages: Record<string, () => ReturnType<typeof loader>> = {
en: () => loader('./locales/en.json'),
ja: () => loader('./locales/ja.json')
}
// define custom locale detector and lazy loading
const localeDetector = async (
event: H3Event,
i18n: CoreContext<string, DefineLocaleMessage>
): Promise<string> => {
// detect locale
const locale = getCookieLocale(event).toString()
// resource lazy loading
const loader = messages[locale]
if (loader && !i18n.messages[locale]) {
const message = await loader()
i18n.messages[locale] = message
}
return locale
}
const middleware = defineI18nMiddleware({
// set your custom locale detector
locale: localeDetector
// something options
// ...
})
```
## ๐งฉ Type-safe resources
<!-- eslint-disable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
> [!WARNING]
> **This is experimental feature (inspired from [vue-i18n](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#typescript-support)).**
> We would like to get feedback from you ๐.
> [!NOTE]
> The exeample code is [here](https://github.com/intlify/h3/tree/main/playground/typesafe-schema)
<!-- eslint-enable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
You can support the type-safe resources with schema using TypeScript on `defineI18nMiddleware` options.
Locale messages resource:
```ts
export default {
hello: 'hello, {name}!'
}
```
your application code:
```ts
import { defineI18nMiddleware } from '/h3'
import { createApp } from 'h3'
import en from './locales/en.ts'
// define resource schema, as 'en' is master resource schema
type ResourceSchema = typeof en
const middleware = defineI18nMiddleware<[ResourceSchema], 'en' | 'ja'>({
messages: {
en: { hello: 'Hello, {name}' }
}
// something options
// ...
})
const app = createApp({ ...middleware })
// something your implementation code ...
// ...
```
Result of type checking with `tsc`:
```sh
npx tsc --noEmit
index.ts:13:3 - error TS2741: Property 'ja' is missing in type '{ en: { hello: string; }; }' but required in type '{ en: ResourceSchema; ja: ResourceSchema; }'.
13 messages: {
~~~~~~~~
../../node_modules/@intlify/core/node_modules//core-base/dist/core-base.d.ts:125:5
125 messages?: {
~~~~~~~~
The expected type comes from property 'messages' which is declared here on type 'CoreOptions<string, { message: ResourceSchema; datetime: DateTimeFormat; number: NumberFormat; }, { messages: "en"; datetimeFormats: "en"; numberFormats: "en"; } | { ...; }, ... 8 more ..., NumberFormats<...>>'
Found 1 error in index.ts:13
```
If you are using [Visual Studio Code](https://code.visualstudio.com/) as an editor, you can notice that there is a resource definition omission in the editor with the following error before you run the typescript compilation.

## ๐๏ธ Resource keys completion
<!-- eslint-disable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
> [!WARNING]
> **This is experimental feature (inspired from [vue-i18n](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#typescript-support)).**
> We would like to get feedback from you ๐.
> [!NOTE]
> Resource Keys completion can be used if you are using [Visual Studio Code](https://code.visualstudio.com/)
<!-- eslint-enable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
You can completion resources key on translation function with `useTranslation`.

resource keys completion has twe ways.
### Type parameter for `useTranslation`
<!-- eslint-disable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
> [!NOTE]
> The exeample code is [here](https://github.com/intlify/h3/tree/main/playground/local-schema)
<!-- eslint-enable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
You can `useTranslation` set the type parameter to the resource schema you want to key completion of the translation function.
the part of example:
```ts
const router = createRouter()
router.get(
'/',
eventHandler(async event => {
type ResourceSchema = {
hello: string
}
// set resource schema as type parameter
const t = await useTranslation<ResourceSchema>(event)
// you can completion when you type `t('`
return t('hello', { name: 'h3' })
})
)
```
### global resource schema with `declare module '/h3'`
<!-- eslint-disable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
> [!NOTE]
> The exeample code is [here](https://github.com/intlify/h3/tree/main/playground/global-schema)
<!-- eslint-enable markdown/no-missing-label-refs -- NOTE(kazupon): ignore github alert -->
You can do resource key completion with the translation function using the typescript `declare module`.
the part of example:
```ts
import en from './locales/en.ts'
// 'en' resource is master schema
type ResourceSchema = typeof en
// you can put the type extending with `declare module` as global resource schema
declare module '/h3' {
// extend `DefineLocaleMessage` with `ResourceSchema`
export interface DefineLocaleMessage extends ResourceSchema {}
}
const router = createRouter()
router.get(
'/',
eventHandler(async event => {
const t = await useTranslation(event)
// you can completion when you type `t('`
return t('hello', { name: 'h3' })
})
)
```
The advantage of this way is that it is not necessary to specify the resource schema in the `useTranslation` type parameter.
## ๐ ๏ธ Utilities & Helpers
`/h3` has a concept of composable utilities & helpers.
See the [API References](./docs/index.md)
## ๐ Contributing guidelines
If you are interested in contributing to `/h3`, I highly recommend checking out [the contributing guidelines](/CONTRIBUTING.md) here. You'll find all the relevant information such as [how to make a PR](/CONTRIBUTING.md#pull-request-guidelines), [how to setup development](/CONTRIBUTING.md#development-setup)) etc., there.
## ยฉ๏ธ License
[MIT](http://opensource.org/licenses/MIT)
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/@intlify/h3?style=flat&colorA=18181B&colorB=FFAD33
[npm-version-href]: https://npmjs.com/package/@intlify/h3
[npm-downloads-src]: https://img.shields.io/npm/dm/@intlify/h3?style=flat&colorA=18181B&colorB=FFAD33
[npm-downloads-href]: https://npmjs.com/package/@intlify/h3
[ci-src]: https://github.com/intlify/utils/actions/workflows/ci.yml/badge.svg
[ci-href]: https://github.com/intlify/utils/actions/workflows/ci.yml