Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support resource key completion & type-safe resource #9

Merged
merged 3 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,145 @@ const middleware = defineI18nMiddleware({
})
```

## 🧩 Type-safe resources

> [!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](./playground/typesafe-schema)

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 '@intlify/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 })
// someting 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/@intlify/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.

![Type-safe resources](assets/typesafe-schema.png)


## 🖌️ Resource keys completion

> [!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/)

You can completion resources key on translation function with `useTranslation`.

![Key Completion](assets/key-completion.gif)

resource keys completion has twe ways.

### Type parameter for `useTranslation`

> [!NOTE]
> The exeample code is [here](./playground/local-schema)

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((event) => {
type ResourceSchema = {
hello: string
}
// set resource schema as type parameter
const t = useTranslation<ResourceSchema>(event)
// you can completion when you type `t('`
return t('hello', { name: 'h3' })
}),
)
```

### define global resource schema with `declare module '@intlify/h3'`

> [!NOTE]
> The exeample code is [here](./playground/global-schema)

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 '@intlify/h3' {
// extend `DefineLocaleMessage` with `ResourceSchema`
export interface DefineLocaleMessage extends ResourceSchema {}
}

const router = createRouter()
router.get(
'/',
eventHandler((event) => {
const t = 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.


## 🛠️ Utilites & Helpers

`@intlify/h3` has a concept of composable utilities & helpers.
Expand Down
Binary file added assets/key-completion.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/key-completion.mp4
Binary file not shown.
Binary file added assets/typesafe-schema.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bun.lockb
Binary file not shown.
22 changes: 8 additions & 14 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,18 @@
"h3": "npm:h3"
},
"fmt": {
"files": {
/*
"include": [
],
*/
"exclude": ["node_modules", "dist", "README.md"]
},
"exclude": [
"node_modules",
"dist",
"coverage",
"**/*.md"
],
"lineWidth": 100,
"semiColons": false,
"singleQuote": true
},
"lint": {
"files": {
/*
"include": [
],
*/
"exclude": ["node_modules", "dist"]
}
"exclude": ["node_modules", "dist", "coverage", "playground"]
/*
"rules": {
"tags": ["recommended"],
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@
"lint": "deno lint",
"format": "deno fmt",
"build": "unbuild",
"play": "bun run ./playground/index.ts",
"test": "vitest run",
"play:basic": "bun run ./playground/basicindex.ts",
"test": "npm run test:type && npm run test:unit",
"test:type": "vitest typecheck --run",
"test:unit": "vitest run",
"test:coverage": "npm test -- --reporter verbose --coverage"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,md,json,jsonc}": [
"*.{js,ts,jsx,tsx,json,jsonc}": [
"deno fmt"
],
"*.{js,ts,jsx,tsx}": [
Expand All @@ -80,7 +82,7 @@
"vitest": "^1.0.0-beta.2"
},
"dependencies": {
"@intlify/core": "npm:@intlify/[email protected]3caed81",
"@intlify/core": "npm:@intlify/[email protected]bd7ec22",
"@intlify/utils": "^0.9.0"
}
}
5 changes: 2 additions & 3 deletions playground/README.md → playground/basic/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `@intlify/h3` playground
# basic usage playground

This playground is translation with `accept-language` header.

Expand All @@ -8,8 +8,7 @@ This playground is translation with `accept-language` header.
npm run dev
```

and then, you try to access to `http://localhost:3000` with `accept-language`
header with another shell:
and then, you try to access to `http://localhost:3000` with `accept-language` header with another shell:

```sh
curl -H 'Accept-Language: ja,en-US;q=0.7,en;q=0.3' http://localhost:3000
Expand Down
2 changes: 1 addition & 1 deletion playground/index.ts → playground/basic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
defineI18nMiddleware,
detectLocaleFromAcceptLanguageHeader,
useTranslation,
} from '../src/index'
} from '../../src/index.ts' // `@inlify/h3`

const middleware = defineI18nMiddleware({
locale: detectLocaleFromAcceptLanguageHeader,
Expand Down
39 changes: 39 additions & 0 deletions playground/global-schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createServer } from 'node:http'
import { createApp, createRouter, eventHandler, toNodeListener } from 'h3'
import {
defineI18nMiddleware,
detectLocaleFromAcceptLanguageHeader,
useTranslation,
} from '../../src/index.ts' // in your project, `import { ... } from '@inlify/h3'`

import en from './locales/en.ts'
import ja from './locales/ja.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 '../../src/index.ts' { // please use `declare module '@intlifly/h3'`, if you want to use global resource schema in your project.
export interface DefineLocaleMessage extends ResourceSchema {}
}
const middleware = defineI18nMiddleware({
locale: detectLocaleFromAcceptLanguageHeader,
messages: {
en,
ja,
},
})

const app = createApp({ ...middleware })

const router = createRouter()
router.get(
'/',
eventHandler((event) => {
const t = useTranslation(event)
return t('hello', { name: 'h3' })
}),
)

app.use(router)
createServer(toNodeListener(app)).listen(3000)
8 changes: 8 additions & 0 deletions playground/global-schema/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
hello: 'hello, {name}',
nest: {
foo: {
bar: 'bar',
},
},
}
8 changes: 8 additions & 0 deletions playground/global-schema/locales/ja.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
hello: 'こんにちは、{name}',
nest: {
foo: {
bar: 'ばー',
},
},
}
35 changes: 35 additions & 0 deletions playground/local-schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createServer } from 'node:http'
import { createApp, createRouter, eventHandler, toNodeListener, use } from 'h3'
import {
defineI18nMiddleware,
detectLocaleFromAcceptLanguageHeader,
useTranslation,
} from '../../src/index.ts' // in your project, `import { ... } from '@inlify/h3'`

import en from './locales/en.ts'
import ja from './locales/ja.ts'

const middleware = defineI18nMiddleware({
locale: detectLocaleFromAcceptLanguageHeader,
messages: {
en,
ja,
},
})

const app = createApp({ ...middleware })

const router = createRouter()
router.get(
'/',
eventHandler((event) => {
type ResourceSchema = {
hello: string
}
const t = useTranslation<ResourceSchema>(event)
return t('hello', { name: 'h3' })
}),
)

app.use(router)
createServer(toNodeListener(app)).listen(3000)
8 changes: 8 additions & 0 deletions playground/local-schema/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
hello: 'hello, {name}',
nest: {
foo: {
bar: 'bar',
},
},
}
8 changes: 8 additions & 0 deletions playground/local-schema/locales/ja.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
hello: 'こんにちは、{name}',
nest: {
foo: {
bar: 'ばー',
},
},
}
25 changes: 25 additions & 0 deletions playground/typesafe-schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// in your project, `import { ... } from '@inlify/h3'`
import { defineI18nMiddleware } from '../../src/index.ts'
import { createApp } from 'h3'

// define resource schema
type ResourceSchema = {
hello: string
}

// you can specify resource schema and locales to type parameter.
// - first type parameter: resource schema
// - second type parameter: locales
const middleware = defineI18nMiddleware<[ResourceSchema], 'en' | 'ja'>({
messages: {
en: { hello: 'Hello, {name}' },
// you can see the type error, when you will comment out the below `ja` resource
ja: { hello: 'こんにちは、{name}' },
},
// something options
// ...
})

const app = createApp({ ...middleware })
// someting your implementation code ...
// ...
3 changes: 3 additions & 0 deletions playground/typesafe-schema/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
hello: 'world',
}
Loading