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: integrate i18next middleware #622

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/violet-toes-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/i18next': major
---

This middleware adds a translation function (t) for internationalization (i18next) to the context of each request. This function allows you to translate text based on the language preference.
25 changes: 25 additions & 0 deletions .github/workflows/ci-i18next.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci-i18next
on:
push:
branches: [main]
paths:
- 'packages/i18next/**'
pull_request:
branches: ['*']
paths:
- 'packages/i18next/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/i18next
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
52 changes: 52 additions & 0 deletions packages/i18next/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# i18next middleware for Hono

## Installation

```bash
bun add @hono/i18next i18next
# or
npm install @hono/i18next i18next
# or
yarn add @hono/i18next i18next
```

## Usage

```ts
import { useI18next, type I18nextMiddlewareOptions } from '@hono/i18next'
import { Hono } from 'hono'

const app = new Hono()

const options: I18nextMiddlewareOptions = {
headerName: 'X-Localization',
// if true, it will set the language in the i18next instance
setLanguageChanges: false,
// i18next options:
fallbackLng: 'en',
returnNull: false,
nsSeparator: '.',
interpolation: { escapeValue: false },
resources: {
en: { translation: { hello: 'Hello!' } },
tr: { translation: { hello: 'Merhaba!' } },
},
}

app.use('*', useI18next(options))

app.get('/hello', (c) => {
const t = c.get('t')
return c.json({ message: t('hello') })
})

export default app
```

## Author

Onur Ozkaya <https://github.com/nrzky>

## License

MIT
1 change: 1 addition & 0 deletions packages/i18next/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../jest.config.js')
50 changes: 50 additions & 0 deletions packages/i18next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@hono/i18next",
"version": "1.0.0",
"description": "i18next middleware for Hono",
"type": "module",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "vitest --run",
"build": "tsup ./src/index.ts --format esm,cjs --dts",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"license": "MIT",
"author": "Onur Ozkaya <[email protected]> (https://github.com/nrzky)",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"hono": "*",
"i18next": "*"
},
"devDependencies": {
"hono": "^4.4.12",
"i18next": "^23.11.5",
"tsup": "^8.1.0",
"vitest": "^1.6.0"
}
}
90 changes: 90 additions & 0 deletions packages/i18next/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'

import { useI18next } from '../src'
import type { I18nextMiddlewareOptions } from '../src'

describe('i18next middleware', () => {
const en = { hello: 'Hello!', errMsg: 'Opps!' }
const tr = { hello: 'Merhaba!', errMsg: 'Hay Aksi!' }

const fallbackLng = 'en'

const requestHeaderName = 'X-Localization'

const options: I18nextMiddlewareOptions = {
headerName: requestHeaderName,
setLanguageChanges: false,
fallbackLng: fallbackLng,
returnNull: false,
nsSeparator: '.',
interpolation: { escapeValue: false },
resources: { en: { translation: en }, tr: { translation: tr } },
}

const app = new Hono()

app.use('*', useI18next(options))

app.get('/hello', (c) => {
const t = c.get('t')
return c.json({ message: t('hello') })
})

app.get('/error', (c) => {
const t = c.get('t')
throw new HTTPException(400, { message: t('errMsg') })
})

app.onError((err, c) => {
return c.json({ message: err.message }, err instanceof HTTPException ? err.status : 500)
})

it('Should return hello message in English', async () => {
const res = await app.request('http://localhost/hello', {
headers: { [requestHeaderName]: 'en' },
})

expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ message: en.hello })
})

it('Should return hello message in Turkish', async () => {
const res = await app.request('http://localhost/hello', {
headers: { [requestHeaderName]: 'tr' },
})

expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ message: tr.hello })
})

it('Should return hello message in fallback language when language not supported', async () => {
const res = await app.request('http://localhost/hello', {
headers: { [requestHeaderName]: 'fr' },
})

expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ message: en.hello })
})

it('Should return hello message in fallback language when language is undefined', async () => {
const res = await app.request('http://localhost/hello')

expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ message: en.hello })
})

it('Should return error message in English', async () => {
const res = await app.request('http://localhost/error', {
headers: { [requestHeaderName]: 'en' },
})

expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ message: en.errMsg })
})
})
42 changes: 42 additions & 0 deletions packages/i18next/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createMiddleware } from 'hono/factory'
import i18next from 'i18next'
import type { InitOptions, TFunction } from 'i18next'

declare module 'hono' {
interface ContextVariableMap {
t: TFunction
}
}

export interface I18nextMiddlewareOptions extends InitOptions {
headerName: string
setLanguageChanges?: boolean
}

const useI18next = ({
headerName,
setLanguageChanges = false,
...initOptions
}: I18nextMiddlewareOptions) => {
if (!i18next.isInitialized) {
i18next.init(initOptions)
}

return createMiddleware(async (c, next) => {
const language = c.req.header(headerName) || initOptions.lng

const t = (key: string, options?: object) => {
return i18next.t(key, { lng: language, ...options })
}

if (setLanguageChanges) {
await i18next.changeLanguage(language)
}

c.set('t', t)

await next()
})
}

export { i18next, useI18next }
8 changes: 8 additions & 0 deletions packages/i18next/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}
8 changes: 8 additions & 0 deletions packages/i18next/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
},
})
32 changes: 32 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,15 @@ __metadata:
languageName: node
linkType: hard

"@babel/runtime@npm:^7.23.2":
version: 7.24.7
resolution: "@babel/runtime@npm:7.24.7"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: b6fa3ec61a53402f3c1d75f4d808f48b35e0dfae0ec8e2bb5c6fc79fb95935da75766e0ca534d0f1c84871f6ae0d2ebdd950727cfadb745a2cdbef13faef5513
languageName: node
linkType: hard

"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3":
version: 7.22.15
resolution: "@babel/template@npm:7.22.15"
Expand Down Expand Up @@ -2113,6 +2122,20 @@ __metadata:
languageName: unknown
linkType: soft

"@hono/i18next@workspace:packages/i18next":
version: 0.0.0-use.local
resolution: "@hono/i18next@workspace:packages/i18next"
dependencies:
hono: "npm:^4.4.12"
i18next: "npm:^23.11.5"
tsup: "npm:^8.1.0"
vitest: "npm:^1.6.0"
peerDependencies:
hono: "*"
i18next: "*"
languageName: unknown
linkType: soft

"@hono/medley-router@workspace:packages/medley-router":
version: 0.0.0-use.local
resolution: "@hono/medley-router@workspace:packages/medley-router"
Expand Down Expand Up @@ -10116,6 +10139,15 @@ __metadata:
languageName: node
linkType: hard

"i18next@npm:^23.11.5":
version: 23.11.5
resolution: "i18next@npm:23.11.5"
dependencies:
"@babel/runtime": "npm:^7.23.2"
checksum: b0bec64250a3e529d4c51e2fc511406a85c5dde3d005d3aabe919551ca31dfc0a8f5490bf6e44649822e895a1fa91a58092d112367669cd11b2eb89e6ba90d1a
languageName: node
linkType: hard

"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
Expand Down
Loading