From 44f81d1021486bb5f2f00a1610ea5c38f96b49dd Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 20 Nov 2025 23:57:49 +0900 Subject: [PATCH 1/3] feat(h3,hono): add `getDetectorLocale` util --- packages/h3/README.md | 9 ++- .../h3/docs/functions/getDetectorLocale.md | 41 +++++++++++ packages/h3/docs/index.md | 1 + packages/h3/src/index.test.ts | 25 +++++++ packages/h3/src/index.ts | 54 ++++++++++---- packages/hono/README.md | 14 +++- .../hono/docs/functions/getDetectorLocale.md | 41 +++++++++++ .../hono/docs/functions/useTranslation.md | 2 +- packages/hono/docs/index.md | 3 +- packages/hono/src/index.test.ts | 31 ++++++++ packages/hono/src/index.ts | 71 +++++++++++++------ 11 files changed, 252 insertions(+), 40 deletions(-) create mode 100644 packages/h3/docs/functions/getDetectorLocale.md create mode 100644 packages/hono/docs/functions/getDetectorLocale.md diff --git a/packages/h3/README.md b/packages/h3/README.md index 168909c..e7f0621 100644 --- a/packages/h3/README.md +++ b/packages/h3/README.md @@ -149,11 +149,11 @@ export default defineNitroPlugin(nitroApp => { You can detect locale with your custom logic from current `H3Event`. -example for detecting locale from url query: +example for detecting locale from url query, and get locale with `getDetectorLocale` util: ```ts import { H3 } from 'h3' -import { intlify, getQueryLocale } from '@intlify/h3' +import { intlify, getQueryLocale, getDetectorLocale } from '@intlify/h3' import type { H3Event } from 'h3' @@ -172,6 +172,11 @@ const app = new H3({ }) ] }) + +app.get('/', async event => { + const locale = await getDetectorLocale(event) + console.log(`Current Locale: ${locale.language}`) +}) ``` You can make that function asynchronous. This is useful when loading resources along with locale detection. diff --git a/packages/h3/docs/functions/getDetectorLocale.md b/packages/h3/docs/functions/getDetectorLocale.md new file mode 100644 index 0000000..8ae01aa --- /dev/null +++ b/packages/h3/docs/functions/getDetectorLocale.md @@ -0,0 +1,41 @@ +[**@intlify/h3**](../index.md) + +*** + +[@intlify/h3](../index.md) / getDetectorLocale + +# Function: getDetectorLocale() + +```ts +function getDetectorLocale(event): Promise; +``` + +get a locale which is detected with locale detector. + +## Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `event` | `H3Event` | A H3 event | + +## Returns + +`Promise`\<`Locale`\> + +Return a Intl.Locale \| locale + +## Description + +The locale obtainable via this function comes from the locale detector specified in the `locale` option of the [intlify](../variables/intlify.md) plugin. + +## Example + +```js +app.get( + '/', + async (event) => { + const locale = await getDetectorLocale(event) + return `Current Locale: ${locale.language}` + }, +) +``` diff --git a/packages/h3/docs/index.md b/packages/h3/docs/index.md index 8d10a1a..e875dde 100644 --- a/packages/h3/docs/index.md +++ b/packages/h3/docs/index.md @@ -18,6 +18,7 @@ Internationalization middleware & utilities for h3 | ------ | ------ | | [detectLocaleFromAcceptLanguageHeader](functions/detectLocaleFromAcceptLanguageHeader.md) | Locale detection with `Accept-Language` header | | [getCookieLocale](functions/getCookieLocale.md) | get locale from cookie | +| [getDetectorLocale](functions/getDetectorLocale.md) | get a locale which is detected with locale detector. | | [getHeaderLanguage](functions/getHeaderLanguage.md) | get language from header | | [getHeaderLanguages](functions/getHeaderLanguages.md) | get languages from header | | [getHeaderLocale](functions/getHeaderLocale.md) | get locale from header | diff --git a/packages/h3/src/index.test.ts b/packages/h3/src/index.test.ts index 481390d..db85d4f 100644 --- a/packages/h3/src/index.test.ts +++ b/packages/h3/src/index.test.ts @@ -5,6 +5,7 @@ import { SYMBOL_I18N, SYMBOL_I18N_LOCALE } from './symbols.ts' import { defineI18nMiddleware, detectLocaleFromAcceptLanguageHeader, + getDetectorLocale, useTranslation } from './index.ts' @@ -88,3 +89,27 @@ describe('useTranslation', () => { await expect(() => useTranslation(eventMock)).rejects.toThrowError() }) }) + +test('getDetectorLocale', async () => { + const context = createCoreContext({ + locale: detectLocaleFromAcceptLanguageHeader + }) + const eventMock = { + req: { + headers: { + get: _name => (_name === 'accept-language' ? 'ja;q=0.9,en;q=0.8' : '') + } + }, + context: { + [SYMBOL_I18N]: context as CoreContext + } + } as H3Event + const _locale = context.locale as unknown + const bindLocaleDetector = (_locale as LocaleDetector).bind(null, eventMock) + // @ts-ignore ignore type error because this is test + context.locale = bindLocaleDetector + eventMock.context[SYMBOL_I18N_LOCALE] = bindLocaleDetector + + const locale = await getDetectorLocale(eventMock) + expect(locale.language).toEqual('ja') +}) diff --git a/packages/h3/src/index.ts b/packages/h3/src/index.ts index a370411..7f26ad1 100644 --- a/packages/h3/src/index.ts +++ b/packages/h3/src/index.ts @@ -261,6 +261,22 @@ export const detectLocaleFromAcceptLanguageHeader = (event: H3Event): Locale => */ export interface DefineLocaleMessage extends LocaleMessage {} +async function getLocaleAndEventContext(event: H3Event): Promise<[string, H3EventContext]> { + const context = getEventContext(event) + if (context[SYMBOL_I18N] == null) { + throw new Error( + 'plugin has not been initialized. Please check that the `intlify` plugin is installed correctly.' + ) + } + + const localeDetector = context[SYMBOL_I18N_LOCALE] as unknown as LocaleDetector + // Always await detector call - works for both sync and async detectors + // (awaiting a non-promise value returns it immediately) + const locale = await localeDetector(event) + context[SYMBOL_I18N].locale = locale + return [locale, context] +} + /** * Use translation function in event handler * @@ -283,19 +299,7 @@ export async function useTranslation< Schema extends Record = {}, // eslint-disable-line @typescript-eslint/no-explicit-any -- NOTE(kazupon): generic type Event extends H3Event = H3Event >(event: Event): Promise> { - const context = getEventContext(event) - if (context[SYMBOL_I18N] == null) { - throw new Error( - 'middleware not initialized, please setup `onRequest` and `onResponse` options of `H3` with the middleware obtained with `defineI18nMiddleware`' - ) - } - - const localeDetector = context[SYMBOL_I18N_LOCALE] as unknown as LocaleDetector - // Always await detector call - works for both sync and async detectors - // (awaiting a non-promise value returns it immediately) - const locale = await localeDetector(event) - context[SYMBOL_I18N].locale = locale - + const [locale, context] = await getLocaleAndEventContext(event) function translate(key: string, ...args: unknown[]): string { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- NOTE(kazupon): generic type const [_, options] = parseTranslateArgs(key, ...args) @@ -317,3 +321,27 @@ export async function useTranslation< return translate as TranslationFunction } + +/** + * get a locale which is detected with locale detector. + * + * @description The locale obtainable via this function comes from the locale detector specified in the `locale` option of the {@link intlify} plugin. + * + * @example + * ```js + * app.get( + * '/', + * async (event) => { + * const locale = await getDetectorLocale(event) + * return `Current Locale: ${locale.language}` + * }, + * ) + * ``` + * @param event - A H3 event + * + * @returns Return a {@link Intl.Locale | locale} + */ +export async function getDetectorLocale(event: H3Event): Promise { + const result = await getLocaleAndEventContext(event) + return new Intl.Locale(result[0]) +} diff --git a/packages/hono/README.md b/packages/hono/README.md index b3d6f2a..f15e1c0 100644 --- a/packages/hono/README.md +++ b/packages/hono/README.md @@ -117,10 +117,11 @@ export default app You can detect locale with your custom logic from current `Context`. -example for detecting locale from url query: +example for detecting locale from url query, and get locale with `getDetectorLocale` util: ```ts -import { defineI18nMiddleware, getQueryLocale } from '@intlify/hono' +import { Hono } from 'hono' +import { defineI18nMiddleware, getQueryLocale, getDetectorLocale } from '@intlify/hono' import type { Context } from 'hono' const DEFAULT_LOCALE = 'en' @@ -134,12 +135,19 @@ const localeDetector = (ctx: Context): string => { } } -const middleware = defineI18nMiddleware({ +const i18nMiddleware = defineI18nMiddleware({ // set your custom locale detector locale: localeDetector // something options // ... }) + +const app = new Hono() +app.use('*', i18nMiddleware) +app.get('/', async ctx => { + const locale = await getDetectorLocale(ctx) + return ctx.text(`Current Locale: ${locale.language}`) +}) ``` You can make that function asynchronous. This is useful when loading resources along with locale detection. diff --git a/packages/hono/docs/functions/getDetectorLocale.md b/packages/hono/docs/functions/getDetectorLocale.md new file mode 100644 index 0000000..a197b46 --- /dev/null +++ b/packages/hono/docs/functions/getDetectorLocale.md @@ -0,0 +1,41 @@ +[**@intlify/hono**](../index.md) + +*** + +[@intlify/hono](../index.md) / getDetectorLocale + +# Function: getDetectorLocale() + +```ts +function getDetectorLocale(ctx): Promise; +``` + +get a locale which is detected with locale detector. + +## Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `ctx` | `Context` | A Hono context | + +## Returns + +`Promise`\<`Locale`\> + +Return a Intl.Locale \| locale + +## Description + +The locale obtainable via this function comes from the locale detector specified in the `locale` option of the [defineI18nMiddleware](defineI18nMiddleware.md). + +## Example + +```js +app.get( + '/', + async ctx => { + const locale = await getDetectorLocale(ctx) + return ctx.text(`Current Locale: ${locale.language}`) + }, +) +``` diff --git a/packages/hono/docs/functions/useTranslation.md b/packages/hono/docs/functions/useTranslation.md index d3a3c35..05ec8e4 100644 --- a/packages/hono/docs/functions/useTranslation.md +++ b/packages/hono/docs/functions/useTranslation.md @@ -25,7 +25,7 @@ function useTranslation(ctx): Promise>>>; ``` -use translation function in event handler +use translation function in handler ## Type Parameters diff --git a/packages/hono/docs/index.md b/packages/hono/docs/index.md index dbe47ec..9ef2ebd 100644 --- a/packages/hono/docs/index.md +++ b/packages/hono/docs/index.md @@ -13,6 +13,7 @@ Internationalization middleware & utilities for Hono | [defineI18nMiddleware](functions/defineI18nMiddleware.md) | define i18n middleware for Hono | | [detectLocaleFromAcceptLanguageHeader](functions/detectLocaleFromAcceptLanguageHeader.md) | locale detection with `Accept-Language` header | | [getCookieLocale](functions/getCookieLocale.md) | get locale from cookie | +| [getDetectorLocale](functions/getDetectorLocale.md) | get a locale which is detected with locale detector. | | [getHeaderLanguage](functions/getHeaderLanguage.md) | get language from header | | [getHeaderLanguages](functions/getHeaderLanguages.md) | get languages from header | | [getHeaderLocale](functions/getHeaderLocale.md) | get locale from header | @@ -25,7 +26,7 @@ Internationalization middleware & utilities for Hono | [tryHeaderLocales](functions/tryHeaderLocales.md) | try to get locales from header | | [tryPathLocale](functions/tryPathLocale.md) | try to get the locale from the path | | [tryQueryLocale](functions/tryQueryLocale.md) | try to get the locale from the query | -| [useTranslation](functions/useTranslation.md) | use translation function in event handler | +| [useTranslation](functions/useTranslation.md) | use translation function in handler | ## Interfaces diff --git a/packages/hono/src/index.test.ts b/packages/hono/src/index.test.ts index bb72c59..7921100 100644 --- a/packages/hono/src/index.test.ts +++ b/packages/hono/src/index.test.ts @@ -7,6 +7,7 @@ import type { Context } from 'hono' import { defineI18nMiddleware, detectLocaleFromAcceptLanguageHeader, + getDetectorLocale, useTranslation } from './index.ts' @@ -93,3 +94,33 @@ describe('useTranslation', () => { await expect(() => useTranslation(mockContext)).rejects.toThrowError() }) }) + +test('getDetectorLocale', async () => { + /** + * setup `defineI18nMiddleware` emulates + */ + const context = createCoreContext({ + locale: detectLocaleFromAcceptLanguageHeader + }) + const mockContext = { + req: { + raw: { + headers: { + get: _name => (_name === 'accept-language' ? 'ja;q=0.9,en;q=0.8' : '') + } + } + }, + get: (key: string) => { + if (key === 'i18n') { + return context + } else if (key === 'i18nLocaleDetector') { + const locale = context.locale as unknown + return (locale as LocaleDetector).bind(null, mockContext) + } + } + } as Context + + // test `getDetectorLocale` + const locale = await getDetectorLocale(mockContext) + expect(locale.language).toEqual('ja') +}) diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts index a7adf65..417174b 100644 --- a/packages/hono/src/index.ts +++ b/packages/hono/src/index.ts @@ -193,8 +193,31 @@ export function defineI18nMiddleware< export const detectLocaleFromAcceptLanguageHeader = (ctx: Context): Locale => getHeaderLocale(ctx.req.raw).toString() +async function getLocaleAndIntlifyContext(ctx: Context): Promise<[string, CoreContext]> { + const intlify = ctx.get('i18n') + if (intlify == null) { + throw new Error( + 'middleware not initialized, please setup `app.use` with the middleware obtained with `defineI18nMiddleware`' + ) + } + + const localeDetector = ctx.get('i18nLocaleDetector') + if (localeDetector == null) { + throw new Error( + 'locale detector not found in context, please make sure that the i18n middleware is correctly set up' + ) + } + + // Always await detector call - works for both sync and async detectors + // (awaiting a non-promise value returns it immediately) + const locale = await localeDetector(ctx) + intlify.locale = locale + + return [locale, intlify] +} + /** - * use translation function in event handler + * use translation function in handler * * @param ctx - A Hono context * @returns Return a translation function, which can be translated with i18n resource messages @@ -229,24 +252,7 @@ export async function useTranslation< Schema extends Record = {}, // eslint-disable-line @typescript-eslint/no-explicit-any -- NOTE(kazupon): generic type HonoContext extends Context = Context >(ctx: HonoContext): Promise> { - const i18n = ctx.get('i18n') - if (i18n == null) { - throw new Error( - 'middleware not initialized, please setup `app.use` with the middleware obtained with `defineI18nMiddleware`' - ) - } - - const localeDetector = ctx.get('i18nLocaleDetector') - if (localeDetector == null) { - throw new Error( - 'locale detector not found in context, please make sure that the i18n middleware is correctly set up' - ) - } - // Always await detector call - works for both sync and async detectors - // (awaiting a non-promise value returns it immediately) - const locale = await localeDetector(ctx) - i18n.locale = locale - + const [locale, intlify] = await getLocaleAndIntlifyContext(ctx) function translate(key: string, ...args: unknown[]): string { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- NOTE(kazupon): generic type const [_, options] = parseTranslateArgs(key, ...args) @@ -254,7 +260,7 @@ export async function useTranslation< // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- NOTE(kazupon): generic type const result = Reflect.apply(_translate, null, [ - i18n, + intlify, key, arg2, { @@ -268,3 +274,28 @@ export async function useTranslation< return translate as TranslationFunction } + +/** + * get a locale which is detected with locale detector. + * + * @description The locale obtainable via this function comes from the locale detector specified in the `locale` option of the {@link defineI18nMiddleware}. + * + * @example + * ```js + * app.get( + * '/', + * async ctx => { + * const locale = await getDetectorLocale(ctx) + * return ctx.text(`Current Locale: ${locale.language}`) + * }, + * ) + * ``` + * + * @param ctx - A Hono context + * + * @returns Return a {@link Intl.Locale | locale} + */ +export async function getDetectorLocale(ctx: Context): Promise { + const [locale] = await getLocaleAndIntlifyContext(ctx) + return new Intl.Locale(locale) +} From 11d7c480e42fda1bca5639097cd6fabe31254753 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Fri, 21 Nov 2025 00:28:52 +0900 Subject: [PATCH 2/3] fix --- packages/h3/docs/functions/getDetectorLocale.md | 2 +- packages/h3/src/index.ts | 4 ++-- packages/hono/docs/functions/getDetectorLocale.md | 2 +- packages/hono/src/index.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/h3/docs/functions/getDetectorLocale.md b/packages/h3/docs/functions/getDetectorLocale.md index 8ae01aa..36a831d 100644 --- a/packages/h3/docs/functions/getDetectorLocale.md +++ b/packages/h3/docs/functions/getDetectorLocale.md @@ -22,7 +22,7 @@ get a locale which is detected with locale detector. `Promise`\<`Locale`\> -Return a Intl.Locale \| locale +Return an `Intl.Locale` instance representing the detected locale ## Description diff --git a/packages/h3/src/index.ts b/packages/h3/src/index.ts index 7f26ad1..3f099ea 100644 --- a/packages/h3/src/index.ts +++ b/packages/h3/src/index.ts @@ -273,7 +273,6 @@ async function getLocaleAndEventContext(event: H3Event): Promise<[string, H3Even // Always await detector call - works for both sync and async detectors // (awaiting a non-promise value returns it immediately) const locale = await localeDetector(event) - context[SYMBOL_I18N].locale = locale return [locale, context] } @@ -300,6 +299,7 @@ export async function useTranslation< Event extends H3Event = H3Event >(event: Event): Promise> { const [locale, context] = await getLocaleAndEventContext(event) + context[SYMBOL_I18N]!.locale = locale function translate(key: string, ...args: unknown[]): string { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- NOTE(kazupon): generic type const [_, options] = parseTranslateArgs(key, ...args) @@ -339,7 +339,7 @@ export async function useTranslation< * ``` * @param event - A H3 event * - * @returns Return a {@link Intl.Locale | locale} + * @returns Return an {@linkcode Intl.Locale} instance representing the detected locale */ export async function getDetectorLocale(event: H3Event): Promise { const result = await getLocaleAndEventContext(event) diff --git a/packages/hono/docs/functions/getDetectorLocale.md b/packages/hono/docs/functions/getDetectorLocale.md index a197b46..f49cd18 100644 --- a/packages/hono/docs/functions/getDetectorLocale.md +++ b/packages/hono/docs/functions/getDetectorLocale.md @@ -22,7 +22,7 @@ get a locale which is detected with locale detector. `Promise`\<`Locale`\> -Return a Intl.Locale \| locale +Return an `Intl.Locale` instance representing the detected locale ## Description diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts index 417174b..5fe0c40 100644 --- a/packages/hono/src/index.ts +++ b/packages/hono/src/index.ts @@ -211,7 +211,6 @@ async function getLocaleAndIntlifyContext(ctx: Context): Promise<[string, CoreCo // Always await detector call - works for both sync and async detectors // (awaiting a non-promise value returns it immediately) const locale = await localeDetector(ctx) - intlify.locale = locale return [locale, intlify] } @@ -253,6 +252,7 @@ export async function useTranslation< HonoContext extends Context = Context >(ctx: HonoContext): Promise> { const [locale, intlify] = await getLocaleAndIntlifyContext(ctx) + intlify.locale = locale function translate(key: string, ...args: unknown[]): string { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- NOTE(kazupon): generic type const [_, options] = parseTranslateArgs(key, ...args) @@ -293,7 +293,7 @@ export async function useTranslation< * * @param ctx - A Hono context * - * @returns Return a {@link Intl.Locale | locale} + * @returns Return an {@linkcode Intl.Locale} instance representing the detected locale */ export async function getDetectorLocale(ctx: Context): Promise { const [locale] = await getLocaleAndIntlifyContext(ctx) From df791215e6e1e77ef0827a27b8dc623cdee42100 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Fri, 21 Nov 2025 00:35:13 +0900 Subject: [PATCH 3/3] docs: fix --- packages/hono/docs/functions/getDetectorLocale.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hono/docs/functions/getDetectorLocale.md b/packages/hono/docs/functions/getDetectorLocale.md index f49cd18..9e0d31f 100644 --- a/packages/hono/docs/functions/getDetectorLocale.md +++ b/packages/hono/docs/functions/getDetectorLocale.md @@ -7,7 +7,7 @@ # Function: getDetectorLocale() ```ts -function getDetectorLocale(ctx): Promise; +function getDetectorLocale(ctx): Promise; ``` get a locale which is detected with locale detector. @@ -20,7 +20,7 @@ get a locale which is detected with locale detector. ## Returns -`Promise`\<`Locale`\> +`Promise`\<`Intl.Locale`\> Return an `Intl.Locale` instance representing the detected locale