Skip to content

Commit 0b3cb45

Browse files
authored
feat: support resource key completion & type-safe resource (#9)
* feat: support resource key completion resolve #7 * fix: format * feat: add type-safe resource feature
1 parent 116599f commit 0b3cb45

File tree

20 files changed

+528
-40
lines changed

20 files changed

+528
-40
lines changed

README.md

+139
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,145 @@ const middleware = defineI18nMiddleware({
107107
})
108108
```
109109

110+
## 🧩 Type-safe resources
111+
112+
> [!WARNING]
113+
> **This is experimental feature (inspired from [vue-i18n](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#typescript-support)).**
114+
> We would like to get feedback from you 🙂.
115+
116+
> [!NOTE]
117+
> The exeample code is [here](./playground/typesafe-schema)
118+
119+
You can support the type-safe resources with schema using TypeScript on `defineI18nMiddleware` options.
120+
121+
Locale messages resource:
122+
123+
```ts
124+
export default {
125+
hello: 'hello, {name}!'
126+
}
127+
```
128+
129+
your application code:
130+
131+
```ts
132+
import { defineI18nMiddleware } from '@intlify/h3'
133+
import { createApp } from 'h3'
134+
import en from './locales/en.ts'
135+
136+
// define resource schema, as 'en' is master resource schema
137+
type ResourceSchema = typeof en
138+
139+
const middleware = defineI18nMiddleware<[ResourceSchema], 'en' | 'ja'>({
140+
messages: {
141+
en: { hello: 'Hello, {name}' },
142+
},
143+
// something options
144+
// ...
145+
})
146+
147+
const app = createApp({ ...middleware })
148+
// someting your implementation code ...
149+
// ...
150+
```
151+
152+
Result of type checking with `tsc`:
153+
154+
```sh
155+
npx tsc --noEmit
156+
index.ts:13:3 - error TS2741: Property 'ja' is missing in type '{ en: { hello: string; }; }' but required in type '{ en: ResourceSchema; ja: ResourceSchema; }'.
157+
158+
13 messages: {
159+
~~~~~~~~
160+
161+
../../node_modules/@intlify/core/node_modules/@intlify/core-base/dist/core-base.d.ts:125:5
162+
125 messages?: {
163+
~~~~~~~~
164+
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<...>>'
165+
166+
167+
Found 1 error in index.ts:13
168+
```
169+
170+
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.
171+
172+
![Type-safe resources](assets/typesafe-schema.png)
173+
174+
175+
## 🖌️ Resource keys completion
176+
177+
> [!WARNING]
178+
> **This is experimental feature (inspired from [vue-i18n](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#typescript-support)).**
179+
> We would like to get feedback from you 🙂.
180+
181+
> [!NOTE]
182+
> Resource Keys completion can be used if you are using [Visual Studio Code](https://code.visualstudio.com/)
183+
184+
You can completion resources key on translation function with `useTranslation`.
185+
186+
![Key Completion](assets/key-completion.gif)
187+
188+
resource keys completion has twe ways.
189+
190+
### Type parameter for `useTranslation`
191+
192+
> [!NOTE]
193+
> The exeample code is [here](./playground/local-schema)
194+
195+
You can `useTranslation` set the type parameter to the resource schema you want to key completion of the translation function.
196+
197+
the part of example:
198+
```ts
199+
const router = createRouter()
200+
router.get(
201+
'/',
202+
eventHandler((event) => {
203+
type ResourceSchema = {
204+
hello: string
205+
}
206+
// set resource schema as type parameter
207+
const t = useTranslation<ResourceSchema>(event)
208+
// you can completion when you type `t('`
209+
return t('hello', { name: 'h3' })
210+
}),
211+
)
212+
```
213+
214+
### define global resource schema with `declare module '@intlify/h3'`
215+
216+
> [!NOTE]
217+
> The exeample code is [here](./playground/global-schema)
218+
219+
You can do resource key completion with the translation function using the typescript `declare module`.
220+
221+
the part of example:
222+
```ts
223+
import en from './locales/en.ts'
224+
225+
// 'en' resource is master schema
226+
type ResourceSchema = typeof en
227+
228+
// you can put the type extending with `declare module` as global resource schema
229+
declare module '@intlify/h3' {
230+
// extend `DefineLocaleMessage` with `ResourceSchema`
231+
export interface DefineLocaleMessage extends ResourceSchema {}
232+
}
233+
234+
const router = createRouter()
235+
router.get(
236+
'/',
237+
eventHandler((event) => {
238+
const t = useTranslation(event)
239+
// you can completion when you type `t('`
240+
return t('hello', { name: 'h3' })
241+
}),
242+
)
243+
244+
```
245+
246+
The advantage of this way is that it is not necessary to specify the resource schema in the `useTranslation` type parameter.
247+
248+
110249
## 🛠️ Utilites & Helpers
111250
112251
`@intlify/h3` has a concept of composable utilities & helpers.

assets/key-completion.gif

2.52 MB
Loading

assets/key-completion.mp4

1.39 MB
Binary file not shown.

assets/typesafe-schema.png

193 KB
Loading

bun.lockb

-6 Bytes
Binary file not shown.

deno.jsonc

+8-14
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,18 @@
33
"h3": "npm:h3"
44
},
55
"fmt": {
6-
"files": {
7-
/*
8-
"include": [
9-
],
10-
*/
11-
"exclude": ["node_modules", "dist", "README.md"]
12-
},
6+
"exclude": [
7+
"node_modules",
8+
"dist",
9+
"coverage",
10+
"**/*.md"
11+
],
12+
"lineWidth": 100,
1313
"semiColons": false,
1414
"singleQuote": true
1515
},
1616
"lint": {
17-
"files": {
18-
/*
19-
"include": [
20-
],
21-
*/
22-
"exclude": ["node_modules", "dist"]
23-
}
17+
"exclude": ["node_modules", "dist", "coverage", "playground"]
2418
/*
2519
"rules": {
2620
"tags": ["recommended"],

package.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@
5454
"lint": "deno lint",
5555
"format": "deno fmt",
5656
"build": "unbuild",
57-
"play": "bun run ./playground/index.ts",
58-
"test": "vitest run",
57+
"play:basic": "bun run ./playground/basicindex.ts",
58+
"test": "npm run test:type && npm run test:unit",
59+
"test:type": "vitest typecheck --run",
60+
"test:unit": "vitest run",
5961
"test:coverage": "npm test -- --reporter verbose --coverage"
6062
},
6163
"lint-staged": {
62-
"*.{js,ts,jsx,tsx,md,json,jsonc}": [
64+
"*.{js,ts,jsx,tsx,json,jsonc}": [
6365
"deno fmt"
6466
],
6567
"*.{js,ts,jsx,tsx}": [
@@ -80,7 +82,7 @@
8082
"vitest": "^1.0.0-beta.2"
8183
},
8284
"dependencies": {
83-
"@intlify/core": "npm:@intlify/[email protected]3caed81",
85+
"@intlify/core": "npm:@intlify/[email protected]bd7ec22",
8486
"@intlify/utils": "^0.9.0"
8587
}
8688
}

playground/README.md renamed to playground/basic/README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `@intlify/h3` playground
1+
# basic usage playground
22

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

@@ -8,8 +8,7 @@ This playground is translation with `accept-language` header.
88
npm run dev
99
```
1010

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

1413
```sh
1514
curl -H 'Accept-Language: ja,en-US;q=0.7,en;q=0.3' http://localhost:3000

playground/index.ts renamed to playground/basic/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
defineI18nMiddleware,
55
detectLocaleFromAcceptLanguageHeader,
66
useTranslation,
7-
} from '../src/index'
7+
} from '../../src/index.ts' // `@inlify/h3`
88

99
const middleware = defineI18nMiddleware({
1010
locale: detectLocaleFromAcceptLanguageHeader,

playground/global-schema/index.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createServer } from 'node:http'
2+
import { createApp, createRouter, eventHandler, toNodeListener } from 'h3'
3+
import {
4+
defineI18nMiddleware,
5+
detectLocaleFromAcceptLanguageHeader,
6+
useTranslation,
7+
} from '../../src/index.ts' // in your project, `import { ... } from '@inlify/h3'`
8+
9+
import en from './locales/en.ts'
10+
import ja from './locales/ja.ts'
11+
12+
// 'en' resource is master schema
13+
type ResourceSchema = typeof en
14+
15+
// you can put the type extending with `declare module` as global resource schema
16+
declare module '../../src/index.ts' { // please use `declare module '@intlifly/h3'`, if you want to use global resource schema in your project.
17+
export interface DefineLocaleMessage extends ResourceSchema {}
18+
}
19+
const middleware = defineI18nMiddleware({
20+
locale: detectLocaleFromAcceptLanguageHeader,
21+
messages: {
22+
en,
23+
ja,
24+
},
25+
})
26+
27+
const app = createApp({ ...middleware })
28+
29+
const router = createRouter()
30+
router.get(
31+
'/',
32+
eventHandler((event) => {
33+
const t = useTranslation(event)
34+
return t('hello', { name: 'h3' })
35+
}),
36+
)
37+
38+
app.use(router)
39+
createServer(toNodeListener(app)).listen(3000)
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
hello: 'hello, {name}',
3+
nest: {
4+
foo: {
5+
bar: 'bar',
6+
},
7+
},
8+
}
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
hello: 'こんにちは、{name}',
3+
nest: {
4+
foo: {
5+
bar: 'ばー',
6+
},
7+
},
8+
}

playground/local-schema/index.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { createServer } from 'node:http'
2+
import { createApp, createRouter, eventHandler, toNodeListener, use } from 'h3'
3+
import {
4+
defineI18nMiddleware,
5+
detectLocaleFromAcceptLanguageHeader,
6+
useTranslation,
7+
} from '../../src/index.ts' // in your project, `import { ... } from '@inlify/h3'`
8+
9+
import en from './locales/en.ts'
10+
import ja from './locales/ja.ts'
11+
12+
const middleware = defineI18nMiddleware({
13+
locale: detectLocaleFromAcceptLanguageHeader,
14+
messages: {
15+
en,
16+
ja,
17+
},
18+
})
19+
20+
const app = createApp({ ...middleware })
21+
22+
const router = createRouter()
23+
router.get(
24+
'/',
25+
eventHandler((event) => {
26+
type ResourceSchema = {
27+
hello: string
28+
}
29+
const t = useTranslation<ResourceSchema>(event)
30+
return t('hello', { name: 'h3' })
31+
}),
32+
)
33+
34+
app.use(router)
35+
createServer(toNodeListener(app)).listen(3000)

playground/local-schema/locales/en.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
hello: 'hello, {name}',
3+
nest: {
4+
foo: {
5+
bar: 'bar',
6+
},
7+
},
8+
}

playground/local-schema/locales/ja.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
hello: 'こんにちは、{name}',
3+
nest: {
4+
foo: {
5+
bar: 'ばー',
6+
},
7+
},
8+
}

playground/typesafe-schema/index.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// in your project, `import { ... } from '@inlify/h3'`
2+
import { defineI18nMiddleware } from '../../src/index.ts'
3+
import { createApp } from 'h3'
4+
5+
// define resource schema
6+
type ResourceSchema = {
7+
hello: string
8+
}
9+
10+
// you can specify resource schema and locales to type parameter.
11+
// - first type parameter: resource schema
12+
// - second type parameter: locales
13+
const middleware = defineI18nMiddleware<[ResourceSchema], 'en' | 'ja'>({
14+
messages: {
15+
en: { hello: 'Hello, {name}' },
16+
// you can see the type error, when you will comment out the below `ja` resource
17+
ja: { hello: 'こんにちは、{name}' },
18+
},
19+
// something options
20+
// ...
21+
})
22+
23+
const app = createApp({ ...middleware })
24+
// someting your implementation code ...
25+
// ...
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
hello: 'world',
3+
}

0 commit comments

Comments
 (0)