├── .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 | [![Actions](https://github.com/i18next/i18next-http-middleware/workflows/node/badge.svg)](https://github.com/i18next/i18next-http-middleware/actions?query=workflow%3Anode) 4 | [![Actions deno](https://github.com/i18next/i18next-http-middleware/workflows/deno/badge.svg)](https://github.com/i18next/i18next-http-middleware/actions?query=workflow%3Adeno) 5 | [![Travis](https://img.shields.io/travis/i18next/i18next-http-middleware/master.svg?style=flat-square)](https://travis-ci.org/i18next/i18next-http-middleware) 6 | [![npm version](https://img.shields.io/npm/v/i18next-http-middleware.svg?style=flat-square)](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 |
english  |  deutsch |  italiano
10 |
11 |
{ 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 | } --------------------------------------------------------------------------------