Skip to content
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
7 changes: 1 addition & 6 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,5 @@ export default {
}
},
ignore: ['**/src/**.test-d.ts'],
ignoreDependencies: [
'lint-staged',
'@vitest/coverage-v8',
'vitest-environment-miniflare',
'miniflare'
]
ignoreDependencies: ['lint-staged', '@vitest/coverage-v8']
} satisfies KnipConfig
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,10 @@
"eslint-plugin-yml": "^1.19.0",
"knip": "^5.69.1",
"lint-staged": "^16.2.6",
"miniflare": "^3.20231016.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.4",
"vitest": "^4.0.0",
"vitest-environment-miniflare": "^2.14.1"
"vitest": "^4.0.0"
},
"prettier": "@kazupon/prettier-config",
"lint-staged": {
Expand Down
7 changes: 3 additions & 4 deletions packages/h3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ app.get('/', event => {

### Translation

If you want to use translation, you need to install plugin. As a result, you can use `useTranslation` within the handler:

```ts
import { createServer } from 'node:http'
import { H3, toNodeListener } from 'h3'
Expand All @@ -84,6 +86,7 @@ import { plugin as i18n, detectLocaleFromAcceptLanguageHeader, useTranslation }
// install plugin with `H3` constructor
const app = new H3({
plugins: [
// configure plugin options
i18n({
// detect locale with `accept-language` header
locale: detectLocaleFromAcceptLanguageHeader,
Expand Down Expand Up @@ -295,10 +298,6 @@ If you are using [Visual Studio Code](https://code.visualstudio.com/) as an edit

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

Expand Down
2 changes: 1 addition & 1 deletion packages/h3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"@types/node": "catalog:",
"h3": "^2.0.1-rc.5",
"publint": "catalog:",
"srvx": "^0.9.6",
"srvx": "catalog:",
"tsdown": "catalog:",
"typedoc": "catalog:",
"typedoc-plugin-markdown": "catalog:",
Expand Down
48 changes: 40 additions & 8 deletions packages/hono/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ Internationalization middleware & utilities for [Hono](https://hono.dev/)

## 🌟 Features

✅️️ &nbsp;**Internationalization utilities:** support [internationalization
utils](https://github.com/intlify/srvmid/blob/main/packages/hono/docs/index.md) via [@intlify/utils](https://github.com/intlify/utils)

✅️ &nbsp;**Translation:** Simple API like
[vue-i18n](https://vue-i18n.intlify.dev/)

✅ &nbsp;**Custom locale detector:** You can implement your own locale detector
on server-side

✅️️ &nbsp;**Useful utilities:** support internationalization composables
utilities via [@intlify/utils](https://github.com/intlify/utils)

## 💿 Installation

```sh
Expand All @@ -35,6 +35,42 @@ bun add @intlify/hono

## 🚀 Usage

### Detect locale with utils

Detect locale from `accept-language` header:

```ts
import { Hono } from 'hono'
import { getHeaderLocale } from '@intlify/h3'

const app = new Hono()

app.get('/', c => {
// detect locale from HTTP header which has `Accept-Language: ja,en-US;q=0.7,en;q=0.3`
const locale = getHeaderLocale(c.req.raw)
return c.text(locale.toString())
})
```

Detect locale from URL query:

```ts
import { Hono } from 'hono'
import { getQueryLocale } from '@intlify/h3'

const app = new Hono()

app.get('/', c => {
// detect locale from query which has 'http://localhost:3000?locale=en'
const locale = getQueryLocale(c.req.raw)
return c.text(locale.toString())
})
```

### Translation

If you want to use translation, you need to install middleware. As a result, you can use `useTranslation` within the handler:

```ts
import { Hono } from 'hono'
import {
Expand Down Expand Up @@ -89,7 +125,7 @@ const DEFAULT_LOCALE = 'en'
// define custom locale detector
const localeDetector = (ctx: Context): string => {
try {
return getQueryLocale(ctx).toString()
return getQueryLocale(ctx.req.raw).toString()
} catch {
return DEFAULT_LOCALE
}
Expand Down Expand Up @@ -173,10 +209,6 @@ If you are using [Visual Studio Code](https://code.visualstudio.com/) as an edit

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

Expand Down
9 changes: 4 additions & 5 deletions packages/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,21 @@
"scripts": {
"build": "tsdown",
"build:docs": "typedoc --excludeInternal",
"play:basic": "wrangler dev ./playground/basic/index.ts",
"play:basic": "node ./playground/basic/index.ts",
"prepack": "pnpm build"
},
"dependencies": {
"@intlify/core": "^11.0.0",
"@intlify/utils": "catalog:"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20231016.0",
"@types/node": "catalog:",
"hono": "^3.8.1",
"hono": "^4.10.6",
"publint": "catalog:",
"srvx": "catalog:",
"tsdown": "catalog:",
"typedoc": "catalog:",
"typedoc-plugin-markdown": "catalog:",
"typescript": "catalog:",
"wrangler": "^3.6.0"
"typescript": "catalog:"
}
}
10 changes: 8 additions & 2 deletions packages/hono/playground/basic/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Hono } from 'hono'
import { serve } from 'srvx'
import {
defineI18nMiddleware,
detectLocaleFromAcceptLanguageHeader,
Expand All @@ -17,11 +18,16 @@ const i18n = defineI18nMiddleware({
}
})

const app = new Hono()
const app: Hono = new Hono()
app.use('*', i18n)
app.get('/', c => {
const t = useTranslation(c)
return c.text(t('hello', { name: 'hono' }) + `\n`)
})

export default app
const server = serve({
port: 3000,
fetch: app.fetch
})

await server.ready()
2 changes: 1 addition & 1 deletion packages/hono/playground/global-schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const i18n = defineI18nMiddleware({
}
})

const app = new Hono()
const app: Hono = new Hono()
app.use('*', i18n)
app.get('/', c => {
const t = useTranslation(c)
Expand Down
2 changes: 1 addition & 1 deletion packages/hono/playground/local-schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const i18n = defineI18nMiddleware({
}
})

const app = new Hono()
const app: Hono = new Hono()
app.use('*', i18n)
app.get('/', c => {
type ResourceSchema = {
Expand Down
4 changes: 2 additions & 2 deletions packages/hono/playground/typesafe-schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// in your project, `import { ... } from '@inlify/hono'`
import { defineI18nMiddleware } from '../../src/index.ts'
import { Hono } from 'hono'
import { defineI18nMiddleware } from '../../src/index.ts'

// define resource schema
type ResourceSchema = {
Expand All @@ -20,7 +20,7 @@ const i18n = defineI18nMiddleware<[ResourceSchema], 'en' | 'ja'>({
// ...
})

const app = new Hono()
const app: Hono = new Hono()
app.use('*', i18n)
// something your implementation code ...
// ...
Expand Down
17 changes: 17 additions & 0 deletions packages/hono/playground/util-header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Hono } from 'hono'
import { serve } from 'srvx'
import { getHeaderLocale } from '../../src/index.ts' // `@intlify/hono`

const app = new Hono()

app.get('/', c => {
const locale = getHeaderLocale(c.req.raw)
return c.text(locale.toString())
})

const server = serve({
port: 3000,
fetch: app.fetch
})

await server.ready()
17 changes: 17 additions & 0 deletions packages/hono/playground/util-query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Hono } from 'hono'
import { serve } from 'srvx'
import { getQueryLocale } from '../../src/index.ts' // `@intlify/hono`

const app = new Hono()

app.get('/', c => {
const locale = getQueryLocale(c.req.raw)
return c.text(locale.toString())
})

const server = serve({
port: 3000,
fetch: app.fetch
})

await server.ready()
59 changes: 59 additions & 0 deletions packages/hono/spec/e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { exec, spawn } from 'node:child_process'
import path from 'node:path'
import { afterEach, describe, expect, test } from 'vitest'

import type { ExecOptions } from 'node:child_process'

export function runCommand(command: string, options?: ExecOptions): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ timeout: 30_000, ...options, env: { ...process.env, ...options?.env } },
(error, stdout, stderr) => {
if (error) {
reject(new Error(`Command failed: ${command}\n${stderr}\n${error.message}`))
} else {
resolve(stdout.toString())
}
}
)
})
}

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

let serve: ReturnType<typeof spawn> | null = null

afterEach(async () => {
serve?.kill()
})

describe('e2e', () => {
test('util-header', async () => {
const target = path.resolve(import.meta.dirname, '../playground/util-header/index.ts')
serve = spawn('pnpx', ['tsx', target])
await delay(2000) // wait for server to start
const stdout = await runCommand(
`curl -H 'Accept-Language: ja,en-US;q=0.7,en;q=0.3' http://localhost:3000`
)
expect(stdout).toContain(`ja`)
})
Comment on lines +32 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded delay makes tests brittle.

The 2-second delay assumes the server will be ready, but this is not guaranteed on slower systems or under load. The test could fail intermittently or waste time waiting longer than necessary.

Consider polling the server endpoint until it responds or times out:

async function waitForServer(url: string, timeout = 10000): Promise<void> {
  const start = Date.now()
  while (Date.now() - start < timeout) {
    try {
      await fetch(url)
      return
    } catch {
      await delay(100)
    }
  }
  throw new Error(`Server at ${url} did not become ready within ${timeout}ms`)
}

Then replace the delay with:

 serve = spawn('pnpx', ['tsx', target])
-await delay(2000) // wait for server to start
+await waitForServer('http://localhost:3000')
🤖 Prompt for AI Agents
In packages/hono/spec/e2e.spec.ts around lines 32–40, the test uses a hardcoded
2s delay which is brittle; replace the fixed delay with a polling helper that
repeatedly attempts the endpoint until it responds or a timeout elapses, then
call that helper (e.g., waitForServer('http://localhost:3000', timeout)) before
running the curl assertion; implement the helper to loop with short sleeps, try
a simple fetch/HTTP request each iteration, return when successful and throw on
timeout, and remove the fixed delay so the test proceeds as soon as the server
is ready.


test('util-query', async () => {
const target = path.resolve(import.meta.dirname, '../playground/util-query/index.ts')
serve = spawn('pnpx', ['tsx', target])
await delay(2000) // wait for server to start
const stdout = await runCommand(`curl http://localhost:3000?locale=en`)
expect(stdout).toContain(`en`)
})

test('translation', async () => {
const target = path.resolve(import.meta.dirname, '../playground/basic/index.ts')
serve = spawn('pnpx', ['tsx', target])
await delay(2000) // wait for server to start
const stdout = await runCommand(
`curl -H 'Accept-Language: ja,en-US;q=0.7,en;q=0.3' http://localhost:3000`
)
expect(stdout).toContain(`こんにちは, h3`)
})
})
5 changes: 2 additions & 3 deletions packages/hono/spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// @vitest-environment miniflare
import { getQueryLocale } from '@intlify/utils/hono'
import { Hono } from 'hono'
import { afterEach, expect, test, vi } from 'vitest'
import {
defineI18nMiddleware,
detectLocaleFromAcceptLanguageHeader,
getQueryLocale,
useTranslation
} from '../src/index.ts'

Expand Down Expand Up @@ -49,7 +48,7 @@ test('custom locale detection', async () => {
// define custom locale detector
const localeDetector = (ctx: Context): string => {
try {
return getQueryLocale(ctx).toString()
return getQueryLocale(ctx.req.raw).toString()
} catch {
return defaultLocale
}
Expand Down
1 change: 0 additions & 1 deletion packages/hono/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @vitest-environment miniflare
import { createCoreContext } from '@intlify/core'
import { describe, expect, test } from 'vitest'

Expand Down
4 changes: 2 additions & 2 deletions packages/hono/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import type { Context, MiddlewareHandler, Next } from 'hono'

declare module 'hono' {
interface ContextVariableMap {
i18n: CoreContext
i18n?: CoreContext
}
}

Expand Down Expand Up @@ -149,7 +149,7 @@ export function defineI18nMiddleware<

return async (ctx: Context, next: Next) => {
i18n.locale = getLocaleDetector(ctx)
ctx.set('i18n', i18n)
ctx.set('i18n', i18n as CoreContext)

await next()

Expand Down
Loading
Loading