Skip to content

Commit 5dd3c2c

Browse files
authored
feat!: support async locale detector (#19)
* feat!: support async locale detector * fix: update docs
1 parent effd59b commit 5dd3c2c

File tree

12 files changed

+172
-69
lines changed

12 files changed

+172
-69
lines changed

README.md

+50-16
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ const app = createApp({ ...middleware })
6767
const router = createRouter()
6868
router.get(
6969
'/',
70-
eventHandler((event) => {
70+
eventHandler(async (event) => {
7171
// use `useTranslation` in event handler
72-
const t = useTranslation(event)
72+
const t = await useTranslation(event)
7373
return t('hello', { name: 'h3' })
7474
}),
7575
)
@@ -88,15 +88,48 @@ example for detecting locale from url query:
8888
import { defineI18nMiddleware, getQueryLocale } from '@intlify/h3'
8989
import type { H3Event } from 'h3'
9090

91-
const DEFAULT_LOCALE = 'en'
92-
9391
// define custom locale detector
9492
const localeDetector = (event: H3Event): string => {
95-
try {
96-
return getQueryLocale(event).toString()
97-
} catch () {
98-
return DEFAULT_LOCALE
93+
return getQueryLocale(event).toString()
94+
}
95+
96+
const middleware = defineI18nMiddleware({
97+
// set your custom locale detector
98+
locale: localeDetector,
99+
// something options
100+
// ...
101+
})
102+
```
103+
104+
You can make that function asynchronous. This is useful when loading resources along with locale detection.
105+
106+
> [!NOTE]
107+
> The case which a synchronous function returns a promise is not supported. you need to use `async function`.
108+
109+
```ts
110+
import { defineI18nMiddleware, getQueryLocale } from '@intlify/h3'
111+
import type { DefineLocaleMessage } from '@intlify/h3'
112+
import type { H3Event } from 'h3'
113+
114+
const loader = (path: string) => import(path).then((m) => m.default || m)
115+
const messages: Record<string, () => ReturnType<typeof loader>> = {
116+
en: () => loader('./locales/en.json'),
117+
ja: () => loader('./locales/ja.json'),
118+
}
119+
120+
// define custom locale detector and lazy loading
121+
const localeDetector = async (event: H3Event, i18n: CoreContext<string, DefineLocaleMessage>): Promise<string> => {
122+
// detect locale
123+
const locale = getCookieLocale(event).toString()
124+
125+
// resource lazy loading
126+
const loader = messages[locale]
127+
if (loader && !i18n.messages[locale]) {
128+
const message = await loader()
129+
i18n.messages[locale] = message
99130
}
131+
132+
return locale
100133
}
101134

102135
const middleware = defineI18nMiddleware({
@@ -107,9 +140,10 @@ const middleware = defineI18nMiddleware({
107140
})
108141
```
109142

143+
110144
## 🧩 Type-safe resources
111145

112-
> [!WARNING]
146+
> [!WARNING]
113147
> **This is experimental feature (inspired from [vue-i18n](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#typescript-support)).**
114148
> We would like to get feedback from you 🙂.
115149
@@ -174,7 +208,7 @@ If you are using [Visual Studio Code](https://code.visualstudio.com/) as an edit
174208
175209
## 🖌️ Resource keys completion
176210
177-
> [!WARNING]
211+
> [!WARNING]
178212
> **This is experimental feature (inspired from [vue-i18n](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#typescript-support)).**
179213
> We would like to get feedback from you 🙂.
180214
@@ -199,12 +233,12 @@ the part of example:
199233
const router = createRouter()
200234
router.get(
201235
'/',
202-
eventHandler((event) => {
236+
eventHandler(async (event) => {
203237
type ResourceSchema = {
204238
hello: string
205239
}
206240
// set resource schema as type parameter
207-
const t = useTranslation<ResourceSchema>(event)
241+
const t = await useTranslation<ResourceSchema>(event)
208242
// you can completion when you type `t('`
209243
return t('hello', { name: 'h3' })
210244
}),
@@ -234,8 +268,8 @@ declare module '@intlify/h3' {
234268
const router = createRouter()
235269
router.get(
236270
'/',
237-
eventHandler((event) => {
238-
const t = useTranslation(event)
271+
eventHandler(async (event) => {
272+
const t = await useTranslation(event)
239273
// you can completion when you type `t('`
240274
return t('hello', { name: 'h3' })
241275
}),
@@ -253,11 +287,11 @@ The advantage of this way is that it is not necessary to specify the resource sc
253287
### Utilities
254288
255289
`@intlify/h3` composable utilities accept event (from
256-
`eventHandler((event) => {})`) as their first argument. (Exclud `useTranslation`) return the [`Intl.Locale`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale)
290+
`eventHandler((event) => {})`) as their first argument. (Exclude `useTranslation`) return the [`Intl.Locale`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale)
257291
258292
### Translations
259293
260-
- `useTranslation(event)`: use translation function
294+
- `useTranslation(event)`: use translation function, asynchronous
261295
262296
### Headers
263297

bun.lockb

0 Bytes
Binary file not shown.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"vitest": "^1.0.0-beta.4"
8383
},
8484
"dependencies": {
85-
"@intlify/core": "^9.7.1",
85+
"@intlify/core": "^9.8.0",
8686
"@intlify/utils": "^0.11.1"
8787
}
8888
}

playground/basic/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ const app = createApp({ ...middleware })
2323
const router = createRouter()
2424
router.get(
2525
'/',
26-
eventHandler((event) => {
27-
const t = useTranslation(event)
26+
eventHandler(async (event) => {
27+
const t = await useTranslation(event)
2828
return t('hello', { name: 'h3' })
2929
}),
3030
)

playground/global-schema/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const app = createApp({ ...middleware })
2929
const router = createRouter()
3030
router.get(
3131
'/',
32-
eventHandler((event) => {
33-
const t = useTranslation(event)
32+
eventHandler(async (event) => {
33+
const t = await useTranslation(event)
3434
return t('hello', { name: 'h3' })
3535
}),
3636
)

playground/local-schema/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ const app = createApp({ ...middleware })
2222
const router = createRouter()
2323
router.get(
2424
'/',
25-
eventHandler((event) => {
25+
eventHandler(async (event) => {
2626
type ResourceSchema = {
2727
hello: string
2828
}
29-
const t = useTranslation<ResourceSchema>(event)
29+
const t = await useTranslation<ResourceSchema>(event)
3030
return t('hello', { name: 'h3' })
3131
}),
3232
)

spec/fixtures/en.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"hello": "hello, {name}"
3+
}

spec/fixtures/ja.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"hello": "こんにちは, {name}"
3+
}

spec/integration.spec.ts

+86-31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterEach, expect, test, vi } from 'vitest'
1+
import { afterEach, describe, expect, test, vi } from 'vitest'
22
import { createApp, eventHandler, toNodeListener } from 'h3'
33
import { getQueryLocale } from '@intlify/utils/h3'
44
import supertest from 'supertest'
@@ -11,6 +11,8 @@ import {
1111

1212
import type { App, H3Event } from 'h3'
1313
import type { SuperTest, Test } from 'supertest'
14+
import type { CoreContext } from '@intlify/core'
15+
import type { DefineLocaleMessage } from '../src/index.ts'
1416

1517
let app: App
1618
let request: SuperTest<Test>
@@ -36,8 +38,8 @@ test('translation', async () => {
3638

3739
app.use(
3840
'/',
39-
eventHandler((event) => {
40-
const t = useTranslation(event)
41+
eventHandler(async (event) => {
42+
const t = await useTranslation(event)
4143
return { message: t('hello', { name: 'h3' }) }
4244
}),
4345
)
@@ -49,40 +51,93 @@ test('translation', async () => {
4951
expect(res.body).toEqual({ message: 'hello, h3' })
5052
})
5153

52-
test('custom locale detection', async () => {
53-
const defaultLocale = 'en'
54-
55-
// define custom locale detector
56-
const localeDetector = (event: H3Event): string => {
57-
try {
54+
describe('custom locale detection', () => {
55+
test('basic', async () => {
56+
// define custom locale detector
57+
const localeDetector = (event: H3Event): string => {
5858
return getQueryLocale(event).toString()
59-
} catch (_e) {
60-
return defaultLocale
6159
}
62-
}
6360

64-
const middleware = defineI18nMiddleware({
65-
locale: localeDetector,
66-
messages: {
61+
const middleware = defineI18nMiddleware({
62+
locale: localeDetector,
63+
messages: {
64+
en: {
65+
hello: 'hello, {name}',
66+
},
67+
ja: {
68+
hello: 'こんにちは, {name}',
69+
},
70+
},
71+
})
72+
app = createApp({ ...middleware })
73+
request = supertest(toNodeListener(app))
74+
75+
app.use(
76+
'/',
77+
eventHandler(async (event) => {
78+
const t = await useTranslation(event)
79+
return { message: t('hello', { name: 'h3' }) }
80+
}),
81+
)
82+
83+
const res = await request.get('/?locale=ja')
84+
expect(res.body).toEqual({ message: 'こんにちは, h3' })
85+
})
86+
87+
test('async', async () => {
88+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
89+
90+
const loader = (path: string) => import(path).then((m) => m.default || m)
91+
const messages: Record<string, () => ReturnType<typeof loader>> = {
92+
en: () => loader('./fixtures/en.json'),
93+
ja: () => loader('./fixtures/ja.json'),
94+
}
95+
96+
// async locale detector
97+
const localeDetector = async (
98+
event: H3Event,
99+
i18n: CoreContext<string, DefineLocaleMessage>,
100+
) => {
101+
const locale = getQueryLocale(event).toString()
102+
await sleep(100)
103+
const loader = messages[locale]
104+
if (loader && !i18n.messages[locale]) {
105+
const message = await loader()
106+
i18n.messages[locale] = message
107+
}
108+
return locale
109+
}
110+
111+
const middleware = defineI18nMiddleware({
112+
locale: localeDetector,
113+
messages: {
114+
en: {
115+
hello: 'hello, {name}',
116+
},
117+
},
118+
})
119+
app = createApp({ ...middleware })
120+
request = supertest(toNodeListener(app))
121+
122+
app.use(
123+
'/',
124+
eventHandler(async (event) => {
125+
const t = await useTranslation(event)
126+
return { message: t('hello', { name: 'h3' }) }
127+
}),
128+
)
129+
130+
const translated: Record<string, { message: string }> = {
67131
en: {
68-
hello: 'hello, {name}',
132+
message: 'hello, h3',
69133
},
70134
ja: {
71-
hello: 'こんにちは, {name}',
135+
message: 'こんにちは, h3',
72136
},
73-
},
137+
}
138+
for (const locale of ['en', 'ja']) {
139+
const res = await request.get(`/?locale=${locale}`)
140+
expect(res.body).toEqual(translated[locale])
141+
}
74142
})
75-
app = createApp({ ...middleware })
76-
request = supertest(toNodeListener(app))
77-
78-
app.use(
79-
'/',
80-
eventHandler((event) => {
81-
const t = useTranslation(event)
82-
return { message: t('hello', { name: 'h3' }) }
83-
}),
84-
)
85-
86-
const res = await request.get('/?locale=ja')
87-
expect(res.body).toEqual({ message: 'こんにちは, h3' })
88143
})

src/index.test-d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ test('defineI18nMiddleware', () => {
2525
})
2626
})
2727

28-
test('translation function', () => {
28+
test('translation function', async () => {
2929
const eventMock = {
3030
node: {
3131
req: {
@@ -44,7 +44,7 @@ test('translation function', () => {
4444
},
4545
}
4646

47-
const t = useTranslation<typeof resources>(eventMock)
47+
const t = await useTranslation<typeof resources>(eventMock)
4848
expectTypeOf<string>(t('test')).toMatchTypeOf<string>()
4949
expectTypeOf<string>(t('foo')).toMatchTypeOf<string>()
5050
expectTypeOf<string>(t('bar.buz.baz')).toMatchTypeOf<string>()

src/index.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ test('defineI18nMiddleware', () => {
4141
})
4242

4343
describe('useTranslation', () => {
44-
test('basic', () => {
44+
test('basic', async () => {
4545
/**
4646
* setup `defineI18nMiddleware` emulates
4747
*/
@@ -75,11 +75,11 @@ describe('useTranslation', () => {
7575
context.locale = bindLocaleDetector
7676

7777
// test `useTranslation`
78-
const t = useTranslation(eventMock)
78+
const t = await useTranslation(eventMock)
7979
expect(t('hello', { name: 'h3' })).toEqual('こんにちは, h3')
8080
})
8181

82-
test('not initilize context', () => {
82+
test('not initilize context', async () => {
8383
const eventMock = {
8484
node: {
8585
req: {
@@ -92,6 +92,6 @@ describe('useTranslation', () => {
9292
context: {},
9393
} as H3Event
9494

95-
expect(() => useTranslation(eventMock)).toThrowError()
95+
await expect(() => useTranslation(eventMock)).rejects.toThrowError()
9696
})
9797
})

0 commit comments

Comments
 (0)