├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── stale.yml
└── workflows
│ ├── deno.yml
│ └── node.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── example
├── basic-locize
│ ├── README.md
│ ├── index.js
│ └── package.json
├── basic-pug
│ ├── README.md
│ ├── index.js
│ ├── locales
│ │ ├── de
│ │ │ └── translation.json
│ │ └── en
│ │ │ └── translation.json
│ ├── package.json
│ └── views
│ │ └── index.pug
├── basic
│ ├── README.md
│ ├── index.js
│ ├── locales
│ │ ├── de
│ │ │ └── translation.json
│ │ ├── en
│ │ │ └── translation.json
│ │ └── fr
│ │ │ └── translation.json
│ └── package.json
├── deno
│ ├── i18n.js
│ ├── index.js
│ ├── locales
│ │ ├── de
│ │ │ └── translation.json
│ │ ├── en
│ │ │ └── translation.json
│ │ └── it
│ │ │ └── translation.json
│ └── views
│ │ └── index.html
├── fastify-pov
│ ├── README.md
│ ├── index.js
│ ├── locales
│ │ ├── de
│ │ │ └── translation.json
│ │ └── en
│ │ │ └── translation.json
│ ├── package.json
│ └── views
│ │ └── index.pug
├── fastify-pug
│ ├── README.md
│ ├── index.js
│ ├── locales
│ │ ├── de
│ │ │ └── translation.json
│ │ └── en
│ │ │ └── translation.json
│ ├── package.json
│ └── views
│ │ └── index.pug
├── fastify
│ ├── README.md
│ ├── index.js
│ ├── locales
│ │ ├── de
│ │ │ └── translation.json
│ │ └── en
│ │ │ └── translation.json
│ └── package.json
└── koa
│ ├── README.md
│ ├── index.js
│ ├── locales
│ ├── de
│ │ └── translation.json
│ ├── en
│ │ └── translation.json
│ └── fr
│ │ └── translation.json
│ └── package.json
├── index.d.mts
├── index.d.ts
├── index.js
├── lib
├── LanguageDetector.js
├── httpFunctions.js
├── index.js
├── languageLookups
│ ├── cookie.js
│ ├── header.js
│ ├── path.js
│ ├── querystring.js
│ └── session.js
└── utils.js
├── licence
├── package.json
├── test
├── addRoute.custom.js
├── addRoute.express.js
├── addRoute.fastify.js
├── addRoute.koa.js
├── deno
│ ├── addRoute.abc.js
│ ├── getResourcesHandler.abc.js
│ ├── middleware.abc.js
│ └── missingKeyHandler.abc.js
├── getResourcesHandler.custom.js
├── getResourcesHandler.express.js
├── getResourcesHandler.fastify.js
├── getResourcesHandler.koa.js
├── languageDetector.js
├── middleware.custom.js
├── middleware.express.js
├── middleware.fastify.js
├── middleware.hapi.js
├── middleware.koa.js
├── missingKeyHandler.custom.js
├── missingKeyHandler.express.js
├── missingKeyHandler.fastify.js
├── missingKeyHandler.hapi.js
├── missingKeyHandler.koa.js
└── types
│ └── index.test-d.ts
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "esm": {
4 | "presets": [
5 | ["@babel/preset-env", {
6 | "modules": false,
7 | "targets": {
8 | "esmodules": true,
9 | "ie": 11
10 | }
11 | }]
12 | ]
13 | },
14 | "cjs": {
15 | "plugins": ["add-module-exports"],
16 | "presets": [
17 | ["@babel/preset-env", {
18 | "targets": {
19 | "ie": 11
20 | }
21 | }]
22 | ]
23 | }
24 | },
25 | "comments": false
26 | }
27 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 | root = true
3 |
4 | [*.{js,jsx,json}]
5 | end_of_line = lf
6 | insert_final_newline = true
7 | charset = utf-8
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /build
2 | /node_modules
3 | /cjs
4 | /esm
5 | /dist
6 | /example
7 | /test/deno
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "standard",
4 | "plugin:require-path-exists/recommended"
5 | ],
6 | "plugins": [
7 | "require-path-exists"
8 | ],
9 | "globals": {
10 | "describe": false,
11 | "it": false,
12 | "before": false,
13 | "after": false,
14 | "beforeEach": false,
15 | "afterEach": false
16 | },
17 | "rules": {
18 | "array-bracket-spacing": 0,
19 | "standard/no-callback-literal": 0
20 | }
21 | }
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 7
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - "discussion"
8 | - "feature request"
9 | - "bug"
10 | - "breaking change"
11 | - "doc"
12 | - "issue"
13 | - "help wanted"
14 | - "good first issue"
15 | # Label to use when marking an issue as stale
16 | staleLabel: stale
17 | # Comment to post when marking an issue as stale. Set to `false` to disable
18 | markComment: >
19 | This issue has been automatically marked as stale because it has not had
20 | recent activity. It will be closed if no further activity occurs. Thank you
21 | for your contributions.
22 | # Comment to post when closing a stale issue. Set to `false` to disable
23 | closeComment: false
24 |
--------------------------------------------------------------------------------
/.github/workflows/deno.yml:
--------------------------------------------------------------------------------
1 | name: deno
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'docs/**'
7 | - '*.md'
8 | pull_request:
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 |
13 | jobs:
14 | test:
15 | name: Test on deno ${{ matrix.deno }} and ${{ matrix.os }}
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | deno: [ '2.x' ]
20 | # os: [ubuntu-latest, windows-latest, macOS-latest]
21 | os: [ubuntu-latest]
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Setup Deno
25 | uses: denolib/setup-deno@master
26 | with:
27 | deno-version: ${{ matrix.deno }}
28 | - run: deno --version
29 | - run: deno test --allow-net test/deno/*.js
30 |
--------------------------------------------------------------------------------
/.github/workflows/node.yml:
--------------------------------------------------------------------------------
1 | name: node
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'docs/**'
7 | - '*.md'
8 | pull_request:
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 |
13 | jobs:
14 | test:
15 | name: Test on node ${{ matrix.node }} and ${{ matrix.os }}
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | node: [ '22.x', '20.x', '18.x' ]
20 | # os: [ubuntu-latest, windows-latest, macOS-latest]
21 | os: [ubuntu-latest]
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Setup Node.js
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: ${{ matrix.node }}
28 | - run: npm install
29 | - run: npm test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 | cjs
6 | esm
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | test
3 | dist
4 | .gitignore
5 | .editorconfig
6 | .eslintignore
7 | .eslintrc
8 | .travis.yml
9 | .github
10 | package-lock.json
11 | README.md
12 | CHANGELOG.md
13 | tsconfig.json
14 | example
15 | .babelrc
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "camelcase": false,
5 | "new-cap": false,
6 | "singleQuote": true,
7 | "tabWidth": 2,
8 | "trailingComma": "none",
9 | "printWidth": 80
10 | }
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - '12'
5 | - '14'
6 | branches:
7 | only:
8 | - master
9 | notifications:
10 | email:
11 | - adriano@raiano.ch
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [v3.7.4](https://github.com/i18next/i18next-http-middleware/compare/v3.7.3...v3.7.4)
2 | - check for common xss attack patterns on detected language
3 |
4 | ## [v3.7.3](https://github.com/i18next/i18next-http-middleware/compare/v3.7.2...v3.7.3)
5 | - add shortcut for resolvedLanguage
6 |
7 | ## [v3.7.2](https://github.com/i18next/i18next-http-middleware/compare/v3.7.1...v3.7.2)
8 | - fix: Fastify no longer complains about the type [#83](https://github.com/i18next/i18next-http-middleware/pull/83)
9 |
10 | ## [v3.7.1](https://github.com/i18next/i18next-http-middleware/compare/v3.7.0...v3.7.1)
11 | - Just to be sure, sanitize the Content-Language response header. (Eventhough there is no known/reproducible vulnerability yet #80)
12 |
13 | ## [v3.7.0](https://github.com/i18next/i18next-http-middleware/compare/v3.6.0...v3.7.0)
14 | - support i18next v24
15 |
16 | ## [v3.6.0](https://github.com/i18next/i18next-http-middleware/compare/v3.5.0...v3.6.0)
17 | - introduce convertDetectedLanguage option
18 |
19 | ## [v3.5.0](https://github.com/i18next/i18next-http-middleware/compare/v3.4.1...v3.5.0)
20 | - fix: separate cjs and mjs typings
21 |
22 | ## [v3.4.1](https://github.com/i18next/i18next-http-middleware/compare/v3.4.0...v3.4.1)
23 | - fix(languageDetector): handle es-419 special case [#65](https://github.com/i18next/i18next-http-middleware/pull/65)
24 |
25 | ## [v3.4.0](https://github.com/i18next/i18next-http-middleware/compare/v3.3.2...v3.4.0)
26 | - support koa [#61](https://github.com/i18next/i18next-http-middleware/issues/61)
27 |
28 | ## [v3.3.2](https://github.com/i18next/i18next-http-middleware/compare/v3.3.1...v3.3.2)
29 | - deno: oak fix [#59](https://github.com/i18next/i18next-http-middleware/issues/59)
30 |
31 | ## [v3.3.1](https://github.com/i18next/i18next-http-middleware/compare/v3.3.0...v3.3.1)
32 | - Support TS5 types exports
33 |
34 | ## [v3.3.0](https://github.com/i18next/i18next-http-middleware/compare/v3.2.2...v3.3.0)
35 | - fallback in getResourcesHandler to check also in route params
36 |
37 | ## [v3.2.2](https://github.com/i18next/i18next-http-middleware/compare/v3.2.1...v3.2.2)
38 | - optimize header based language detection for 3 char languages [#54](https://github.com/i18next/i18next-http-middleware/issues/54)
39 |
40 | ## [v3.2.1](https://github.com/i18next/i18next-http-middleware/compare/v3.2.0...v3.2.1)
41 | - fix issue missing i18n.t and t functions before authenticating [#52](https://github.com/i18next/i18next-http-middleware/pull/52)
42 |
43 | ## [v3.2.0](https://github.com/i18next/i18next-http-middleware/compare/v3.1.6...v3.2.0)
44 | - HapiJs support [#50](https://github.com/i18next/i18next-http-middleware/pull/50)
45 |
46 | ## [v3.1.6](https://github.com/i18next/i18next-http-middleware/compare/v3.1.5...v3.1.6)
47 | - add Fastify type support [#47](https://github.com/i18next/i18next-http-middleware/pull/47)
48 |
49 | ## [v3.1.5](https://github.com/i18next/i18next-http-middleware/compare/v3.1.4...v3.1.5)
50 | - fallbackLng option can also be a function
51 |
52 | ## [v3.1.4](https://github.com/i18next/i18next-http-middleware/compare/v3.1.3...v3.1.4)
53 | - fix for [point-of-view](https://github.com/fastify/point-of-view)
54 | - update all dependencies
55 |
56 | ## [v3.1.3](https://github.com/i18next/i18next-http-middleware/compare/v3.1.2...v3.1.3)
57 | - optimize getQuery() function to check if req.query is iterable
58 |
59 | ## [v3.1.2](https://github.com/i18next/i18next-http-middleware/compare/v3.1.1...v3.1.2)
60 | - fix the type of the lookup method [#37](https://github.com/i18next/i18next-http-middleware/pull/37)
61 |
62 | ## [v3.1.1](https://github.com/i18next/i18next-http-middleware/compare/v3.1.0...v3.1.1)
63 | - make sure no undefined language is detected
64 |
65 | ## [v3.1.0](https://github.com/i18next/i18next-http-middleware/compare/v3.0.6...v3.1.0)
66 | - added types
67 |
68 | ## [v3.0.6](https://github.com/i18next/i18next-http-middleware/compare/v3.0.5...v3.0.6)
69 | - ignoreCase option true by default
70 |
71 | ## [v3.0.5](https://github.com/i18next/i18next-http-middleware/compare/v3.0.4...v3.0.5)
72 | - introduce ignoreCase option
73 |
74 | ## [v3.0.4](https://github.com/i18next/i18next-http-middleware/compare/v3.0.3...v3.0.4)
75 | - fix language detection algorithm to handle fallbackLng correctly
76 |
77 | ## [v3.0.3](https://github.com/i18next/i18next-http-middleware/compare/v3.0.2...v3.0.3)
78 | - prevent URIError with cookie language detector
79 |
80 | ## [v3.0.2](https://github.com/i18next/i18next-http-middleware/compare/v3.0.1...v3.0.2)
81 | - transpile also esm
82 |
83 | ## [v3.0.1](https://github.com/i18next/i18next-http-middleware/compare/v3.0.0...v3.0.1)
84 | - introduce lookupHeaderRegex option
85 |
86 | ## [v3.0.0](https://github.com/i18next/i18next-http-middleware/compare/v2.1.2...v3.0.0)
87 | - **BREAKING** needs i18next >= 19.5.0
88 | - let i18next figure out which detected lng is best match
89 |
90 | ## [v2.1.2](https://github.com/i18next/i18next-http-middleware/compare/v2.1.1...v2.1.2)
91 | - fix: get whitelist from correct property
92 |
93 | ## [v2.1.1](https://github.com/i18next/i18next-http-middleware/compare/v2.1.0...v2.1.1)
94 | - extend httpFunctions getQuery to handle some edge cases
95 |
96 | ## [v2.1.0](https://github.com/i18next/i18next-http-middleware/compare/v2.0.0...v2.1.0)
97 | - LanguageDetector: cookie, new option: cookiePath
98 |
99 | ## [v2.0.0](https://github.com/i18next/i18next-http-middleware/compare/v1.3.1...v2.0.0)
100 | - potentially BREAKING: change cookie defaults: cookieSameSite: 'strict'
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | [](https://github.com/i18next/i18next-http-middleware/actions?query=workflow%3Anode)
4 | [](https://github.com/i18next/i18next-http-middleware/actions?query=workflow%3Adeno)
5 | [](https://travis-ci.org/i18next/i18next-http-middleware)
6 | [](https://www.npmjs.com/package/i18next-http-middleware)
7 |
8 | This is a middleware to be used with Node.js web frameworks like express or Fastify and also for Deno.
9 |
10 | It's based on the deprecated [i18next-express-middleware](https://github.com/i18next/i18next-express-middleware) and can be used as a drop-in replacement.
11 | _It's not bound to a specific http framework anymore._
12 |
13 | ## Advice:
14 |
15 | To get started with server side internationalization, you may also have a look at [this blog post](https://dev.to/adrai/how-does-server-side-internationalization-i18n-look-like-5f4c) also using [using i18next-http-middleware](https://dev.to/adrai/how-does-server-side-internationalization-i18n-look-like-5f4c#ssr).
16 |
17 | # Getting started
18 |
19 | ```bash
20 | # npm package
21 | $ npm install i18next-http-middleware
22 | ```
23 |
24 | ## wire up i18next to request object
25 |
26 | ```js
27 | var i18next = require('i18next')
28 | var middleware = require('i18next-http-middleware')
29 | var express = require('express')
30 |
31 | i18next.use(middleware.LanguageDetector).init({
32 | preload: ['en', 'de', 'it'],
33 | ...otherOptions
34 | })
35 |
36 | var app = express()
37 | app.use(
38 | middleware.handle(i18next, {
39 | ignoreRoutes: ['/foo'], // or function(req, res, options, i18next) { /* return true to ignore */ }
40 | removeLngFromUrl: false // removes the language from the url when language detected in path
41 | })
42 | )
43 |
44 | // in your request handler
45 | app.get('myRoute', (req, res) => {
46 | var resolvedLng = req.resolvedLanguage // 'de-CH'
47 | var lng = req.language // 'de-CH'
48 | var lngs = req.languages // ['de-CH', 'de', 'en']
49 | req.i18n.changeLanguage('en') // will not load that!!! assert it was preloaded
50 |
51 | var exists = req.i18n.exists('myKey')
52 | var translation = req.t('myKey')
53 | })
54 |
55 | // in your views, eg. in pug (ex. jade)
56 | div = t('myKey')
57 | ```
58 |
59 | ### Fastify usage
60 |
61 | ```js
62 | var i18next = require('i18next')
63 | var middleware = require('i18next-http-middleware')
64 | var fastify = require('fastify')
65 |
66 | i18next.use(middleware.LanguageDetector).init({
67 | preload: ['en', 'de', 'it'],
68 | ...otherOptions
69 | })
70 |
71 | var app = fastify()
72 | app.register(i18nextMiddleware.plugin, {
73 | i18next,
74 | ignoreRoutes: ['/foo'] // or function(req, res, options, i18next) { /* return true to ignore */ }
75 | })
76 | // or
77 | // app.addHook('preHandler', i18nextMiddleware.handle(i18next, {
78 | // ignoreRoutes: ['/foo'] // or function(req, res, options, i18next) { /* return true to ignore */ }
79 | // }))
80 |
81 | // in your request handler
82 | app.get('myRoute', (request, reply) => {
83 | var lng = request.language // 'de-CH'
84 | var lngs = v.languages // ['de-CH', 'de', 'en']
85 | request.i18n.changeLanguage('en') // will not load that!!! assert it was preloaded
86 |
87 | var exists = request.i18n.exists('myKey')
88 | var translation = request.t('myKey')
89 | })
90 | ```
91 |
92 | ### Hapi usage
93 |
94 | ```js
95 | const i18next = require('i18next')
96 | const middleware = require('i18next-http-middleware')
97 | const Hapi = require('@hapi/hapi')
98 |
99 | i18next.use(middleware.LanguageDetector).init({
100 | preload: ['en', 'de', 'it'],
101 | ...otherOptions
102 | })
103 |
104 | const server = Hapi.server({
105 | port: port,
106 | host: '0.0.0.0',
107 |
108 | await server.register({
109 | plugin: i18nextMiddleware.hapiPlugin,
110 | options: {
111 | i18next,
112 | ignoreRoutes: ['/foo'] // or function(req, res, options, i18next) { /* return true to ignore
113 | }
114 | })
115 |
116 | // in your request handler
117 | server.route({
118 | method: 'GET',
119 | path: '/myRoute',
120 | handler: (request, h) => {
121 | var resolvedLng = request.resolvedLanguage // 'de-CH'
122 | var lng = request.language // 'de-CH'
123 | var lngs = v.languages // ['de-CH', 'de', 'en']
124 | request.i18n.changeLanguage('en') // will not load that!!! assert it was preloaded
125 |
126 | var exists = request.i18n.exists('myKey')
127 | var translation = request.t('myKey')
128 | }
129 | })
130 |
131 | ```
132 |
133 | ### Koa usage
134 |
135 | ```js
136 | var i18next = require('i18next')
137 | var middleware = require('i18next-http-middleware')
138 | const Koa = require('koa')
139 | const router = require('@koa/router')()
140 |
141 | i18next.use(middleware.LanguageDetector).init({
142 | preload: ['en', 'de', 'it'],
143 | ...otherOptions
144 | })
145 |
146 | var app = new Koa()
147 | app.use(i18nextMiddleware.koaPlugin(i18next, {
148 | ignoreRoutes: ['/foo'] // or function(req, res, options, i18next) { /* return true to ignore */ }
149 | }))
150 |
151 | // in your request handler
152 | router.get('/myRoute', ctx => {
153 | ctx.body = JSON.stringify({
154 | 'ctx.resolvedLanguage': ctx.resolvedLanguage,
155 | 'ctx.language': ctx.language,
156 | 'ctx.i18n.resolvedLanguage': ctx.i18n.resolvedLanguage,
157 | 'ctx.i18n.language': ctx.i18n.language,
158 | 'ctx.i18n.languages': ctx.i18n.languages,
159 | 'ctx.i18n.languages[0]': ctx.i18n.languages[0],
160 | 'ctx.t("home.title")': ctx.t('home.title')
161 | }, null, 2)
162 | })
163 | ```
164 |
165 | ### Deno usage
166 |
167 | #### abc
168 |
169 | ```js
170 | import i18next from 'https://deno.land/x/i18next/index.js'
171 | import Backend from 'https://cdn.jsdelivr.net/gh/i18next/i18next-fs-backend/index.js'
172 | import i18nextMiddleware from 'https://deno.land/x/i18next_http_middleware/index.js'
173 | import { Application } from 'https://deno.land/x/abc/mod.ts'
174 | import { config } from 'https://deno.land/x/dotenv/dotenv.ts'
175 |
176 | i18next
177 | .use(Backend)
178 | .use(i18nextMiddleware.LanguageDetector)
179 | .init({
180 | // debug: true,
181 | backend: {
182 | // eslint-disable-next-line no-path-concat
183 | loadPath: 'locales/{{lng}}/{{ns}}.json',
184 | // eslint-disable-next-line no-path-concat
185 | addPath: 'locales/{{lng}}/{{ns}}.missing.json'
186 | },
187 | fallbackLng: 'en',
188 | preload: ['en', 'de']
189 | })
190 |
191 | const port = config.PORT || 8080
192 | const app = new Application()
193 | const handle = i18nextMiddleware.handle(i18next)
194 | app.use((next) => (c) => {
195 | handle(c.request, c.response, () => {})
196 | return next(c)
197 | })
198 | app.get('/', (c) => c.request.t('home.title'))
199 | await app.start({ port })
200 | ```
201 |
202 | #### ServestJS
203 |
204 | ```js
205 | import i18next from 'https://deno.land/x/i18next/index.js'
206 | import Backend from 'https://cdn.jsdelivr.net/gh/i18next/i18next-fs-backend/index.js'
207 | import i18nextMiddleware from 'https://deno.land/x/i18next_http_middleware/index.js'
208 | import { createApp } from 'https://servestjs.org/@v1.0.0-rc2/mod.ts'
209 | import { config } from 'https://deno.land/x/dotenv/dotenv.ts'
210 |
211 | i18next
212 | .use(Backend)
213 | .use(i18nextMiddleware.LanguageDetector)
214 | .init({
215 | // debug: true,
216 | backend: {
217 | // eslint-disable-next-line no-path-concat
218 | loadPath: 'locales/{{lng}}/{{ns}}.json',
219 | // eslint-disable-next-line no-path-concat
220 | addPath: 'locales/{{lng}}/{{ns}}.missing.json'
221 | },
222 | fallbackLng: 'en',
223 | preload: ['en', 'de']
224 | })
225 |
226 | const port = config.PORT || 8080
227 | const app = createApp()
228 | app.use(i18nextMiddleware.handle(i18next))
229 | app.get('/', async (req) => {
230 | await req.respond({
231 | status: 200,
232 | headers: new Headers({
233 | 'content-type': 'text/plain'
234 | }),
235 | body: req.t('home.title')
236 | })
237 | })
238 | await app.listen({ port })
239 | ```
240 |
241 | ## add routes
242 |
243 | ```js
244 | // missing keys make sure the body is parsed (i.e. with [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions))
245 | app.post('/locales/add/:lng/:ns', middleware.missingKeyHandler(i18next))
246 | // addPath for client: http://localhost:8080/locales/add/{{lng}}/{{ns}}
247 |
248 | // multiload backend route
249 | app.get('/locales/resources.json', middleware.getResourcesHandler(i18next))
250 | // can be used like:
251 | // GET /locales/resources.json
252 | // GET /locales/resources.json?lng=en
253 | // GET /locales/resources.json?lng=en&ns=translation
254 |
255 | // serve translations:
256 | app.use('/locales', express.static('locales'))
257 | // GET /locales/en/translation.json
258 | // loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}.json
259 |
260 | // or instead of static
261 | app.get('/locales/:lng/:ns', middleware.getResourcesHandler(i18next))
262 | // GET /locales/en/translation
263 | // loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}
264 |
265 | app.get('/locales/:lng/:ns', middleware.getResourcesHandler(i18next, {
266 | maxAge: 60 * 60 * 24 * 30, // adds appropriate cache header if cache option is passed or NODE_ENV === 'production', defaults to 30 days
267 | cache: true // defaults to false
268 | }))
269 | ```
270 |
271 | ## add localized routes
272 |
273 | You can add your routes directly to the express app
274 |
275 | ```js
276 | var express = require('express'),
277 | app = express(),
278 | i18next = require('i18next'),
279 | FilesystemBackend = require('i18next-fs-backend'),
280 | i18nextMiddleware = require('i18next-http-middleware'),
281 | port = 3000
282 |
283 | i18next
284 | .use(i18nextMiddleware.LanguageDetector)
285 | .use(FilesystemBackend)
286 | .init({ preload: ['en', 'de', 'it'], ...otherOptions }, () => {
287 | i18nextMiddleware.addRoute(
288 | i18next,
289 | '/:lng/key-to-translate',
290 | ['en', 'de', 'it'],
291 | app,
292 | 'get',
293 | (req, res) => {
294 | //endpoint function
295 | }
296 | )
297 | })
298 | app.use(i18nextMiddleware.handle(i18next))
299 | app.listen(port, () => {
300 | console.log('Server listening on port', port)
301 | })
302 | ```
303 |
304 | or to an express router
305 |
306 | ```js
307 | var express = require('express'),
308 | app = express(),
309 | i18next = require('i18next'),
310 | FilesystemBackend = require('i18next-fs-backend'),
311 | i18nextMiddleware = require('i18next-http-middleware'),
312 | router = require('express').Router(),
313 | port = 3000
314 |
315 | i18next
316 | .use(i18nextMiddleware.LanguageDetector)
317 | .use(FilesystemBackend)
318 | .init({ preload: ['en', 'de', 'it'], ...otherOptions }, () => {
319 | i18nextMiddleware.addRoute(
320 | i18next,
321 | '/:lng/key-to-translate',
322 | ['en', 'de', 'it'],
323 | router,
324 | 'get',
325 | (req, res) => {
326 | //endpoint function
327 | }
328 | )
329 | app.use('/', router)
330 | })
331 | app.use(i18nextMiddleware.handle(i18next))
332 | app.listen(port, () => {
333 | console.log('Server listening on port', port)
334 | })
335 | ```
336 |
337 | ## custom http server
338 |
339 | Define your own functions to handle your custom request or response
340 |
341 | ```js
342 | middleware.handle(i18next, {
343 | getPath: (req) => req.path,
344 | getUrl: (req) => req.url,
345 | setUrl: (req, url) => (req.url = url),
346 | getQuery: (req) => req.query,
347 | getParams: (req) => req.params,
348 | getBody: (req) => req.body,
349 | setHeader: (res, name, value) => res.setHeader(name, value),
350 | setContentType: (res, type) => res.contentType(type),
351 | setStatus: (res, code) => res.status(code),
352 | send: (res, body) => res.send(body)
353 | })
354 | ```
355 |
356 | ## language detection
357 |
358 | Detects user language from current request. Comes with support for:
359 |
360 | - path
361 | - cookie
362 | - header
363 | - querystring
364 | - session
365 |
366 | Based on the i18next language detection handling: https://www.i18next.com/misc/creating-own-plugins#languagedetector
367 |
368 | Wiring up:
369 |
370 | ```js
371 | var i18next = require('i18next')
372 | var middleware = require('i18next-http-middleware')
373 |
374 | i18next.use(middleware.LanguageDetector).init(i18nextOptions)
375 | ```
376 |
377 | As with all modules you can either pass the constructor function (class) to the i18next.use or a concrete instance.
378 |
379 | ## Detector Options
380 |
381 | ```js
382 | {
383 | // order and from where user language should be detected
384 | order: [/*'path', 'session', */ 'querystring', 'cookie', 'header'],
385 |
386 | // keys or params to lookup language from
387 | lookupQuerystring: 'lng',
388 | lookupCookie: 'i18next',
389 | lookupHeader: 'accept-language',
390 | lookupHeaderRegex: /(([a-z]{2})-?([A-Z]{2})?)\s*;?\s*(q=([0-9.]+))?/gi,
391 | lookupSession: 'lng',
392 | lookupPath: 'lng',
393 | lookupFromPathIndex: 0,
394 |
395 | // cache user language, you can define if an how the detected language should be "saved" => 'cookie' and/or 'session'
396 | caches: false, // ['cookie']
397 |
398 | ignoreCase: true, // ignore case of detected language
399 |
400 | // optional expire and domain for set cookie
401 | cookieExpirationDate: new Date(),
402 | cookieDomain: 'myDomain',
403 | cookiePath: '/my/path',
404 | cookieSecure: true, // if need secure cookie
405 | cookieSameSite: 'strict', // 'strict', 'lax' or 'none'
406 |
407 | // optional conversion function used to modify the detected language code
408 | convertDetectedLanguage: 'Iso15897',
409 | convertDetectedLanguage: (lng) => lng.replace('-', '_')
410 | }
411 | ```
412 |
413 | Options can be passed in:
414 |
415 | **preferred** - by setting options.detection in i18next.init:
416 |
417 | ```js
418 | var i18next = require('i18next')
419 | var middleware = require('i18next-http-middleware')
420 |
421 | i18next.use(middleware.LanguageDetector).init({
422 | detection: options
423 | })
424 | ```
425 |
426 | on construction:
427 |
428 | ```js
429 | var middleware = require('i18next-http-middleware')
430 | var lngDetector = new middleware.LanguageDetector(null, options)
431 | ```
432 |
433 | via calling init:
434 |
435 | ```js
436 | var middleware = require('i18next-http-middleware')
437 |
438 | var lngDetector = new middleware.LanguageDetector()
439 | lngDetector.init(options)
440 | ```
441 |
442 | ## Adding own detection functionality
443 |
444 | ### interface
445 |
446 | ```js
447 | module.exports = {
448 | name: 'myDetectorsName',
449 |
450 | lookup: function (req, res, options) {
451 | // options -> are passed in options
452 | return 'en'
453 | },
454 |
455 | cacheUserLanguage: function (req, res, lng, options) {
456 | // options -> are passed in options
457 | // lng -> current language, will be called after init and on changeLanguage
458 | // store it
459 | }
460 | }
461 | ```
462 |
463 | ### adding it
464 |
465 | ```js
466 | var i18next = require('i18next')
467 | var middleware = require('i18next-http-middleware')
468 |
469 | var lngDetector = new middleware.LanguageDetector()
470 | lngDetector.addDetector(myDetector)
471 |
472 | i18next.use(lngDetector).init({
473 | detection: options
474 | })
475 | ```
476 |
477 | Don't forget: You have to add the name of your detector (`myDetectorsName` in this case) to the `order` array in your `options` object. Without that, your detector won't be used. See the [Detector Options section for more](#detector-options).
478 |
479 | ---
480 |
481 |
Gold Sponsors
482 |
483 |
484 |
485 |
486 |
487 |
488 |
--------------------------------------------------------------------------------
/example/basic-locize/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/basic-locize/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const i18next = require('i18next')
3 | const i18nextMiddleware = require('i18next-http-middleware')
4 | const Backend = require('i18next-locize-backend')
5 |
6 | const app = express()
7 | const port = process.env.PORT || 8080
8 |
9 | i18next
10 | .use(Backend)
11 | .use(i18nextMiddleware.LanguageDetector)
12 | .init({
13 | backend: {
14 | referenceLng: 'en',
15 | projectId: '79a28dc3-b858-44a4-9603-93455e9e8c65'
16 | // apiKey: 'do not show in production',
17 | },
18 | fallbackLng: 'en',
19 | preload: ['en', 'de'],
20 | debug: true,
21 | saveMissing: true
22 | })
23 |
24 | app.use(i18nextMiddleware.handle(i18next))
25 |
26 | app.get('/', (req, res) => {
27 | res.send(req.t('home.title'))
28 | })
29 |
30 | app.listen(port, () => {
31 | console.log(`Server is listening on port ${port}`)
32 | })
33 |
--------------------------------------------------------------------------------
/example/basic-locize/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-express-basic",
3 | "version": "1.0.0",
4 | "description": "Node Express server with i18next.",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "start": "node index.js"
9 | },
10 | "dependencies": {
11 | "express": "4.20.0",
12 | "i18next": "19.8.1",
13 | "i18next-http-middleware": "3.1.4",
14 | "i18next-locize-backend": "4.0.7"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/basic-pug/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/basic-pug/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const i18next = require('i18next')
3 | const i18nextMiddleware = require('i18next-http-middleware')
4 | const i18nextBackend = require('i18next-fs-backend')
5 |
6 | const app = express()
7 |
8 | app.set('view engine', 'pug');
9 | app.set('view options', { pretty: true });
10 | app.disable('x-powered-by');
11 | app.set('trust proxy', true);
12 | app.locals = { config: { whatever: 'this is' } };
13 |
14 | const port = process.env.PORT || 8080
15 |
16 | i18next
17 | .use(i18nextBackend)
18 | .use(i18nextMiddleware.LanguageDetector)
19 | .init({
20 | debug: true,
21 | fallbackLng: 'en',
22 | preload: ['de', 'en'],
23 | saveMissing: true,
24 | backend: {
25 | // eslint-disable-next-line no-path-concat
26 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
27 | // eslint-disable-next-line no-path-concat
28 | addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json'
29 | },
30 | detection: {
31 | order: ['querystring', 'cookie'],
32 | caches: ['cookie'],
33 | lookupQuerystring: 'locale',
34 | lookupCookie: 'locale',
35 | ignoreCase: true,
36 | cookieSecure: false
37 | }
38 | })
39 | app.use(i18nextMiddleware.handle(i18next))
40 |
41 | app.get('/', (req, res) => {
42 | res.render('index')
43 | })
44 |
45 | app.listen(port, () => {
46 | console.log(`Server is listening on port ${port}`)
47 | })
48 |
--------------------------------------------------------------------------------
/example/basic-pug/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/basic-pug/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/basic-pug/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-express-basic",
3 | "version": "1.0.0",
4 | "description": "Node Express server with i18next.",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "start": "node index.js"
9 | },
10 | "dependencies": {
11 | "express": "4.20.0",
12 | "i18next": "19.8.1",
13 | "i18next-http-middleware": "3.1.4",
14 | "i18next-fs-backend": "1.0.7",
15 | "pug": "3.0.3"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/basic-pug/views/index.pug:
--------------------------------------------------------------------------------
1 | html
2 | head
3 | title i18next - express with pug
4 | body
5 | h1= t('home.title')
6 | div
7 | a(href="/?locale=en") english
8 | | |
9 | a(href="/?locale=de") deutsch
10 |
--------------------------------------------------------------------------------
/example/basic/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/basic/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const i18next = require('i18next')
3 | const i18nextMiddleware = require('i18next-http-middleware')
4 | // const i18nextMiddleware = require('../../../i18next-http-middleware')
5 | const Backend = require('i18next-fs-backend')
6 | // const Backend = require('../../../i18next-fs-backend')
7 |
8 | const app = express()
9 | const port = process.env.PORT || 8080
10 |
11 | // const customDetector = {
12 | // name: 'customDetector',
13 | // lookup: (req) => {
14 | // console.log('Custom detector lookup');
15 | // console.log('req.query.custom', req.query.custom);
16 |
17 | // switch (req.query.custom) {
18 | // case 'en':
19 | // return 'en-US';
20 | // case 'fr':
21 | // return 'fr-FR';
22 | // default:
23 | // return 'en-US';
24 | // }
25 | // }
26 | // }
27 | // const languageDetector = new i18nextMiddleware.LanguageDetector()
28 | // languageDetector.addDetector(customDetector)
29 |
30 | i18next
31 | .use(Backend)
32 | // .use(languageDetector)
33 | .use(i18nextMiddleware.LanguageDetector)
34 | .init({
35 | // debug: true,
36 | // detection: {
37 | // order: ['customDetector']
38 | // },
39 | backend: {
40 | // eslint-disable-next-line no-path-concat
41 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
42 | // eslint-disable-next-line no-path-concat
43 | addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json'
44 | },
45 | fallbackLng: 'en',
46 | // nonExplicitSupportedLngs: true,
47 | // supportedLngs: ['en', 'de'],
48 | load: 'languageOnly',
49 | saveMissing: true
50 | })
51 |
52 | app.use(i18nextMiddleware.handle(i18next))
53 |
54 | app.get('/', (req, res) => {
55 | res.send(JSON.stringify({
56 | 'req.language': req.language,
57 | 'req.i18n.resolvedLanguage': req.i18n.resolvedLanguage,
58 | 'req.i18n.language': req.i18n.language,
59 | 'req.i18n.languages': req.i18n.languages,
60 | 'req.i18n.languages[0]': req.i18n.languages[0],
61 | 'req.t("home.title")': req.t('home.title')
62 | }, null, 2))
63 | })
64 |
65 | app.get('/missingtest', (req, res) => {
66 | req.t('nonExisting', 'some default value')
67 | res.send('check the locales files...')
68 | })
69 |
70 | // loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}.json
71 | app.use('/locales', express.static('locales'))
72 |
73 | // or instead of static
74 | // app.get('/locales/:lng/:ns', i18nextMiddleware.getResourcesHandler(i18next))
75 | // app.get('/locales/:lng/:ns', i18nextMiddleware.getResourcesHandler(i18next, { cache: false }))
76 | // loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}
77 |
78 | // missing keys make sure the body is parsed (i.e. with [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions))
79 | app.post('/locales/add/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next))
80 | // The client can be configured with i18next-http-backend, for example like this:
81 | // import HttpBackend from 'i18next-http-backend'
82 | // i18next.use(HttpBackend).init({
83 | // lng: 'en',
84 | // fallbackLng: 'en',
85 | // backend: {
86 | // loadPath: 'http://localhost:8080/locales/{{lng}}/{{ns}}.json',
87 | // addPath: 'http://localhost:8080/locales/add/{{lng}}/{{ns}}'
88 | // }
89 | // })
90 |
91 |
92 | app.listen(port, () => {
93 | console.log(`Server is listening on port ${port}`)
94 | })
95 |
96 | // curl localhost:8080 -H 'Accept-Language: de-de'
97 |
--------------------------------------------------------------------------------
/example/basic/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/basic/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/basic/locales/fr/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Bonjour le monde!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-express-basic",
3 | "version": "1.0.0",
4 | "description": "Node Express server with i18next.",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "start": "node index.js"
9 | },
10 | "dependencies": {
11 | "express": "4.20.0",
12 | "i18next": "25.0.0",
13 | "i18next-http-middleware": "3.7.3",
14 | "i18next-fs-backend": "2.6.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/deno/i18n.js:
--------------------------------------------------------------------------------
1 | import i18next from 'https://deno.land/x/i18next/index.js'
2 | // import i18next from 'https://raw.githubusercontent.com/i18next/i18next/master/src/index.js'
3 | // import i18next from 'https://cdn.jsdelivr.net/gh/i18next/i18next/src/index.js'
4 | import Backend from 'https://deno.land/x/i18next_fs_backend/index.js'
5 | // import Backend from 'https://cdn.jsdelivr.net/gh/i18next/i18next-fs-backend/index.js'
6 | // import Backend from 'https://raw.githubusercontent.com/i18next/i18next-fs-backend/master/index.js'
7 | // import Backend from '../../../i18next-fs-backend/lib/index.js'
8 |
9 | import i18nextMiddleware from 'https://deno.land/x/i18next_http_middleware/index.js'
10 | // import i18nextMiddleware from 'https://raw.githubusercontent.com/i18next/i18next-http-middleware/master/index.js'
11 | // import i18nextMiddleware from '../../../i18next-http-middleware/index.js'
12 |
13 | i18next
14 | .use(Backend)
15 | .use(i18nextMiddleware.LanguageDetector)
16 | .init({
17 | // debug: true,
18 | initAsync: false, // setting initAsync to false, will load the resources synchronously
19 | backend: {
20 | // eslint-disable-next-line no-path-concat
21 | loadPath: 'locales/{{lng}}/{{ns}}.json',
22 | // eslint-disable-next-line no-path-concat
23 | addPath: 'locales/{{lng}}/{{ns}}.missing.json'
24 | },
25 | fallbackLng: 'en',
26 | preload: ['en', 'de', 'it'],
27 | saveMissing: true
28 | })
29 |
30 | export const i18n = i18next
31 | export const middleware = i18nextMiddleware
32 |
--------------------------------------------------------------------------------
/example/deno/index.js:
--------------------------------------------------------------------------------
1 | // deno run --allow-net --allow-read index.js
2 | import { Application } from 'https://deno.land/x/abc/mod.ts'
3 | import { config } from "https://deno.land/x/dotenv/mod.ts"
4 | import { i18n, middleware } from './i18n.js'
5 | import { renderFile } from 'https://deno.land/x/dejs/mod.ts'
6 |
7 | const port = config.PORT || 8080
8 | const app = new Application()
9 |
10 | app.renderer = {
11 | render(name, data) {
12 | return renderFile(`./views/${name}.html`, data)
13 | }
14 | }
15 |
16 | const handle = middleware.handle(i18n)
17 |
18 | app.use((next) =>
19 | (c) => {
20 | handle(c)
21 | return next(c)
22 | }
23 | )
24 |
25 | app.get('/', (c) => c.render('index', { t: c.request.t, i18n: c.request.i18n }))
26 | app.get('/raw', (c) => c.request.t('home.title'))
27 |
28 | app.start({ port })
29 |
30 | console.log(i18n.t('server.started', { port }))
31 | console.log(i18n.t('server.started', { port, lng: 'de' }))
32 | console.log(i18n.t('server.started', { port, lng: 'it' }))
33 |
--------------------------------------------------------------------------------
/example/deno/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | },
5 | "server": {
6 | "started": "Der server lauscht auf dem Port {{port}}."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/deno/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | },
5 | "server": {
6 | "started": "Server is listening on port {{port}}."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/deno/locales/it/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Ciao Mondo!"
4 | },
5 | "server": {
6 | "started": "Il server sta aspettando sul port {{port}}."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/deno/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | i18next - abc with dejs
5 |
6 |
7 |
8 | <%= t('home.title') %>
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/fastify-pov/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/fastify-pov/index.js:
--------------------------------------------------------------------------------
1 | const fastify = require('fastify')
2 | const pov = require('point-of-view')
3 | const pug = require('pug')
4 | const i18next = require('i18next')
5 | // const i18nextMiddleware = require('i18next-http-middleware')
6 | const i18nextMiddleware = require('../../cjs')
7 | const Backend = require('i18next-fs-backend')
8 | // const Backend = require('../../../i18next-fs-backend')
9 |
10 | const app = fastify()
11 | app.register(pov, { engine: { pug } })
12 | const port = process.env.PORT || 8080
13 |
14 | i18next
15 | .use(Backend)
16 | .use(i18nextMiddleware.LanguageDetector)
17 | .init({
18 | // debug: true,
19 | backend: {
20 | // eslint-disable-next-line no-path-concat
21 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
22 | // eslint-disable-next-line no-path-concat
23 | addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json'
24 | },
25 | fallbackLng: 'en',
26 | preload: ['en', 'de'],
27 | saveMissing: true
28 | })
29 |
30 | app.register(i18nextMiddleware.plugin, { i18next })
31 | // app.addHook('preHandler', i18nextMiddleware.handle(i18next))
32 |
33 | app.get('/', (req, res) => {
34 | res.view('/views/index.pug')
35 | })
36 |
37 | app.listen(port, () => {
38 | console.log(`Server is listening on port ${port}`)
39 | })
40 |
--------------------------------------------------------------------------------
/example/fastify-pov/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/fastify-pov/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/fastify-pov/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-fastify-pov",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "type": "commonjs",
6 | "scripts": {
7 | "start": "node index.js"
8 | },
9 | "dependencies": {
10 | "fastify": "3.18.0",
11 | "point-of-view": "4.15.0",
12 | "pug": "3.0.3",
13 | "i18next": "20.3.2",
14 | "i18next-http-middleware": "3.1.4",
15 | "i18next-fs-backend": "1.1.1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/fastify-pov/views/index.pug:
--------------------------------------------------------------------------------
1 | html
2 | head
3 | title i18next - express with pug
4 | body
5 | h1= t('home.title')
6 | div
7 | a(href="/?lng=en") english
8 | | |
9 | a(href="/?lng=de") deutsch
10 |
--------------------------------------------------------------------------------
/example/fastify-pug/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/fastify-pug/index.js:
--------------------------------------------------------------------------------
1 | const fastify = require('fastify')
2 | const fastifyPug = require('fastify-pug')
3 | const i18next = require('i18next')
4 | const i18nextMiddleware = require('i18next-http-middleware')
5 | // const i18nextMiddleware = require('../../cjs')
6 | const Backend = require('i18next-fs-backend')
7 | // const Backend = require('../../../i18next-fs-backend')
8 |
9 | const app = fastify()
10 | app.register(fastifyPug, { views: 'views' })
11 | const port = process.env.PORT || 8080
12 |
13 | i18next
14 | .use(Backend)
15 | .use(i18nextMiddleware.LanguageDetector)
16 | .init({
17 | // debug: true,
18 | backend: {
19 | // eslint-disable-next-line no-path-concat
20 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
21 | // eslint-disable-next-line no-path-concat
22 | addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json'
23 | },
24 | fallbackLng: 'en',
25 | preload: ['en', 'de'],
26 | saveMissing: true
27 | })
28 |
29 | app.register(i18nextMiddleware.plugin, { i18next })
30 | // app.addHook('preHandler', i18nextMiddleware.handle(i18next))
31 |
32 | app.get('/', (req, res) => {
33 | res.render('index.pug')
34 | })
35 |
36 | app.listen(port, () => {
37 | console.log(`Server is listening on port ${port}`)
38 | })
39 |
--------------------------------------------------------------------------------
/example/fastify-pug/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/fastify-pug/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/fastify-pug/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-fastify-pug",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "type": "commonjs",
6 | "scripts": {
7 | "start": "node index.js"
8 | },
9 | "dependencies": {
10 | "fastify": "3.18.0",
11 | "fastify-pug": "2.0.0",
12 | "i18next": "20.3.2",
13 | "i18next-http-middleware": "3.1.4",
14 | "i18next-fs-backend": "1.1.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/fastify-pug/views/index.pug:
--------------------------------------------------------------------------------
1 | html
2 | head
3 | title i18next - express with pug
4 | body
5 | h1= t('home.title')
6 | div
7 | a(href="/?lng=en") english
8 | | |
9 | a(href="/?lng=de") deutsch
10 |
--------------------------------------------------------------------------------
/example/fastify/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/fastify/index.js:
--------------------------------------------------------------------------------
1 | const fastify = require('fastify')
2 | const i18next = require('i18next')
3 | const i18nextMiddleware = require('i18next-http-middleware')
4 | // const i18nextMiddleware = require('../../cjs')
5 | const Backend = require('i18next-fs-backend')
6 | // const Backend = require('../../../i18next-fs-backend')
7 |
8 | const app = fastify()
9 | const port = process.env.PORT || 8080
10 |
11 | i18next
12 | .use(Backend)
13 | .use(i18nextMiddleware.LanguageDetector)
14 | .init({
15 | // debug: true,
16 | backend: {
17 | // eslint-disable-next-line no-path-concat
18 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
19 | // eslint-disable-next-line no-path-concat
20 | addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json'
21 | },
22 | fallbackLng: 'en',
23 | preload: ['en', 'de'],
24 | saveMissing: true
25 | })
26 |
27 | app.register(i18nextMiddleware.plugin, { i18next })
28 | // app.addHook('preHandler', i18nextMiddleware.handle(i18next))
29 |
30 | app.setErrorHandler(function (error, request, reply) {
31 | reply.send(request.t('error'))
32 | })
33 |
34 | app.get('/', (req, res) => {
35 | res.send(req.t('home.title'))
36 | })
37 |
38 | app.get('/err', (req, res) => {
39 | throw 'some err'
40 | })
41 |
42 | app.listen(port, () => {
43 | console.log(`Server is listening on port ${port}`)
44 | })
45 |
--------------------------------------------------------------------------------
/example/fastify/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/fastify/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | },
5 | "error": "there was some error"
6 | }
7 |
--------------------------------------------------------------------------------
/example/fastify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-express-basic",
3 | "version": "1.0.0",
4 | "description": "Node Express server with i18next.",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "start": "node index.js"
9 | },
10 | "dependencies": {
11 | "fastify": "3.21.6",
12 | "i18next": "21.1.1",
13 | "i18next-http-middleware": "3.1.4",
14 | "i18next-fs-backend": "1.1.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/koa/README.md:
--------------------------------------------------------------------------------
1 | # run the sample
2 |
3 | ```
4 | $ npm i
5 | $ npm start
6 | ```
7 |
8 | open: http://localhost:8080
9 |
--------------------------------------------------------------------------------
/example/koa/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa')
2 | const router = require('@koa/router')()
3 | const koaBody = require('koa-body').default
4 | const serve = require('koa-static')
5 | const mount = require('koa-mount')
6 | const i18next = require('i18next')
7 | const i18nextMiddleware = require('i18next-http-middleware')
8 | // const i18nextMiddleware = require('../../../i18next-http-middleware')
9 | const Backend = require('i18next-fs-backend')
10 | // const Backend = require('../../../i18next-fs-backend')
11 |
12 | const app = new Koa()
13 | app.use(koaBody({
14 | jsonLimit: '1kb'
15 | }))
16 | const port = process.env.PORT || 8080
17 |
18 | i18next
19 | .use(Backend)
20 | // .use(languageDetector)
21 | .use(i18nextMiddleware.LanguageDetector)
22 | .init({
23 | // debug: true,
24 | // detection: {
25 | // order: ['customDetector']
26 | // },
27 | backend: {
28 | // eslint-disable-next-line no-path-concat
29 | loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json',
30 | // eslint-disable-next-line no-path-concat
31 | addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json'
32 | },
33 | fallbackLng: 'en',
34 | // nonExplicitSupportedLngs: true,
35 | // supportedLngs: ['en', 'de'],
36 | load: 'languageOnly',
37 | saveMissing: true
38 | })
39 |
40 | app.use(i18nextMiddleware.koaPlugin(i18next))
41 |
42 | router.get('/', ctx => {
43 | ctx.body = JSON.stringify({
44 | 'ctx.language': ctx.language,
45 | 'ctx.i18n.language': ctx.i18n.language,
46 | 'ctx.i18n.languages': ctx.i18n.languages,
47 | 'ctx.i18n.languages[0]': ctx.i18n.languages[0],
48 | 'ctx.t("home.title")': ctx.t('home.title')
49 | }, null, 2)
50 | })
51 |
52 |
53 | router.get('/missingtest', ctx => {
54 | ctx.t('nonExisting', 'some default value')
55 | ctx.body = 'check the locales files...'
56 | })
57 |
58 | // loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}.json
59 | app.use(mount('/locales', serve('./locales')))
60 |
61 | // or instead of static
62 | // router.get('/locales/:lng/:ns', i18nextMiddleware.getResourcesHandler(i18next))
63 | // loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}
64 |
65 | // missing keys make sure the body is parsed (i.e. with [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions))
66 | router.post('/locales/add/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next))
67 | // The client can be configured with i18next-http-backend, for example like this:
68 | // import HttpBackend from 'i18next-http-backend'
69 | // i18next.use(HttpBackend).init({
70 | // lng: 'en',
71 | // fallbackLng: 'en',
72 | // backend: {
73 | // loadPath: 'http://localhost:8080/locales/{{lng}}/{{ns}}.json',
74 | // addPath: 'http://localhost:8080/locales/add/{{lng}}/{{ns}}'
75 | // }
76 | // })
77 |
78 | app.use(router.routes())
79 |
80 | app.listen(port, () => {
81 | console.log(`Server is listening on port ${port}`)
82 | })
83 |
84 | // curl localhost:8080 -H 'Accept-Language: de-de'
85 |
--------------------------------------------------------------------------------
/example/koa/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hallo Welt!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/koa/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Hello World!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/koa/locales/fr/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": {
3 | "title": "Bonjour le monde!"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/koa/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-express-basic",
3 | "version": "1.0.0",
4 | "description": "Node Express server with i18next.",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "start": "node index.js"
9 | },
10 | "dependencies": {
11 | "@koa/router": "12.0.0",
12 | "i18next": "23.5.1",
13 | "i18next-fs-backend": "2.2.0",
14 | "i18next-http-middleware": "3.4.0",
15 | "koa": "2.16.1",
16 | "koa-body": "6.0.1",
17 | "koa-mount": "4.0.0",
18 | "koa-static": "5.0.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/index.d.mts:
--------------------------------------------------------------------------------
1 | export * from './index.js';
2 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Application,
3 | Handler,
4 | Request,
5 | RequestHandler,
6 | Response,
7 | Router,
8 | } from "express-serve-static-core";
9 | import * as i18next from "i18next";
10 |
11 | ///
12 |
13 | type I18next = i18next.i18n;
14 | type App = Application | Router;
15 |
16 | type I18NextRequest = {
17 | language: string;
18 | languages: string[];
19 | i18n: i18next.i18n;
20 | t: i18next.TFunction;
21 | };
22 |
23 | declare global {
24 | namespace Express {
25 | interface Request extends I18NextRequest {}
26 | }
27 | }
28 |
29 | declare module 'fastify' {
30 | interface FastifyRequest extends I18NextRequest {}
31 | }
32 |
33 | interface ExtendedOptions extends Object {
34 | getPath?: (req: Request) => string;
35 | getOriginalUrl?: (req: Request) => string;
36 | getUrl?: (req: Request) => string;
37 | setUrl?: (req: Request, url: string) => void;
38 | getParams?: (req: Request) => Object;
39 | getSession?: (req: Request) => Object;
40 | getQuery?: (req: Request) => Object;
41 | getCookies?: (req: Request) => Object;
42 | getBody?: (req: Request) => Object;
43 | getHeaders?: (req: Request) => Object;
44 | getHeader?: (res: Response, name: string) => Object;
45 | setHeader?: (res: Response, name: string, value: string) => void;
46 | setContentType?: (res: Response, type: string) => void;
47 | setStatus?: (res: Response, code: number) => void;
48 | send?: (res: Response, body: any) => void;
49 | }
50 |
51 | interface HandleOptions extends ExtendedOptions {
52 | ignoreRoutes?: string[] | IgnoreRoutesFunction;
53 | removeLngFromUrl?: boolean;
54 | }
55 |
56 | interface GetResourcesHandlerOptions extends ExtendedOptions {
57 | maxAge?: number;
58 | cache?: boolean;
59 | lngParam?: string;
60 | nsParam?: string;
61 | }
62 |
63 | interface MissingKeyHandlerOptions extends ExtendedOptions {
64 | lngParam?: string;
65 | nsParam?: string;
66 | }
67 |
68 | type IgnoreRoutesFunction = (
69 | req: Request,
70 | res: Response,
71 | options: HandleOptions,
72 | i18next: I18next
73 | ) => boolean;
74 |
75 | export function handle(i18next: I18next, options?: HandleOptions): Handler;
76 |
77 | export function koaPlugin(i18next: I18next, options?: HandleOptions): (context: unknown, next: Function) => any;
78 |
79 | export function plugin(
80 | instance: any,
81 | options: HandleOptions & { i18next?: I18next },
82 | next: (err?: Error) => void
83 | ): void;
84 |
85 | export function getResourcesHandler(
86 | i18next: I18next,
87 | options?: GetResourcesHandlerOptions
88 | ): Handler;
89 |
90 | export function missingKeyHandler(
91 | i18next: I18next,
92 | options?: MissingKeyHandlerOptions
93 | ): Handler;
94 |
95 | export function addRoute(
96 | i18next: I18next,
97 | route: string,
98 | lngs: string[],
99 | app: App,
100 | verb: string,
101 | fc: RequestHandler
102 | ): void;
103 |
104 | // LanguageDetector
105 | type LanguageDetectorServices = any;
106 | type LanguageDetectorOrder = string[];
107 | type LanguageDetectorCaches = boolean | string[];
108 | interface LanguageDetectorOptions {
109 | order?: LanguageDetectorOrder;
110 | lookupQuerystring?: string;
111 | lookupCookie?: string;
112 | lookupSession?: string;
113 | lookupFromPathIndex?: number;
114 | caches?: LanguageDetectorCaches;
115 | cookieExpirationDate?: Date;
116 | cookieDomain?: string;
117 |
118 | /**
119 | * optional conversion function to use to modify the detected language code
120 | */
121 | convertDetectedLanguage?: 'Iso15897' | ((lng: string) => string);
122 | }
123 | interface LanguageDetectorAllOptions {
124 | fallbackLng: boolean | string | string[];
125 | }
126 | interface LanguageDetectorInterfaceOptions {
127 | [name: string]: any;
128 | }
129 | interface LanguageDetectorInterface {
130 | name: string;
131 |
132 | lookup: (
133 | req: Request,
134 | res: Response,
135 | options?: LanguageDetectorInterfaceOptions
136 | ) => string | string[] | undefined;
137 |
138 | cacheUserLanguage?: (
139 | req: Request,
140 | res: Response,
141 | lng: string,
142 | options?: Object
143 | ) => void;
144 | }
145 |
146 | export class LanguageDetector implements i18next.Module {
147 | type: "languageDetector";
148 |
149 | constructor(
150 | services: LanguageDetectorServices,
151 | options?: LanguageDetectorOptions,
152 | allOptions?: LanguageDetectorAllOptions
153 | );
154 |
155 | constructor(
156 | options?: LanguageDetectorOptions,
157 | allOptions?: LanguageDetectorAllOptions
158 | );
159 |
160 | init(
161 | services: LanguageDetectorServices,
162 | options?: LanguageDetectorOptions,
163 | allOptions?: LanguageDetectorAllOptions
164 | ): void;
165 |
166 | init(
167 | options?: LanguageDetectorOptions,
168 | allOptions?: LanguageDetectorAllOptions
169 | ): void;
170 |
171 | addDetector(detector: LanguageDetectorInterface): void;
172 |
173 | detect(
174 | req: Request,
175 | res: Response,
176 | detectionOrder: LanguageDetectorOrder
177 | ): void;
178 |
179 | cacheUserLanguage(
180 | req: Request,
181 | res: Response,
182 | lng: string,
183 | caches: LanguageDetectorCaches
184 | ): void;
185 | }
186 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // // @deno-types='./index.d.ts'
2 | import mod from './lib/index.js'
3 | export default mod
4 |
--------------------------------------------------------------------------------
/lib/LanguageDetector.js:
--------------------------------------------------------------------------------
1 | import * as utils from './utils.js'
2 | import cookieLookup from './languageLookups/cookie.js'
3 | import querystringLookup from './languageLookups/querystring.js'
4 | import pathLookup from './languageLookups/path.js'
5 | import headerLookup from './languageLookups/header.js'
6 | import sessionLookup from './languageLookups/session.js'
7 | import { extendOptionsWithDefaults } from './httpFunctions.js'
8 |
9 | function getDefaults () {
10 | return extendOptionsWithDefaults({
11 | order: [/* 'path', 'session' */ 'querystring', 'cookie', 'header'],
12 | lookupQuerystring: 'lng',
13 | lookupCookie: 'i18next',
14 | lookupSession: 'lng',
15 | lookupFromPathIndex: 0,
16 |
17 | // cache user language
18 | caches: false, // ['cookie']
19 | // cookieExpirationDate: new Date(),
20 | // cookieDomain: 'myDomain'
21 | // cookiePath: '/my/path'
22 | cookieSameSite: 'strict',
23 | ignoreCase: true,
24 |
25 | convertDetectedLanguage: (l) => l
26 | })
27 | }
28 |
29 | class LanguageDetector {
30 | constructor (services, options = {}, allOptions = {}) {
31 | this.type = 'languageDetector'
32 | this.detectors = {}
33 |
34 | this.init(services, options, allOptions)
35 | }
36 |
37 | init (services, options = {}, allOptions = {}) {
38 | this.services = services
39 | this.options = utils.defaults(options, this.options || {}, getDefaults())
40 | this.allOptions = allOptions
41 |
42 | if (typeof this.options.convertDetectedLanguage === 'string' && this.options.convertDetectedLanguage.indexOf('15897') > -1) {
43 | this.options.convertDetectedLanguage = (l) => l.replace('-', '_')
44 | }
45 |
46 | this.addDetector(cookieLookup)
47 | this.addDetector(querystringLookup)
48 | this.addDetector(pathLookup)
49 | this.addDetector(headerLookup)
50 | this.addDetector(sessionLookup)
51 | }
52 |
53 | addDetector (detector) {
54 | this.detectors[detector.name] = detector
55 | }
56 |
57 | detect (req, res, detectionOrder) {
58 | if (arguments.length < 2) return
59 | if (!detectionOrder) detectionOrder = this.options.order
60 |
61 | let found
62 | detectionOrder.forEach(detectorName => {
63 | if (found || !this.detectors[detectorName]) return
64 |
65 | let detections = this.detectors[detectorName].lookup(req, res, this.options)
66 | if (!detections) return
67 | if (!Array.isArray(detections)) detections = [detections]
68 |
69 | detections = detections
70 | .filter((d) => d !== undefined && d !== null && !utils.hasXSS(d))
71 | .map((d) => this.options.convertDetectedLanguage(d))
72 |
73 | if (this.services.languageUtils.getBestMatchFromCodes) { // new i18next v19.5.0
74 | found = this.services.languageUtils.getBestMatchFromCodes(detections)
75 | if (found) {
76 | if (this.options.ignoreCase) {
77 | if (detections.map(d => d.toLowerCase()).indexOf(found.toLowerCase()) < 0) found = undefined
78 | } else {
79 | if (detections.indexOf(found) < 0) found = undefined
80 | }
81 | }
82 | if (found) req.i18nextLookupName = detectorName
83 | } else {
84 | found = detections.length > 0 ? detections[0] : null // a little backward compatibility
85 | }
86 | })
87 |
88 | if (!found) {
89 | let fallbacks = this.allOptions.fallbackLng
90 | if (typeof fallbacks === 'function') fallbacks = fallbacks()
91 | if (typeof fallbacks === 'string') fallbacks = [fallbacks]
92 | if (!fallbacks) fallbacks = []
93 | if (Object.prototype.toString.apply(fallbacks) === '[object Array]') {
94 | found = fallbacks[0]
95 | } else {
96 | found = fallbacks[0] || (fallbacks.default && fallbacks.default[0])
97 | }
98 | };
99 |
100 | return found
101 | }
102 |
103 | cacheUserLanguage (req, res, lng, caches) {
104 | if (arguments.length < 3) return
105 | if (!caches) caches = this.options.caches
106 | if (!caches) return
107 | caches.forEach(cacheName => {
108 | if (this.detectors[cacheName] && this.detectors[cacheName].cacheUserLanguage && res.cachedUserLanguage !== lng) {
109 | this.detectors[cacheName].cacheUserLanguage(req, res, lng, this.options)
110 | res.cachedUserLanguage = lng
111 | }
112 | })
113 | }
114 | }
115 |
116 | LanguageDetector.type = 'languageDetector'
117 |
118 | export default LanguageDetector
119 |
--------------------------------------------------------------------------------
/lib/httpFunctions.js:
--------------------------------------------------------------------------------
1 | export const getPath = (req) => {
2 | if (req.path) return req.path
3 | if (req.raw && req.raw.path) return req.raw.path
4 | if (req.url) return req.url
5 | console.log('no possibility found to get path')
6 | }
7 | export const getUrl = (req) => {
8 | if (req.url && req.url.href) return req.url.href
9 | if (req.url) return req.url
10 | if (req.raw && req.raw.url) return req.raw.url
11 | console.log('no possibility found to get url')
12 | }
13 | export const setUrl = (req, url) => {
14 | if (req.url) {
15 | req.url = url
16 | return
17 | }
18 | console.log('no possibility found to get url')
19 | }
20 | export const getOriginalUrl = (req) => {
21 | if (req.originalUrl) return req.originalUrl
22 | if (req.raw && req.raw.originalUrl) return req.raw.originalUrl
23 | return getUrl(req)
24 | }
25 | export const getQuery = (req) => {
26 | if (
27 | req.query &&
28 | typeof req.query.entries === 'function' &&
29 | typeof Object.fromEntries === 'function' &&
30 | typeof req.query[Symbol.iterator] === 'function'
31 | ) {
32 | return Object.fromEntries(req.query)
33 | }
34 | if (req.query) return req.query
35 | if (req.searchParams) return req.searchParams
36 | if (req.raw && req.raw.query) return req.raw.query
37 | if (req.ctx && req.ctx.queryParams) return req.ctx.queryParams
38 | if (req.url && req.url.searchParams) return req.url.searchParams
39 | const url = req.url || (req.raw && req.raw.url)
40 | if (url && url.indexOf('?') < 0) return {}
41 | console.log('no possibility found to get query')
42 | return {}
43 | }
44 | export const getParams = (req) => {
45 | if (req.params) return req.params
46 | if (req.raw && req.raw.params) return req.raw.params
47 | if (req.ctx && req.ctx.params) return req.ctx.params
48 | console.log('no possibility found to get params')
49 | return {}
50 | }
51 | export const getHeaders = (req) => {
52 | if (req.headers) return req.headers
53 | console.log('no possibility found to get headers')
54 | }
55 | export const getCookies = (req) => {
56 | if (req.cookies) return req.cookies
57 | if (getHeaders(req)) {
58 | const list = {}
59 | const rc = getHeaders(req).cookie
60 | rc &&
61 | rc.split(';').forEach((cookie) => {
62 | const parts = cookie.split('=')
63 | list[parts.shift().trim()] = decodeURI(encodeURI(parts.join('=')))
64 | })
65 | return list
66 | }
67 | console.log('no possibility found to get cookies')
68 | }
69 | export const getBody = (req) => {
70 | if (req.ctx && typeof req.ctx.body === 'function') {
71 | return req.ctx.body.bind(req.ctx)
72 | }
73 | if (req.ctx && req.ctx.body) return req.ctx.body
74 | if (req.json) return req.json
75 | if (req.body) return req.body
76 | if (req.payload) return req.payload
77 | if (req.request && req.request.body) return req.request.body
78 | console.log('no possibility found to get body')
79 | return {}
80 | }
81 | export const getHeader = (res, name) => {
82 | if (res.getHeader) return res.getHeader(name)
83 | if (res.headers) return res.headers[name]
84 | if (getHeaders(res) && getHeaders(res)[name]) return getHeaders(res)[name]
85 | console.log('no possibility found to get header')
86 | return undefined
87 | }
88 | export const setHeader = (res, name, value) => {
89 | if (res._headerSent || res.headersSent) return
90 | if (typeof res.setHeader === 'function') return res.setHeader(name, value)
91 | if (typeof res.header === 'function') return res.header(name, value)
92 | if (res.responseHeaders && typeof res.responseHeaders.set === 'function') {
93 | return res.responseHeaders.set(name, value)
94 | }
95 | if (res.headers && typeof res.headers.set === 'function') {
96 | return res.headers.set(name, value)
97 | }
98 | if (typeof res.set === 'function') {
99 | return res.set(name, value)
100 | }
101 | console.log('no possibility found to set header')
102 | }
103 | export const setContentType = (res, type) => {
104 | if (typeof res.contentType === 'function') return res.contentType(type)
105 | if (typeof res.type === 'function') return res.type(type)
106 | setHeader(res, 'Content-Type', type)
107 | }
108 | export const setStatus = (res, code) => {
109 | if (typeof res.status === 'function') return res.status(code)
110 | // eslint-disable-next-line no-return-assign
111 | if (res.status) return (res.status = code)
112 | console.log('no possibility found to set status')
113 | }
114 | export const send = (res, body) => {
115 | if (typeof res.send === 'function') return res.send(body)
116 | if (res.request && res.response && res.app) res.body = body
117 | return body
118 | }
119 | export const getSession = (req) => {
120 | if (req.session) return req.session
121 | if (req.raw && req.raw.session) return req.raw.session
122 | console.log('no possibility found to get session')
123 | }
124 |
125 | export const extendOptionsWithDefaults = (options = {}) => {
126 | options.getPath = options.getPath || getPath
127 | options.getOriginalUrl = options.getOriginalUrl || getOriginalUrl
128 | options.getUrl = options.getUrl || getUrl
129 | options.setUrl = options.setUrl || setUrl
130 | options.getParams = options.getParams || getParams
131 | options.getSession = options.getSession || getSession
132 | options.getQuery = options.getQuery || getQuery
133 | options.getCookies = options.getCookies || getCookies
134 | options.getBody = options.getBody || getBody
135 | options.getHeaders = options.getHeaders || getHeaders
136 | options.getHeader = options.getHeader || getHeader
137 | options.setHeader = options.setHeader || setHeader
138 | options.setContentType = options.setContentType || setContentType
139 | options.setStatus = options.setStatus || setStatus
140 | options.send = options.send || send
141 | return options
142 | }
143 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import * as utils from './utils.js'
2 | import LD from './LanguageDetector.js'
3 | import { extendOptionsWithDefaults } from './httpFunctions.js'
4 |
5 | export const LanguageDetector = LD
6 |
7 | const checkForCombinedReqRes = (req, res, next) => {
8 | if (!res) {
9 | if (req.request && req.response) {
10 | res = req.response
11 | if (!req.request.ctx) req.request.ctx = req
12 | req = req.request
13 | if (!next) next = () => {}
14 | } else if (req.respond) {
15 | res = req
16 | if (!next) next = () => {}
17 | }
18 | } else if (!next && typeof res === 'function' && req.req && req.res) {
19 | return {
20 | req,
21 | res: req,
22 | next: res
23 | }
24 | }
25 | return { req, res, next }
26 | }
27 |
28 | export function handle (i18next, options = {}) {
29 | extendOptionsWithDefaults(options)
30 |
31 | return function i18nextMiddleware (rq, rs, n) {
32 | const { req, res, next } = checkForCombinedReqRes(rq, rs, n)
33 |
34 | if (typeof options.ignoreRoutes === 'function') {
35 | if (options.ignoreRoutes(req, res, options, i18next)) {
36 | return next()
37 | }
38 | } else {
39 | const ignores =
40 | (options.ignoreRoutes instanceof Array && options.ignoreRoutes) || []
41 | for (let i = 0; i < ignores.length; i++) {
42 | if (options.getPath(req).indexOf(ignores[i]) > -1) return next()
43 | }
44 | }
45 |
46 | const i18n = i18next.cloneInstance({ initAsync: false, initImmediate: false })
47 | i18n.on('languageChanged', lng => {
48 | // Keep language in sync
49 | req.language = req.locale = req.lng = lng
50 |
51 | if (options.attachLocals) res.locals = res.locals || {}
52 | if (res.locals) {
53 | res.locals.language = lng
54 | res.locals.languageDir = i18next.dir(lng)
55 | res.locals.resolvedLanguage = i18n.resolvedLanguage
56 | }
57 |
58 | if (lng && options.getHeader(res, 'Content-Language') !== lng) {
59 | options.setHeader(res, 'Content-Language', utils.escape(lng))
60 | }
61 |
62 | req.languages = i18next.services.languageUtils.toResolveHierarchy(lng)
63 | req.resolvedLanguage = i18n.resolvedLanguage
64 |
65 | if (i18next.services.languageDetector) {
66 | i18next.services.languageDetector.cacheUserLanguage(req, res, lng)
67 | }
68 | })
69 |
70 | let lng = req.lng
71 | if (!lng && i18next.services.languageDetector) {
72 | lng = i18next.services.languageDetector.detect(req, res)
73 | }
74 |
75 | // set locale
76 | req.language = req.locale = req.lng = lng
77 | if (lng && options.getHeader(res, 'Content-Language') !== lng) {
78 | options.setHeader(res, 'Content-Language', utils.escape(lng))
79 | }
80 | req.languages = i18next.services.languageUtils.toResolveHierarchy(lng)
81 | req.resolvedLanguage = i18n.resolvedLanguage
82 |
83 | // trigger sync to instance - might trigger async load!
84 | i18n.changeLanguage(lng || i18next.options.fallbackLng[0])
85 |
86 | if (req.i18nextLookupName === 'path' && options.removeLngFromUrl) {
87 | options.setUrl(
88 | req,
89 | utils.removeLngFromUrl(
90 | options.getUrl(req),
91 | i18next.services.languageDetector.options.lookupFromPathIndex
92 | )
93 | )
94 | }
95 |
96 | const t = i18n.t.bind(i18n)
97 | const exists = i18n.exists.bind(i18n)
98 |
99 | // assert for req
100 | req.i18n = i18n
101 | req.t = t
102 |
103 | // assert for res -> template
104 | if (options.attachLocals) res.locals = res.locals || {}
105 | if (res.locals) {
106 | res.locals.t = t
107 | res.locals.exists = exists
108 | res.locals.i18n = i18n
109 | res.locals.language = lng
110 | res.locals.resolvedLanguage = i18n.resolvedLanguage
111 | res.locals.languageDir = i18next.dir(lng)
112 | }
113 |
114 | if (i18next.services.languageDetector) {
115 | i18next.services.languageDetector.cacheUserLanguage(req, res, lng)
116 | }
117 | // load resources
118 | if (!req.lng) return next()
119 | i18next.loadLanguages(req.lng, () => next())
120 | }
121 | }
122 |
123 | export function plugin (instance, options, next) {
124 | options.attachLocals = true
125 | const middleware = handle(options.i18next, options)
126 | instance.addHook('preHandler', (request, reply, next) =>
127 | middleware(request, reply, next)
128 | )
129 | return next()
130 | }
131 |
132 | export function koaPlugin (i18next, options) {
133 | const middleware = handle(i18next, options)
134 | return async function koaMiddleware (ctx, next) {
135 | await new Promise((resolve) => middleware(ctx, ctx, resolve))
136 | await next()
137 | }
138 | }
139 |
140 | export const hapiPlugin = {
141 | name: 'i18next-http-middleware',
142 | version: '1',
143 | register: (server, options) => {
144 | options.attachLocals = true
145 | const middleware = handle(options.i18next, {
146 | ...options
147 | })
148 | server.ext('onPreAuth', (request, h) => {
149 | middleware(
150 | request,
151 | request.raw.res || h.response() || request.Response,
152 | () => h.continue
153 | )
154 | return h.continue
155 | })
156 | }
157 | }
158 |
159 | plugin[Symbol.for('skip-override')] = true
160 |
161 | export function getResourcesHandler (i18next, options = {}) {
162 | extendOptionsWithDefaults(options)
163 | const maxAge = options.maxAge || 60 * 60 * 24 * 30
164 |
165 | return function (rq, rs) {
166 | const { req, res } = checkForCombinedReqRes(rq, rs)
167 |
168 | if (!i18next.services.backendConnector) {
169 | options.setStatus(res, 404)
170 | return options.send(
171 | res,
172 | 'i18next-express-middleware:: no backend configured'
173 | )
174 | }
175 |
176 | const resources = {}
177 |
178 | options.setContentType(res, 'application/json')
179 | if (
180 | options.cache !== undefined
181 | ? options.cache
182 | : typeof process !== 'undefined' &&
183 | process.env &&
184 | process.env.NODE_ENV === 'production'
185 | ) {
186 | options.setHeader(res, 'Cache-Control', 'public, max-age=' + maxAge)
187 | options.setHeader(
188 | res,
189 | 'Expires',
190 | new Date(new Date().getTime() + maxAge * 1000).toUTCString()
191 | )
192 | } else {
193 | options.setHeader(res, 'Pragma', 'no-cache')
194 | options.setHeader(res, 'Cache-Control', 'no-cache')
195 | }
196 |
197 | // first check query
198 | let languages = options.getQuery(req)[options.lngParam || 'lng']
199 | ? options.getQuery(req)[options.lngParam || 'lng'].split(' ')
200 | : []
201 | let namespaces = options.getQuery(req)[options.nsParam || 'ns']
202 | ? options.getQuery(req)[options.nsParam || 'ns'].split(' ')
203 | : []
204 |
205 | // then check route params
206 | if (languages.length === 0 && namespaces.length === 0) {
207 | languages = options.getParams(req)[options.lngParam || 'lng']
208 | ? options.getParams(req)[options.lngParam || 'lng'].split(' ')
209 | : []
210 | namespaces = options.getParams(req)[options.nsParam || 'ns']
211 | ? options.getParams(req)[options.nsParam || 'ns'].split(' ')
212 | : []
213 | }
214 |
215 | // extend ns
216 | namespaces.forEach(ns => {
217 | if (i18next.options.ns && i18next.options.ns.indexOf(ns) < 0) {
218 | i18next.options.ns.push(ns)
219 | }
220 | })
221 |
222 | i18next.services.backendConnector.load(languages, namespaces, function () {
223 | languages.forEach(lng => {
224 | namespaces.forEach(ns => {
225 | utils.setPath(
226 | resources,
227 | [lng, ns],
228 | i18next.getResourceBundle(lng, ns)
229 | )
230 | })
231 | })
232 | })
233 |
234 | return options.send(res, resources)
235 | }
236 | }
237 |
238 | export function missingKeyHandler (i18next, options = {}) {
239 | extendOptionsWithDefaults(options)
240 |
241 | return function (rq, rs) {
242 | const { req, res } = checkForCombinedReqRes(rq, rs)
243 |
244 | const lng = options.getParams(req)[options.lngParam || 'lng']
245 | const ns = options.getParams(req)[options.nsParam || 'ns']
246 |
247 | if (!i18next.services.backendConnector) {
248 | options.setStatus(res, 404)
249 | return options.send(
250 | res,
251 | 'i18next-express-middleware:: no backend configured'
252 | )
253 | }
254 |
255 | const body = options.getBody(req)
256 |
257 | if (typeof body === 'function') {
258 | const promise = body()
259 | if (promise && typeof promise.then === 'function') {
260 | return new Promise(resolve => {
261 | promise.then(b => {
262 | for (const m in b) {
263 | i18next.services.backendConnector.saveMissing([lng], ns, m, b[m])
264 | }
265 | resolve(options.send(res, 'ok'))
266 | })
267 | })
268 | }
269 | }
270 |
271 | for (const m in body) {
272 | i18next.services.backendConnector.saveMissing([lng], ns, m, body[m])
273 | }
274 |
275 | return options.send(res, 'ok')
276 | }
277 | }
278 |
279 | export function addRoute (i18next, route, lngs, app, verb, fc) {
280 | if (typeof verb === 'function') {
281 | fc = verb
282 | verb = 'get'
283 | }
284 |
285 | // Combine `fc` and possible more callbacks to one array
286 | const callbacks = [fc].concat(Array.prototype.slice.call(arguments, 6))
287 |
288 | for (let i = 0, li = lngs.length; i < li; i++) {
289 | const parts = String(route).split('/')
290 | const locRoute = []
291 | for (let y = 0, ly = parts.length; y < ly; y++) {
292 | const part = parts[y]
293 | // if the route includes the parameter :lng
294 | // this is replaced with the value of the language
295 | if (part === ':lng') {
296 | locRoute.push(lngs[i])
297 | } else if (part.indexOf(':') === 0 || part === '') {
298 | locRoute.push(part)
299 | } else {
300 | locRoute.push(i18next.t(part, { lng: lngs[i] }))
301 | }
302 | }
303 |
304 | const routes = [locRoute.join('/')]
305 | app[verb || 'get'].apply(app, routes.concat(callbacks))
306 | }
307 | }
308 |
309 | export default {
310 | plugin,
311 | hapiPlugin,
312 | koaPlugin,
313 | handle,
314 | getResourcesHandler,
315 | missingKeyHandler,
316 | addRoute,
317 | LanguageDetector
318 | }
319 |
--------------------------------------------------------------------------------
/lib/languageLookups/cookie.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-control-regex
2 | const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/
3 |
4 | const serializeCookie = (name, val, options) => {
5 | const opt = options || {}
6 | opt.path = opt.path || '/'
7 | const value = encodeURIComponent(val)
8 | let str = name + '=' + value
9 | if (opt.maxAge > 0) {
10 | const maxAge = opt.maxAge - 0
11 | if (isNaN(maxAge)) throw new Error('maxAge should be a Number')
12 | str += '; Max-Age=' + Math.floor(maxAge)
13 | }
14 | if (opt.domain) {
15 | if (!fieldContentRegExp.test(opt.domain)) {
16 | throw new TypeError('option domain is invalid')
17 | }
18 | str += '; Domain=' + opt.domain
19 | }
20 | if (opt.path) {
21 | if (!fieldContentRegExp.test(opt.path)) {
22 | throw new TypeError('option path is invalid')
23 | }
24 | str += '; Path=' + opt.path
25 | }
26 | if (opt.expires) {
27 | if (typeof opt.expires.toUTCString !== 'function') {
28 | throw new TypeError('option expires is invalid')
29 | }
30 | str += '; Expires=' + opt.expires.toUTCString()
31 | }
32 | if (opt.httpOnly) str += '; HttpOnly'
33 | if (opt.secure) str += '; Secure'
34 | if (opt.sameSite) {
35 | const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite
36 | switch (sameSite) {
37 | case true:
38 | str += '; SameSite=Strict'
39 | break
40 | case 'lax':
41 | str += '; SameSite=Lax'
42 | break
43 | case 'strict':
44 | str += '; SameSite=Strict'
45 | break
46 | case 'none':
47 | str += '; SameSite=None'
48 | break
49 | default:
50 | throw new TypeError('option sameSite is invalid')
51 | }
52 | }
53 | return str
54 | }
55 |
56 | export default {
57 | name: 'cookie',
58 |
59 | lookup (req, res, options) {
60 | let found
61 |
62 | if (options.lookupCookie && typeof req !== 'undefined') {
63 | const cookies = options.getCookies(req)
64 | if (cookies) found = cookies[options.lookupCookie]
65 | }
66 |
67 | return found
68 | },
69 |
70 | cacheUserLanguage (req, res, lng, options = {}) {
71 | if (options.lookupCookie && req !== 'undefined') {
72 | let expirationDate = options.cookieExpirationDate
73 | if (!expirationDate) {
74 | expirationDate = new Date()
75 | expirationDate.setFullYear(expirationDate.getFullYear() + 1)
76 | }
77 |
78 | const cookieOptions = {
79 | expires: expirationDate,
80 | domain: options.cookieDomain,
81 | path: options.cookiePath,
82 | httpOnly: false,
83 | overwrite: true,
84 | sameSite: options.cookieSameSite
85 | }
86 |
87 | if (options.cookieSecure) cookieOptions.secure = options.cookieSecure
88 |
89 | let existingCookie = options.getHeader(res, 'set-cookie') || options.getHeader(res, 'Set-Cookie') || []
90 | if (typeof existingCookie === 'string') existingCookie = [existingCookie]
91 | if (!Array.isArray(existingCookie)) existingCookie = []
92 | existingCookie = existingCookie.filter((c) => c.indexOf(`${options.lookupCookie}=`) !== 0)
93 | existingCookie.push(serializeCookie(options.lookupCookie, lng, cookieOptions))
94 | options.setHeader(res, 'Set-Cookie', existingCookie.length === 1 ? existingCookie[0] : existingCookie)
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/languageLookups/header.js:
--------------------------------------------------------------------------------
1 | const specialCases = ['hans', 'hant', 'latn', 'cyrl', 'cans', 'mong', 'arab', '419']
2 |
3 | export default {
4 | name: 'header',
5 |
6 | lookup (req, res, options) {
7 | let found
8 |
9 | if (typeof req !== 'undefined') {
10 | const headers = options.getHeaders(req)
11 | if (!headers) return found
12 |
13 | const locales = []
14 | const acceptLanguage = options.lookupHeader ? headers[options.lookupHeader] : headers['accept-language']
15 |
16 | if (acceptLanguage) {
17 | let lookupRegex = /(([a-z]{2,3})-?([A-Z]{2})?)\s*;?\s*(q=([0-9.]+))?/gi
18 | if (acceptLanguage.indexOf('-') > 0) {
19 | const foundSpecialCase = specialCases.find((s) => acceptLanguage.toLowerCase().indexOf(`-${s}`) > 0)
20 | if (foundSpecialCase) lookupRegex = /(([a-z]{2,3})-?([A-Z0-9]{2,4})?)\s*;?\s*(q=([0-9.]+))?/gi
21 | }
22 | const lngs = []; let i; let m
23 | const rgx = options.lookupHeaderRegex || lookupRegex
24 |
25 | do {
26 | m = rgx.exec(acceptLanguage)
27 | if (m) {
28 | const lng = m[1]; const weight = m[5] || '1'; const q = Number(weight)
29 | if (lng && !isNaN(q)) {
30 | lngs.push({ lng, q })
31 | }
32 | }
33 | } while (m)
34 |
35 | lngs.sort(function (a, b) {
36 | return b.q - a.q
37 | })
38 |
39 | for (i = 0; i < lngs.length; i++) {
40 | locales.push(lngs[i].lng)
41 | }
42 |
43 | if (locales.length) found = locales
44 | }
45 | }
46 |
47 | return found
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/languageLookups/path.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'path',
3 |
4 | lookup (req, res, options) {
5 | let found
6 |
7 | if (req === undefined) {
8 | return found
9 | }
10 |
11 | if (options.lookupPath !== undefined && req.params) {
12 | found = options.getParams(req)[options.lookupPath]
13 | }
14 |
15 | if (!found && typeof options.lookupFromPathIndex === 'number' && options.getOriginalUrl(req)) {
16 | const path = options.getOriginalUrl(req).split('?')[0]
17 | const parts = path.split('/')
18 | if (parts[0] === '') { // Handle paths that start with a slash, i.e., '/foo' -> ['', 'foo']
19 | parts.shift()
20 | }
21 |
22 | if (parts.length > options.lookupFromPathIndex) {
23 | found = parts[options.lookupFromPathIndex]
24 | }
25 | }
26 |
27 | return found
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/languageLookups/querystring.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'querystring',
3 |
4 | lookup (req, res, options) {
5 | let found
6 |
7 | if (options.lookupQuerystring !== undefined && typeof req !== 'undefined') {
8 | if (options.getQuery(req)) {
9 | found = options.getQuery(req)[options.lookupQuerystring]
10 | }
11 | if (!found && options.getUrl(req) && options.getUrl(req).indexOf('?')) {
12 | const lastPartOfUri = options.getUrl(req).substring(options.getUrl(req).indexOf('?'))
13 | if (typeof URLSearchParams !== 'undefined') {
14 | const urlParams = new URLSearchParams(lastPartOfUri)
15 | found = urlParams.get(options.lookupQuerystring)
16 | } else {
17 | const indexOfQsStart = lastPartOfUri.indexOf(`${options.lookupQuerystring}=`)
18 | if (indexOfQsStart > -1) {
19 | const restOfUri = lastPartOfUri.substring(options.lookupQuerystring.length + 2)
20 | if (restOfUri.indexOf('&') < 0) {
21 | found = restOfUri
22 | } else {
23 | found = restOfUri.substring(0, restOfUri.indexOf('&'))
24 | }
25 | }
26 | }
27 | }
28 | }
29 |
30 | return found
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/languageLookups/session.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'session',
3 |
4 | lookup (req, res, options) {
5 | let found
6 |
7 | if (options.lookupSession !== undefined && typeof req && options.getSession(req)) {
8 | found = options.getSession(req)[options.lookupSession]
9 | }
10 |
11 | return found
12 | },
13 |
14 | cacheUserLanguage (req, res, lng, options = {}) {
15 | if (options.lookupSession && req && options.getSession(req)) {
16 | options.getSession(req)[options.lookupSession] = lng
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | export function setPath (object, path, newValue) {
2 | let stack
3 | if (typeof path !== 'string') stack = [].concat(path)
4 | if (typeof path === 'string') stack = path.split('.')
5 |
6 | while (stack.length > 1) {
7 | let key = stack.shift()
8 | if (key.indexOf('###') > -1) key = key.replace(/###/g, '.')
9 | if (!object[key]) object[key] = {}
10 | object = object[key]
11 | }
12 |
13 | let key = stack.shift()
14 | if (key.indexOf('###') > -1) key = key.replace(/###/g, '.')
15 | object[key] = newValue
16 | }
17 |
18 | const arr = []
19 | const each = arr.forEach
20 | const slice = arr.slice
21 |
22 | export function defaults (obj) {
23 | each.call(slice.call(arguments, 1), function (source) {
24 | if (source) {
25 | for (const prop in source) {
26 | if (obj[prop] === undefined) obj[prop] = source[prop]
27 | }
28 | }
29 | })
30 | return obj
31 | }
32 |
33 | export function extend (obj) {
34 | each.call(slice.call(arguments, 1), function (source) {
35 | if (source) {
36 | for (const prop in source) {
37 | obj[prop] = source[prop]
38 | }
39 | }
40 | })
41 | return obj
42 | }
43 |
44 | export function removeLngFromUrl (url, lookupFromPathIndex) {
45 | let first = ''
46 | let pos = lookupFromPathIndex
47 |
48 | if (url[0] === '/') {
49 | pos++
50 | first = '/'
51 | }
52 |
53 | // Build new url
54 | const parts = url.split('/')
55 | parts.splice(pos, 1)
56 | url = parts.join('/')
57 | if (url[0] !== '/') url = first + url
58 |
59 | return url
60 | }
61 |
62 | export function escape (str) {
63 | return (str.replace(/&/g, '&')
64 | .replace(/"/g, '"')
65 | .replace(/'/g, ''')
66 | .replace(//g, '>')
68 | .replace(/\//g, '/')
69 | .replace(/\\/g, '\')
70 | .replace(/`/g, '`'))
71 | }
72 |
73 | export function hasXSS (input) {
74 | if (typeof input !== 'string') return false
75 |
76 | // Common XSS attack patterns
77 | const xssPatterns = [
78 | /<\s*script.*?>/i,
79 | /<\s*\/\s*script\s*>/i,
80 | /<\s*img.*?on\w+\s*=/i,
81 | /<\s*\w+\s*on\w+\s*=.*?>/i,
82 | /javascript\s*:/i,
83 | /vbscript\s*:/i,
84 | /expression\s*\(/i,
85 | /eval\s*\(/i,
86 | /alert\s*\(/i,
87 | /document\.cookie/i,
88 | /document\.write\s*\(/i,
89 | /window\.location/i,
90 | /innerHTML/i
91 | ]
92 |
93 | return xssPatterns.some((pattern) => pattern.test(input))
94 | }
95 |
--------------------------------------------------------------------------------
/licence:
--------------------------------------------------------------------------------
1 | Copyright (c) 2025 i18next
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i18next-http-middleware",
3 | "version": "3.7.4",
4 | "private": false,
5 | "type": "module",
6 | "main": "./cjs/index.js",
7 | "types": "./index.d.mts",
8 | "exports": {
9 | "./package.json": "./package.json",
10 | ".": {
11 | "types": {
12 | "require": "./cjs/index.d.ts",
13 | "import": "./esm/index.d.mts"
14 | },
15 | "module": "./esm/index.js",
16 | "import": "./esm/index.js",
17 | "require": "./cjs/index.js",
18 | "default": "./esm/index.js"
19 | },
20 | "./cjs": {
21 | "types": "./cjs/index.d.ts",
22 | "default": "./cjs/index.js"
23 | },
24 | "./esm": {
25 | "types": "./esm/index.d.mts",
26 | "default": "./esm/index.js"
27 | }
28 | },
29 | "module": "./esm/index.js",
30 | "devDependencies": {
31 | "@babel/cli": "7.25.9",
32 | "@babel/core": "7.26.0",
33 | "@babel/preset-env": "7.26.0",
34 | "@hapi/hapi": "^21.3.12",
35 | "@types/express-serve-static-core": "^5.0.1",
36 | "@koa/router": "12.0.1",
37 | "koa": "2.16.1",
38 | "babel-plugin-add-module-exports": "1.0.4",
39 | "eslint": "8.53.0",
40 | "eslint-config-standard": "17.1.0",
41 | "eslint-plugin-import": "2.31.0",
42 | "eslint-plugin-n": "16.6.2",
43 | "eslint-plugin-promise": "6.6.0",
44 | "eslint-plugin-require-path-exists": "1.1.9",
45 | "eslint-plugin-standard": "5.0.0",
46 | "expect.js": "0.3.1",
47 | "express": "4.21.1",
48 | "fastify": "5.3.2",
49 | "i18next": "24.0.0",
50 | "mocha": "10.8.2",
51 | "supertest": "7.0.0",
52 | "tsd": "0.31.2",
53 | "uglify-js": "3.19.3"
54 | },
55 | "description": "i18next-http-middleware is a middleware to be used with Node.js web frameworks like express or Fastify and also for Deno.",
56 | "keywords": [
57 | "i18next",
58 | "i18next-backend",
59 | "i18next-http-middleware",
60 | "express"
61 | ],
62 | "homepage": "https://github.com/i18next/i18next-http-middleware",
63 | "repository": {
64 | "type": "git",
65 | "url": "git@github.com:i18next/i18next-http-middleware.git"
66 | },
67 | "bugs": {
68 | "url": "https://github.com/i18next/i18next-http-middleware/issues"
69 | },
70 | "license": "MIT",
71 | "scripts": {
72 | "lint": "eslint .",
73 | "compile:esm": "rm -rf esm && mkdir esm && BABEL_ENV=esm babel lib -d esm && cp index.d.ts esm/index.d.ts && cp index.d.mts esm/index.d.mts",
74 | "compile:cjs": "rm -rf cjs && mkdir cjs && BABEL_ENV=cjs babel lib -d cjs && cp index.d.ts cjs/index.d.ts && echo '{\"type\":\"commonjs\"}' > cjs/package.json",
75 | "compile": "npm run compile:esm && npm run compile:cjs",
76 | "build": "npm run compile",
77 | "test": "npm run lint && npm run build && mocha test -R spec --exit --experimental-modules && npm run test:types",
78 | "test:deno": "deno test --allow-net test/deno/*.js",
79 | "test:types": "tsd",
80 | "preversion": "npm run test && npm run build && git push",
81 | "postversion": "git push && git push --tags"
82 | },
83 | "tsd": {
84 | "directory": "test/types"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/addRoute.custom.js:
--------------------------------------------------------------------------------
1 | // import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 |
5 | i18next.init({
6 | fallbackLng: 'en',
7 | preload: ['en', 'de'],
8 | saveMissing: true
9 | })
10 |
11 | describe('addRoute custom framework', () => {
12 | describe('and handling a request', () => {
13 | it('should return the appropriate resource', (done) => {
14 | const app = { get: (route, fn) => {} }
15 | const handle = (req, res) => {}
16 | i18nextMiddleware.addRoute(i18next, '/myroute/:lng/:ns', ['en'], app, 'get', handle)
17 | done()
18 | })
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/test/addRoute.express.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import express from 'express'
5 | import request from 'supertest'
6 |
7 | i18next.init({
8 | fallbackLng: 'en',
9 | preload: ['en', 'de'],
10 | saveMissing: true
11 | })
12 |
13 | describe('addRoute express', () => {
14 | describe('and handling a request', () => {
15 | const app = express()
16 | let server
17 |
18 | before((done) => {
19 | server = app.listen(7001, done)
20 | })
21 | after((done) => server.close(done))
22 |
23 | it('should return the appropriate resource', (done) => {
24 | app.use(i18nextMiddleware.handle(i18next))
25 | const handle = (req, res) => {
26 | expect(req).to.have.property('lng', 'en')
27 | expect(req).to.have.property('locale', 'en')
28 | expect(req).to.have.property('language', 'en')
29 | expect(req).to.have.property('languages')
30 | expect(req.languages).to.eql(['en'])
31 | expect(req).to.have.property('i18n')
32 | expect(req).to.have.property('t')
33 | expect(req.t('key')).to.eql('key')
34 | res.send(req.t('key'))
35 | }
36 | i18nextMiddleware.addRoute(i18next, '/myroute/:lng/:ns', ['en'], app, 'get', handle)
37 |
38 | request(app)
39 | .get('/myroute/en/test')
40 | .expect('Content-Language', 'en')
41 | .expect(200, (err, res) => {
42 | expect(err).not.to.be.ok()
43 | expect(res.text).to.eql('key')
44 | done()
45 | })
46 | })
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/test/addRoute.fastify.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import fastify from 'fastify'
5 |
6 | i18next.init({
7 | fallbackLng: 'en',
8 | preload: ['en', 'de'],
9 | saveMissing: true
10 | })
11 |
12 | describe('addRoute fastify', () => {
13 | describe('and handling a request', () => {
14 | it('should return the appropriate resource', (done) => {
15 | const app = fastify()
16 | app.register(i18nextMiddleware.plugin, { i18next })
17 | // app.addHook('preHandler', i18nextMiddleware.handle(i18next))
18 | const handle = (req, res) => {
19 | expect(req).to.have.property('lng', 'en')
20 | expect(req).to.have.property('locale', 'en')
21 | expect(req).to.have.property('language', 'en')
22 | expect(req).to.have.property('languages')
23 | expect(req.languages).to.eql(['en'])
24 | expect(req).to.have.property('i18n')
25 | expect(req).to.have.property('t')
26 | expect(req.t('key')).to.eql('key')
27 | res.send(req.t('key'))
28 | }
29 | i18nextMiddleware.addRoute(i18next, '/myroute/:lng/:ns', ['en'], app, 'get', handle)
30 |
31 | app.inject({
32 | method: 'GET',
33 | url: '/myroute/en/test'
34 | }, (err, res) => {
35 | expect(err).not.to.be.ok()
36 | expect(res.headers).to.property('content-language', 'en')
37 | expect(res.payload).to.eql('key')
38 | done()
39 | })
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/test/addRoute.koa.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import Koa from 'koa'
5 | import Router from '@koa/router'
6 | import request from 'supertest'
7 |
8 | const router = Router()
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true
13 | })
14 |
15 | describe('addRoute koa', () => {
16 | describe('and handling a request', () => {
17 | const app = new Koa()
18 | app.use(i18nextMiddleware.koaPlugin(i18next))
19 | let server
20 |
21 | before((done) => {
22 | server = app.listen(7002, done)
23 | })
24 | after((done) => server.close(done))
25 |
26 | it('should return the appropriate resource', (done) => {
27 | app.use(i18nextMiddleware.handle(i18next))
28 | const handle = (ctx) => {
29 | expect(ctx).to.have.property('lng', 'en')
30 | expect(ctx).to.have.property('locale', 'en')
31 | expect(ctx).to.have.property('language', 'en')
32 | expect(ctx).to.have.property('languages')
33 | expect(ctx.languages).to.eql(['en'])
34 | expect(ctx).to.have.property('i18n')
35 | expect(ctx).to.have.property('t')
36 | expect(ctx.t('key')).to.eql('key')
37 | ctx.body = ctx.t('key')
38 | }
39 | i18nextMiddleware.addRoute(i18next, '/myroute/:lng/:ns', ['en'], router, 'get', handle)
40 |
41 | app.use(router.routes())
42 |
43 | request(server)
44 | .get('/myroute/en/test')
45 | .expect('Content-Language', 'en')
46 | .expect(200, (err, res) => {
47 | expect(err).not.to.be.ok()
48 | expect(res.text).to.eql('key')
49 | done()
50 | })
51 | })
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/test/deno/addRoute.abc.js:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertNotEquals } from 'https://deno.land/std/testing/asserts.ts'
2 | import i18next from 'https://deno.land/x/i18next/index.js'
3 | import { Application } from 'https://deno.land/x/abc/mod.ts'
4 | import i18nextMiddleware from '../../index.js'
5 | const { test } = Deno
6 |
7 | test('addRoute abc', async () => {
8 | // before
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true
13 | })
14 | const app = new Application()
15 | const handle = i18nextMiddleware.handle(i18next)
16 | app.use((next) =>
17 | (c) => {
18 | handle(c.request, c.response, () => {})
19 | return next(c)
20 | }
21 | )
22 |
23 | // test
24 | const routeHandle = (c) => {
25 | assertEquals(c.request.lng, 'en')
26 | assertEquals(c.request.locale, 'en')
27 | assertEquals(c.request.language, 'en')
28 | assertEquals(c.request.languages, ['en'])
29 | assertNotEquals(c.request.i18n, undefined)
30 | assertNotEquals(c.request.t, undefined)
31 |
32 | assertEquals(c.request.t('key'), 'key')
33 | return c.request.t('key')
34 | }
35 | i18nextMiddleware.addRoute(i18next, '/myroute/:lng/:ns', ['en'], app, 'get', routeHandle)
36 | await app.start({ port: 7001 })
37 |
38 | const res = await fetch('http://localhost:7001/myroute/en/test')
39 | assertEquals(res.status, 200)
40 | assertEquals(await res.text(), 'key')
41 | assertEquals(res.headers.get('content-language'), 'en')
42 |
43 | // after
44 | await app.close()
45 | })
46 |
--------------------------------------------------------------------------------
/test/deno/getResourcesHandler.abc.js:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertNotEquals } from 'https://deno.land/std/testing/asserts.ts'
2 | import i18next from 'https://deno.land/x/i18next/index.js'
3 | import { Application } from 'https://deno.land/x/abc/mod.ts'
4 | import i18nextMiddleware from '../../index.js'
5 | const { test } = Deno
6 |
7 | test('getResourcesHandler abc', async () => {
8 | // before
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true,
13 | resources: {
14 | en: {
15 | translation: { hi: 'there' }
16 | }
17 | }
18 | })
19 | const app = new Application()
20 | app.get('/', i18nextMiddleware.getResourcesHandler(i18next))
21 | await app.start({ port: 7002 })
22 |
23 | // test
24 | const res = await fetch('http://localhost:7002?lng=en&ns=translation')
25 | assertEquals(res.status, 200)
26 | assertEquals(res.headers.get('cache-control'), 'no-cache')
27 | assertEquals(res.headers.get('pragma'), 'no-cache')
28 | assertEquals(await res.json(), {
29 | en: {
30 | translation: { hi: 'there' }
31 | }
32 | })
33 |
34 | // after
35 | await app.close()
36 | })
37 |
--------------------------------------------------------------------------------
/test/deno/middleware.abc.js:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertNotEquals } from 'https://deno.land/std/testing/asserts.ts'
2 | import i18next from 'https://deno.land/x/i18next/index.js'
3 | import { Application } from 'https://deno.land/x/abc/mod.ts'
4 | import i18nextMiddleware from '../../index.js'
5 | const { test } = Deno
6 |
7 | test('middleware abc', async () => {
8 | // before
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true
13 | })
14 | const app = new Application()
15 | const handle = i18nextMiddleware.handle(i18next)
16 | app.use((next) =>
17 | (c) => {
18 | handle(c)
19 | return next(c)
20 | }
21 | )
22 |
23 | // test
24 | app.get('/', (c) => {
25 | assertEquals(c.request.lng, 'en')
26 | assertEquals(c.request.locale, 'en')
27 | assertEquals(c.request.language, 'en')
28 | assertEquals(c.request.languages, ['en'])
29 | assertNotEquals(c.request.i18n, undefined)
30 | assertNotEquals(c.request.t, undefined)
31 |
32 | assertEquals(c.request.t('key'), 'key')
33 | return c.request.t('key')
34 | })
35 | await app.start({ port: 7001 })
36 |
37 | const res = await fetch('http://localhost:7001/')
38 | assertEquals(res.status, 200)
39 | assertEquals(await res.text(), 'key')
40 | assertEquals(res.headers.get('content-language'), 'en')
41 |
42 | // after
43 | await app.close()
44 | })
45 |
--------------------------------------------------------------------------------
/test/deno/missingKeyHandler.abc.js:
--------------------------------------------------------------------------------
1 | // import { assertEquals, assertNotEquals } from 'https://deno.land/std/testing/asserts.ts'
2 | // import i18next from 'https://deno.land/x/i18next/index.js'
3 | // import { Application } from 'https://deno.land/x/abc/mod.ts'
4 | // import i18nextMiddleware from '../../index.js'
5 | // const { test } = Deno
6 |
7 | // test('missingKeyHandler abc', async () => {
8 | // // before
9 | // i18next.init({
10 | // fallbackLng: 'en',
11 | // preload: ['en', 'de'],
12 | // saveMissing: true,
13 | // resources: {
14 | // en: {
15 | // translation: { hi: 'there' }
16 | // }
17 | // }
18 | // })
19 | // const app = new Application()
20 | // app.post('/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next))
21 | // await app.start({ port: 7002 })
22 |
23 | // // test
24 | // const res = await fetch('http://localhost:7002/en/translation', {
25 | // method: 'POST',
26 | // headers: {
27 | // 'Content-Type': 'application/json'
28 | // },
29 | // body: JSON.stringify({ miss: 'key' })
30 | // })
31 | // assertEquals(res.status, 200)
32 | // assertEquals(await res.text(), 'ok')
33 |
34 | // // after
35 | // await app.close()
36 | // })
37 |
--------------------------------------------------------------------------------
/test/getResourcesHandler.custom.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 |
5 | i18next.init({
6 | fallbackLng: 'en',
7 | preload: ['en', 'de'],
8 | saveMissing: true,
9 | resources: {
10 | en: {
11 | translation: { hi: 'there' }
12 | }
13 | }
14 | })
15 |
16 | describe('getResourcesHandler custom framework', () => {
17 | describe('handling a request', () => {
18 | const handle = i18nextMiddleware.getResourcesHandler(i18next, {
19 | getQuery: (req) => req.q,
20 | setContentType: (res, type) => {
21 | res.type = type
22 | },
23 | setHeader: (res, name, value) => {
24 | res.h[name] = value
25 | },
26 | send: (res, body) => res._send(res, body)
27 | })
28 |
29 | it('should return the appropriate resource', (done) => {
30 | const req = {
31 | q: {
32 | lng: 'en',
33 | ns: 'translation'
34 | }
35 | }
36 | const res = {
37 | h: {},
38 | _send: (res, body) => {
39 | expect(res).to.have.property('type', 'application/json')
40 | expect(res).to.have.property('h')
41 | expect(res.h).to.have.property('Cache-Control', 'no-cache')
42 | expect(res.h).to.have.property('Pragma', 'no-cache')
43 | expect(body).to.have.property('en')
44 | expect(body.en).to.have.property('translation')
45 | expect(body.en.translation).to.have.property('hi', 'there')
46 | done()
47 | }
48 | }
49 | handle(req, res)
50 | })
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/test/getResourcesHandler.express.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import express from 'express'
5 | import request from 'supertest'
6 |
7 | i18next.init({
8 | fallbackLng: 'en',
9 | preload: ['en', 'de'],
10 | saveMissing: true,
11 | resources: {
12 | en: {
13 | translation: { hi: 'there' }
14 | }
15 | }
16 | })
17 |
18 | describe('getResourcesHandler express', () => {
19 | describe('handling a request', () => {
20 | const app = express()
21 | let server
22 |
23 | before((done) => {
24 | server = app.listen(7001, done)
25 | })
26 | after((done) => server.close(done))
27 |
28 | it('should return the appropriate resource', (done) => {
29 | app.get('/', i18nextMiddleware.getResourcesHandler(i18next))
30 |
31 | request(app)
32 | .get('/')
33 | .query({
34 | lng: 'en',
35 | ns: 'translation'
36 | })
37 | .expect('content-type', /json/)
38 | .expect('cache-control', 'no-cache')
39 | .expect('pragma', 'no-cache')
40 | .expect(200, (err, res) => {
41 | expect(err).not.to.be.ok()
42 | expect(res.body).to.have.property('en')
43 | expect(res.body.en).to.have.property('translation')
44 | expect(res.body.en.translation).to.have.property('hi', 'there')
45 | done()
46 | })
47 | })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/test/getResourcesHandler.fastify.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import fastify from 'fastify'
5 |
6 | i18next.init({
7 | fallbackLng: 'en',
8 | preload: ['en', 'de'],
9 | saveMissing: true,
10 | resources: {
11 | en: {
12 | translation: { hi: 'there' }
13 | }
14 | }
15 | })
16 |
17 | describe('getResourcesHandler fastify', () => {
18 | describe('handling a request', () => {
19 | it('should return the appropriate resource', (done) => {
20 | const app = fastify()
21 | app.get('/', i18nextMiddleware.getResourcesHandler(i18next))
22 |
23 | app.inject({
24 | method: 'GET',
25 | url: '/',
26 | query: {
27 | lng: 'en',
28 | ns: 'translation'
29 | }
30 | }, (err, res) => {
31 | expect(err).not.to.be.ok()
32 | expect(res.headers).to.property('content-type', 'application/json; charset=utf-8')
33 | expect(res.headers).to.property('cache-control', 'no-cache')
34 | expect(res.headers).to.property('pragma', 'no-cache')
35 | expect(res.json()).to.have.property('en')
36 | expect(res.json().en).to.have.property('translation')
37 | expect(res.json().en.translation).to.have.property('hi', 'there')
38 | done()
39 | })
40 | })
41 | })
42 |
43 | describe('handling a request with route params instead of query', () => {
44 | it('should return the appropriate resource', (done) => {
45 | const app = fastify()
46 | app.get('/locales/:lng/:ns', i18nextMiddleware.getResourcesHandler(i18next))
47 |
48 | app.inject({
49 | method: 'GET',
50 | url: '/locales/en/translation',
51 | query: {}
52 | }, (err, res) => {
53 | expect(err).not.to.be.ok()
54 | expect(res.headers).to.property('content-type', 'application/json; charset=utf-8')
55 | expect(res.headers).to.property('cache-control', 'no-cache')
56 | expect(res.headers).to.property('pragma', 'no-cache')
57 | expect(res.json()).to.have.property('en')
58 | expect(res.json().en).to.have.property('translation')
59 | expect(res.json().en.translation).to.have.property('hi', 'there')
60 | done()
61 | })
62 | })
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/test/getResourcesHandler.koa.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import Koa from 'koa'
5 | import Router from '@koa/router'
6 | import request from 'supertest'
7 |
8 | const router = Router()
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true,
13 | resources: {
14 | en: {
15 | translation: { hi: 'there' }
16 | }
17 | }
18 | })
19 |
20 | describe('getResourcesHandler koa', () => {
21 | describe('handling a request', () => {
22 | const app = new Koa()
23 | let server
24 |
25 | before((done) => {
26 | server = app.listen(7002, done)
27 | })
28 | after((done) => server.close(done))
29 |
30 | it('should return the appropriate resource', (done) => {
31 | router.get('/', i18nextMiddleware.getResourcesHandler(i18next))
32 |
33 | app.use(router.routes())
34 |
35 | request(server)
36 | .get('/')
37 | .query({
38 | lng: 'en',
39 | ns: 'translation'
40 | })
41 | .expect('content-type', /json/)
42 | .expect('cache-control', 'no-cache')
43 | .expect('pragma', 'no-cache')
44 | .expect(200, (err, res) => {
45 | expect(err).not.to.be.ok()
46 | expect(res.body).to.have.property('en')
47 | expect(res.body.en).to.have.property('translation')
48 | expect(res.body.en.translation).to.have.property('hi', 'there')
49 | done()
50 | })
51 | })
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/test/languageDetector.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18next from 'i18next'
3 | import LanguageDetector from '../lib/LanguageDetector.js'
4 |
5 | i18next.init()
6 |
7 | describe('language detector', () => {
8 | const ld = new LanguageDetector(i18next.services, { order: ['session', 'querystring', 'path', 'cookie', 'header'], cookieSameSite: 'none' })
9 |
10 | describe('cookie', () => {
11 | it('detect', () => {
12 | const req = {
13 | headers: {
14 | cookie: 'i18next=de'
15 | }
16 | }
17 | const res = {}
18 | const lng = ld.detect(req, res)
19 | expect(lng).to.eql('de')
20 | // expect(res).to.eql({})
21 | })
22 |
23 | it('shouldn\'t fail on URI malformed from cookie content', () => {
24 | const req = {
25 | headers: {
26 | cookie: 'i18next=%'
27 | }
28 | }
29 | const res = {}
30 | const lng = ld.detect(req, res)
31 | expect(lng).to.eql('%')
32 | })
33 |
34 | it('cacheUserLanguage', () => {
35 | const req = {}
36 | const res = {
37 | headers: {
38 | 'Set-Cookie': 'my=cookie'
39 | }
40 | }
41 | res.header = (name, value) => { res.headers[name] = value }
42 | ld.cacheUserLanguage(req, res, 'it', ['cookie'])
43 | expect(req).to.eql({})
44 | expect(res).to.have.property('headers')
45 | expect(res.headers).to.have.property('Set-Cookie')
46 | expect(res.headers['Set-Cookie']).to.match(/i18next=it/)
47 | expect(res.headers['Set-Cookie']).to.match(/Path=\//)
48 | expect(res.headers['Set-Cookie']).to.match(/my=cookie/)
49 | expect(res.headers['Set-Cookie']).to.match(/SameSite=None/)
50 | })
51 | })
52 |
53 | describe('header', () => {
54 | it('detect', () => {
55 | const req = {
56 | headers: {
57 | 'accept-language': 'de'
58 | }
59 | }
60 | const res = {}
61 | const lng = ld.detect(req, res)
62 | expect(lng).to.eql('de')
63 | // expect(res).to.eql({})
64 | })
65 |
66 | it('detect special', () => {
67 | const req = {
68 | headers: {
69 | 'accept-language': 'zh-Hans'
70 | }
71 | }
72 | const res = {}
73 | const lng = ld.detect(req, res)
74 | expect(lng).to.eql('zh-Hans')
75 | // expect(res).to.eql({})
76 | })
77 |
78 | it('detect 3 char lngs', () => {
79 | const req = {
80 | headers: {
81 | 'accept-language': 'haw-US'
82 | }
83 | }
84 | const res = {}
85 | const lng = ld.detect(req, res)
86 | expect(lng).to.eql('haw-US')
87 | // expect(res).to.eql({})
88 | })
89 |
90 | it('detect with custom regex', () => {
91 | const req = {
92 | headers: {
93 | 'accept-language': 'zh-Hans'
94 | }
95 | }
96 | const res = {}
97 | const ldCustom = new LanguageDetector(i18next.services, { order: ['header'], lookupHeaderRegex: /(([a-z]{4})-?([A-Z]{2})?)\s*;?\s*(q=([0-9.]+))?/gi })
98 | const lng = ldCustom.detect(req, res)
99 | expect(lng).to.eql('Hans')
100 | // expect(res).to.eql({})
101 | })
102 |
103 | it('detect region with numbers', () => {
104 | const req = {
105 | headers: {
106 | 'accept-language': 'es-419'
107 | }
108 | }
109 | const res = {}
110 | const lng = ld.detect(req, res)
111 | expect(lng).to.eql('es-419')
112 | // expect(res).to.eql({})
113 | })
114 |
115 | it('parses weight correctly', () => {
116 | const req = {
117 | headers: {
118 | 'accept-language': 'pt;q=0.9,es-419;q=0.8,en;q=0.7'
119 | }
120 | }
121 | const res = {}
122 | const lng = ld.detect(req, res)
123 | expect(lng).to.eql('pt')
124 | // expect(res).to.eql({})
125 | })
126 |
127 | it('parses weight out of order correctly', () => {
128 | const req = {
129 | headers: {
130 | 'accept-language': 'es-419;q=0.7,en;q=0.8,pt;q=0.9'
131 | }
132 | }
133 | const res = {}
134 | const lng = ld.detect(req, res)
135 | expect(lng).to.eql('pt')
136 | // expect(res).to.eql({})
137 | })
138 |
139 | it('sets weight to 1 as default', () => {
140 | const req = {
141 | headers: {
142 | 'accept-language': 'pt-BR,pt;q=0.9,es-419;q=0.8,en;q=0.7'
143 | }
144 | }
145 | const res = {}
146 | const lng = ld.detect(req, res)
147 | expect(lng).to.eql('pt-BR')
148 | // expect(res).to.eql({})
149 | })
150 | })
151 |
152 | describe('path', () => {
153 | it('detect', () => {
154 | const req = {
155 | url: '/fr-fr/some/route'
156 | }
157 | const res = {}
158 | const lng = ld.detect(req, res)
159 | expect(lng).to.eql('fr-FR')
160 | // expect(res).to.eql({})
161 | })
162 | })
163 |
164 | describe('querystring', () => {
165 | it('detect', () => {
166 | const req = {
167 | url: '/fr/some/route?lng=de'
168 | }
169 | const res = {}
170 | const lng = ld.detect(req, res)
171 | expect(lng).to.eql('de')
172 | // expect(res).to.eql({})
173 | })
174 | })
175 |
176 | describe('session', () => {
177 | it('detect', () => {
178 | const req = {
179 | session: {
180 | lng: 'de'
181 | }
182 | }
183 | const res = {}
184 | const lng = ld.detect(req, res)
185 | expect(lng).to.eql('de')
186 | // expect(res).to.eql({})
187 | })
188 |
189 | it('cacheUserLanguage', () => {
190 | const req = {
191 | session: {
192 | lng: 'de'
193 | }
194 | }
195 | const res = {}
196 | ld.cacheUserLanguage(req, res, 'it', ['session'])
197 | expect(req).to.have.property('session')
198 | expect(req.session).to.have.property('lng', 'it')
199 | // expect(res).to.eql({})
200 | })
201 | })
202 | })
203 |
204 | describe('language detector (ISO 15897 locales)', () => {
205 | const ld = new LanguageDetector(i18next.services, { order: ['session', 'querystring', 'path', 'cookie', 'header'], cookieSameSite: 'none', convertDetectedLanguage: 'Iso15897' })
206 |
207 | describe('cookie', () => {
208 | it('detect', () => {
209 | const req = {
210 | headers: {
211 | cookie: 'i18next=de-CH'
212 | }
213 | }
214 | const res = {}
215 | const lng = ld.detect(req, res)
216 | expect(lng).to.eql('de_CH')
217 | // expect(res).to.eql({})
218 | })
219 |
220 | it('shouldn\'t fail on URI malformed from cookie content', () => {
221 | const req = {
222 | headers: {
223 | cookie: 'i18next=%'
224 | }
225 | }
226 | const res = {}
227 | const lng = ld.detect(req, res)
228 | expect(lng).to.eql('%')
229 | })
230 |
231 | it('cacheUserLanguage', () => {
232 | const req = {}
233 | const res = {
234 | headers: {
235 | 'Set-Cookie': 'my=cookie'
236 | }
237 | }
238 | res.header = (name, value) => { res.headers[name] = value }
239 | ld.cacheUserLanguage(req, res, 'it_IT', ['cookie'])
240 | expect(req).to.eql({})
241 | expect(res).to.have.property('headers')
242 | expect(res.headers).to.have.property('Set-Cookie')
243 | expect(res.headers['Set-Cookie']).to.match(/i18next=it_IT/)
244 | expect(res.headers['Set-Cookie']).to.match(/Path=\//)
245 | expect(res.headers['Set-Cookie']).to.match(/my=cookie/)
246 | expect(res.headers['Set-Cookie']).to.match(/SameSite=None/)
247 | })
248 | })
249 |
250 | describe('header', () => {
251 | it('detect', () => {
252 | const req = {
253 | headers: {
254 | 'accept-language': 'de-DE'
255 | }
256 | }
257 | const res = {}
258 | const lng = ld.detect(req, res)
259 | expect(lng).to.eql('de_DE')
260 | // expect(res).to.eql({})
261 | })
262 |
263 | it('detect special', () => {
264 | const req = {
265 | headers: {
266 | 'accept-language': 'zh-Hans'
267 | }
268 | }
269 | const res = {}
270 | const lng = ld.detect(req, res)
271 | expect(lng).to.eql('zh_Hans')
272 | // expect(res).to.eql({})
273 | })
274 |
275 | it('detect 3 char lngs', () => {
276 | const req = {
277 | headers: {
278 | 'accept-language': 'haw-US'
279 | }
280 | }
281 | const res = {}
282 | const lng = ld.detect(req, res)
283 | expect(lng).to.eql('haw_US')
284 | // expect(res).to.eql({})
285 | })
286 |
287 | it('detect with custom regex', () => {
288 | const req = {
289 | headers: {
290 | 'accept-language': 'zh-Hans'
291 | }
292 | }
293 | const res = {}
294 | const ldCustom = new LanguageDetector(i18next.services, { order: ['header'], lookupHeaderRegex: /(([a-z]{4})-?([A-Z]{2})?)\s*;?\s*(q=([0-9.]+))?/gi })
295 | const lng = ldCustom.detect(req, res)
296 | expect(lng).to.eql('Hans')
297 | // expect(res).to.eql({})
298 | })
299 |
300 | it('detect region with numbers', () => {
301 | const req = {
302 | headers: {
303 | 'accept-language': 'es-419'
304 | }
305 | }
306 | const res = {}
307 | const lng = ld.detect(req, res)
308 | expect(lng).to.eql('es_419')
309 | // expect(res).to.eql({})
310 | })
311 |
312 | it('parses weight correctly', () => {
313 | const req = {
314 | headers: {
315 | 'accept-language': 'pt-PT;q=0.9,es-419;q=0.8,en;q=0.7'
316 | }
317 | }
318 | const res = {}
319 | const lng = ld.detect(req, res)
320 | expect(lng).to.eql('pt_PT')
321 | // expect(res).to.eql({})
322 | })
323 |
324 | it('parses weight out of order correctly', () => {
325 | const req = {
326 | headers: {
327 | 'accept-language': 'es-419;q=0.7,en;q=0.8,pt-PT;q=0.9'
328 | }
329 | }
330 | const res = {}
331 | const lng = ld.detect(req, res)
332 | expect(lng).to.eql('pt_PT')
333 | // expect(res).to.eql({})
334 | })
335 |
336 | it('sets weight to 1 as default', () => {
337 | const req = {
338 | headers: {
339 | 'accept-language': 'pt-BR,pt;q=0.9,es-419;q=0.8,en;q=0.7'
340 | }
341 | }
342 | const res = {}
343 | const lng = ld.detect(req, res)
344 | expect(lng).to.eql('pt_BR')
345 | // expect(res).to.eql({})
346 | })
347 | })
348 |
349 | describe('path', () => {
350 | it('detect', () => {
351 | const req = {
352 | url: '/fr-fr/some/route'
353 | }
354 | const res = {}
355 | const lng = ld.detect(req, res)
356 | expect(lng).to.eql('fr_fr')
357 | // expect(res).to.eql({})
358 | })
359 | })
360 |
361 | describe('querystring', () => {
362 | it('detect', () => {
363 | const req = {
364 | url: '/fr/some/route?lng=de-CH'
365 | }
366 | const res = {}
367 | const lng = ld.detect(req, res)
368 | expect(lng).to.eql('de_CH')
369 | // expect(res).to.eql({})
370 | })
371 | })
372 |
373 | describe('session', () => {
374 | it('detect', () => {
375 | const req = {
376 | session: {
377 | lng: 'de-AT'
378 | }
379 | }
380 | const res = {}
381 | const lng = ld.detect(req, res)
382 | expect(lng).to.eql('de_AT')
383 | // expect(res).to.eql({})
384 | })
385 |
386 | it('cacheUserLanguage', () => {
387 | const req = {
388 | session: {
389 | lng: 'de-DE'
390 | }
391 | }
392 | const res = {}
393 | ld.cacheUserLanguage(req, res, 'it', ['session'])
394 | expect(req).to.have.property('session')
395 | expect(req.session).to.have.property('lng', 'it')
396 | // expect(res).to.eql({})
397 | })
398 | })
399 | })
400 |
401 | describe('language detector (with xss filter)', () => {
402 | const ld = new LanguageDetector(i18next.services, { order: ['cookie', 'header'], cookieSameSite: 'none' })
403 |
404 | it('detect', () => {
405 | const req = {
406 | headers: {
407 | cookie: 'i18next=de-">',
408 | 'accept-language': 'fr-CH'
409 | }
410 | }
411 | const res = {}
412 | const lng = ld.detect(req, res)
413 | expect(lng).to.eql('fr-CH')
414 | // expect(res).to.eql({})
415 | })
416 | })
417 |
--------------------------------------------------------------------------------
/test/middleware.custom.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 |
5 | i18next.init({
6 | fallbackLng: 'en',
7 | preload: ['en', 'de'],
8 | saveMissing: true
9 | })
10 |
11 | describe('middleware custom framework', () => {
12 | describe('handling an empty request', () => {
13 | const handle = i18nextMiddleware.handle(i18next, {})
14 |
15 | it('should extend request and response', (done) => {
16 | const req = {}
17 | const res = {}
18 |
19 | handle(req, res, () => {
20 | expect(req).to.have.property('lng', 'en')
21 | expect(req).to.have.property('locale', 'en')
22 | expect(req).to.have.property('resolvedLanguage', 'en')
23 | expect(req).to.have.property('language', 'en')
24 | expect(req).to.have.property('languages')
25 | expect(req.languages).to.eql(['en'])
26 | expect(req).to.have.property('i18n')
27 | expect(req).to.have.property('t')
28 | expect(res).to.eql({})
29 |
30 | expect(req.t('key')).to.eql('key')
31 | done()
32 | })
33 | })
34 | })
35 |
36 | describe('having possibility to set headers', () => {
37 | const handle = i18nextMiddleware.handle(i18next, {
38 | setHeader: (res, name, value) => {
39 | res.hdr[name] = value
40 | }
41 | })
42 |
43 | it('should extend request and response', (done) => {
44 | const req = {}
45 | const res = { hdr: {} }
46 |
47 | handle(req, res, () => {
48 | expect(req).to.have.property('lng', 'en')
49 | expect(req).to.have.property('locale', 'en')
50 | expect(req).to.have.property('resolvedLanguage', 'en')
51 | expect(req).to.have.property('language', 'en')
52 | expect(req).to.have.property('languages')
53 | expect(req.languages).to.eql(['en'])
54 | expect(req).to.have.property('i18n')
55 | expect(req).to.have.property('t')
56 | expect(res).to.eql({
57 | hdr: {
58 | 'Content-Language': 'en'
59 | }
60 | })
61 |
62 | expect(req.t('key')).to.eql('key')
63 | done()
64 | })
65 | })
66 | })
67 |
68 | describe('ignoreRoutes', () => {
69 | const handle = i18nextMiddleware.handle(i18next, {
70 | ignoreRoutes: ['/to-ignore'],
71 | getPath: (req) => req.p
72 | })
73 |
74 | it('should ignore routes', (done) => {
75 | const req = { p: '/to-ignore' }
76 | const res = {}
77 |
78 | handle(req, res, () => {
79 | expect(req).not.to.have.property('lng')
80 | expect(req).not.to.have.property('locale')
81 | expect(req).not.to.have.property('resolvedLanguage')
82 | expect(req).not.to.have.property('language')
83 | expect(req).not.to.have.property('languages')
84 | expect(req).not.to.have.property('i18n')
85 | expect(req).not.to.have.property('t')
86 | expect(res).to.eql({})
87 |
88 | done()
89 | })
90 | })
91 |
92 | it('should not ignore other routes', (done) => {
93 | const req = { p: '/' }
94 | const res = {}
95 |
96 | handle(req, res, () => {
97 | expect(req).to.have.property('lng', 'en')
98 | expect(req).to.have.property('locale', 'en')
99 | expect(req).to.have.property('resolvedLanguage', 'en')
100 | expect(req).to.have.property('language', 'en')
101 | expect(req).to.have.property('languages')
102 | expect(req.languages).to.eql(['en'])
103 | expect(req).to.have.property('i18n')
104 | expect(req).to.have.property('t')
105 | expect(res).to.eql({})
106 |
107 | expect(req.t('key')).to.eql('key')
108 | done()
109 | })
110 | })
111 | })
112 | })
113 |
--------------------------------------------------------------------------------
/test/middleware.express.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import express from 'express'
5 | import request from 'supertest'
6 |
7 | i18next.init({
8 | fallbackLng: 'en',
9 | preload: ['en', 'de'],
10 | saveMissing: true
11 | })
12 |
13 | describe('middleware express', () => {
14 | describe('handling an empty request', () => {
15 | const app = express()
16 | app.use(i18nextMiddleware.handle(i18next))
17 | let server
18 |
19 | before((done) => {
20 | server = app.listen(7001, done)
21 | })
22 | after((done) => server.close(done))
23 |
24 | it('should extend request and response', (done) => {
25 | app.get('/', (req, res) => {
26 | expect(req).to.have.property('lng', 'en')
27 | expect(req).to.have.property('locale', 'en')
28 | expect(req).to.have.property('resolvedLanguage', 'en')
29 | expect(req).to.have.property('language', 'en')
30 | expect(req).to.have.property('languages')
31 | expect(req.languages).to.eql(['en'])
32 | expect(req).to.have.property('i18n')
33 | expect(req).to.have.property('t')
34 |
35 | expect(req.t('key')).to.eql('key')
36 | res.send(req.t('key'))
37 | })
38 |
39 | request(app)
40 | .get('/')
41 | .expect('Content-Language', 'en')
42 | .expect(200, (err, res) => {
43 | expect(err).not.to.be.ok()
44 | expect(res.text).to.eql('key')
45 |
46 | done()
47 | })
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/test/middleware.fastify.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import fastify from 'fastify'
5 |
6 | i18next.init({
7 | fallbackLng: 'en',
8 | preload: ['en', 'de'],
9 | saveMissing: true
10 | })
11 |
12 | describe('middleware fastify', () => {
13 | describe('handling an empty request', () => {
14 | it('should extend request and response', (done) => {
15 | const app = fastify()
16 | app.register(i18nextMiddleware.plugin, { i18next })
17 | // app.addHook('preHandler', i18nextMiddleware.handle(i18next))
18 |
19 | app.get('/', async (req, res) => {
20 | expect(req).to.have.property('lng', 'en')
21 | expect(req).to.have.property('locale', 'en')
22 | expect(req).to.have.property('resolvedLanguage', 'en')
23 | expect(req).to.have.property('language', 'en')
24 | expect(req).to.have.property('languages')
25 | expect(req.languages).to.eql(['en'])
26 | expect(req).to.have.property('i18n')
27 | expect(req).to.have.property('t')
28 |
29 | expect(req.t('key')).to.eql('key')
30 | return req.t('key')
31 | })
32 |
33 | app.inject({
34 | method: 'GET',
35 | url: '/'
36 | }, (err, res) => {
37 | expect(err).not.to.be.ok()
38 | expect(res.headers).to.property('content-language', 'en')
39 | expect(res.payload).to.eql('key')
40 |
41 | done()
42 | })
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/test/middleware.hapi.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import Hapi from '@hapi/hapi'
5 |
6 | i18next.init({
7 | fallbackLng: 'en',
8 | preload: ['en', 'de'],
9 | saveMissing: true
10 | })
11 |
12 | describe('middleware hapi', () => {
13 | describe('handling an empty request', () => {
14 | const app = Hapi.server()
15 |
16 | before(async () => {
17 | await app.register({
18 | plugin: i18nextMiddleware.hapiPlugin,
19 | options: {
20 | i18next
21 | }
22 | })
23 | await app.initialize()
24 | })
25 |
26 | it('should extend request and response', async () => {
27 | app.route({
28 | method: 'GET',
29 | path: '/',
30 | handler: async (req, h) => {
31 | expect(req).to.have.property('lng', 'en')
32 | expect(req).to.have.property('locale', 'en')
33 | expect(req).to.have.property('resolvedLanguage', 'en')
34 | expect(req).to.have.property('language', 'en')
35 | expect(req).to.have.property('languages')
36 | expect(req.languages).to.eql(['en'])
37 | expect(req).to.have.property('i18n')
38 | expect(req).to.have.property('t')
39 |
40 | expect(req.t('key')).to.eql('key')
41 | return req.t('key')
42 | }
43 | })
44 |
45 | const res = await app.inject({
46 | method: 'GET',
47 | url: '/'
48 | })
49 |
50 | expect(res.headers).to.property('content-language', 'en')
51 | expect(res.payload).to.eql('key')
52 | })
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/test/middleware.koa.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import Koa from 'koa'
5 | import Router from '@koa/router'
6 | import request from 'supertest'
7 |
8 | const router = Router()
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true
13 | })
14 |
15 | describe('middleware koa', () => {
16 | describe('handling an empty request', () => {
17 | const app = new Koa()
18 | app.use(i18nextMiddleware.koaPlugin(i18next))
19 | let server
20 |
21 | before((done) => {
22 | server = app.listen(7002, done)
23 | })
24 | after((done) => server.close(done))
25 |
26 | it('should extend request and response', (done) => {
27 | router.get('/', (ctx) => {
28 | expect(ctx).to.have.property('lng', 'en')
29 | expect(ctx).to.have.property('locale', 'en')
30 | expect(ctx).to.have.property('resolvedLanguage', 'en')
31 | expect(ctx).to.have.property('language', 'en')
32 | expect(ctx).to.have.property('languages')
33 | expect(ctx.languages).to.eql(['en'])
34 | expect(ctx).to.have.property('i18n')
35 | expect(ctx).to.have.property('t')
36 |
37 | expect(ctx.t('key')).to.eql('key')
38 | ctx.body = ctx.t('key')
39 | })
40 |
41 | app.use(router.routes())
42 |
43 | request(server)
44 | .get('/')
45 | .expect('Content-Language', 'en')
46 | .expect(200, (err, res) => {
47 | expect(err).not.to.be.ok()
48 | expect(res.text).to.eql('key')
49 |
50 | done()
51 | })
52 | })
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/test/missingKeyHandler.custom.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 |
5 | i18next.init({
6 | fallbackLng: 'en',
7 | preload: ['en', 'de'],
8 | saveMissing: true
9 | })
10 |
11 | describe('missingKeyHandler custom framework', () => {
12 | describe('handling a request', () => {
13 | const handle = i18nextMiddleware.missingKeyHandler(i18next, {
14 | getParams: (req) => req.p,
15 | send: (res, body) => res._send(res, body)
16 | })
17 |
18 | it('should work', (done) => {
19 | const req = {
20 | p: {
21 | lng: 'en',
22 | ns: 'translation'
23 | },
24 | body: { miss: 'key' }
25 | }
26 | const res = {
27 | _send: (res, body) => {
28 | expect(body).to.eql('ok')
29 | done()
30 | }
31 | }
32 | handle(req, res)
33 | })
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/test/missingKeyHandler.express.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import express from 'express'
5 | import request from 'supertest'
6 |
7 | i18next.init({
8 | fallbackLng: 'en',
9 | preload: ['en', 'de'],
10 | saveMissing: true
11 | })
12 |
13 | describe('missingKeyHandler express', () => {
14 | describe('handling a request', () => {
15 | const app = express()
16 | let server
17 |
18 | before((done) => {
19 | server = app.listen(7001, done)
20 | })
21 | after((done) => server.close(done))
22 |
23 | it('should work', (done) => {
24 | app.post('/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next))
25 |
26 | request(app)
27 | .post('/en/translation')
28 | .send({ miss: 'key' })
29 | .expect(200, (err, res) => {
30 | expect(err).not.to.be.ok()
31 | expect(res.text).to.eql('ok')
32 | done()
33 | })
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/test/missingKeyHandler.fastify.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import fastify from 'fastify'
5 |
6 | i18next.init({
7 | fallbackLng: 'en',
8 | preload: ['en', 'de'],
9 | saveMissing: true
10 | })
11 |
12 | describe('missingKeyHandler fastify', () => {
13 | describe('handling a request', () => {
14 | it('should work', (done) => {
15 | const app = fastify()
16 | app.post('/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next))
17 |
18 | app.inject({
19 | method: 'POST',
20 | url: '/en/translation',
21 | body: { miss: 'key' }
22 | }, (err, res) => {
23 | expect(err).not.to.be.ok()
24 | expect(res.body).to.eql('ok')
25 | done()
26 | })
27 | })
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/test/missingKeyHandler.hapi.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import Hapi from '@hapi/hapi'
5 |
6 | i18next.init({
7 | fallbackLng: 'en',
8 | preload: ['en', 'de'],
9 | saveMissing: true
10 | })
11 |
12 | describe('missingKeyHandler hapi', () => {
13 | describe('handling a request', () => {
14 | const app = Hapi.server()
15 |
16 | before(async () => {
17 | await app.register({
18 | plugin: i18nextMiddleware.hapiPlugin,
19 | options: {
20 | i18next
21 | }
22 | })
23 | await app.initialize()
24 | })
25 |
26 | it('should work', async () => {
27 | app.route({
28 | method: 'POST',
29 | path: '/{lng}/{ns}',
30 | handler: i18nextMiddleware.missingKeyHandler(i18next)
31 | })
32 |
33 | const res = await app.inject({
34 | method: 'POST',
35 | url: '/en/translation',
36 | payload: { miss: 'key' }
37 | })
38 |
39 | expect(res.result).to.eql('ok')
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/test/missingKeyHandler.koa.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js'
2 | import i18nextMiddleware from '../index.js'
3 | import i18next from 'i18next'
4 | import Koa from 'koa'
5 | import Router from '@koa/router'
6 | import request from 'supertest'
7 |
8 | const router = Router()
9 | i18next.init({
10 | fallbackLng: 'en',
11 | preload: ['en', 'de'],
12 | saveMissing: true
13 | })
14 |
15 | describe('missingKeyHandler koa', () => {
16 | describe('handling a request', () => {
17 | const app = new Koa()
18 | let server
19 |
20 | before((done) => {
21 | server = app.listen(7002, done)
22 | })
23 | after((done) => server.close(done))
24 |
25 | it('should work', (done) => {
26 | router.post('/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next))
27 |
28 | app.use(router.routes())
29 |
30 | request(server)
31 | .post('/en/translation')
32 | .send({ miss: 'key' })
33 | .expect(200, (err, res) => {
34 | expect(err).not.to.be.ok()
35 | expect(res.text).to.eql('ok')
36 | done()
37 | })
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/test/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getResourcesHandler,
3 | missingKeyHandler,
4 | addRoute,
5 | handle,
6 | plugin,
7 | LanguageDetector,
8 | } from "../../index";
9 | import { expectType } from "tsd";
10 | import {
11 | Handler,
12 | Application,
13 | Request,
14 | Response,
15 | } from "express-serve-static-core";
16 | import i18next from "i18next";
17 |
18 | const noop = () => {};
19 |
20 | expectType(handle(i18next));
21 |
22 | expectType(plugin({}, { i18next: i18next }, noop));
23 |
24 | expectType(getResourcesHandler(i18next));
25 |
26 | expectType(missingKeyHandler(i18next));
27 |
28 | expectType(
29 | addRoute(i18next, "/path", ["en"], {}, "get", noop)
30 | );
31 |
32 | const languageDetector = new LanguageDetector();
33 |
34 | expectType(languageDetector.init({}));
35 |
36 | expectType(
37 | languageDetector.addDetector({
38 | name: "testDetector",
39 | lookup: () => "en",
40 | cacheUserLanguage: noop,
41 | })
42 | );
43 |
44 | expectType(languageDetector.detect({}, {}, ["en"]));
45 |
46 | expectType(
47 | languageDetector.cacheUserLanguage({}, {}, "en", true)
48 | );
49 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": [
5 | "es2015"
6 | ],
7 | "module": "commonjs",
8 | "noEmit": true,
9 | "strict": true,
10 | "esModuleInterop": true
11 | },
12 | "include": [
13 | "test/types/*.test-d.ts",
14 | "*.d.ts",
15 | "*.d.mts"
16 | ]
17 | }
--------------------------------------------------------------------------------