From 10f8859d93239f9ad8eb31c64f0a82c30032dc00 Mon Sep 17 00:00:00 2001 From: Adriano Raiano Date: Wed, 27 Sep 2023 09:02:19 +0200 Subject: [PATCH] support koa #61 --- CHANGELOG.md | 3 + README.md | 30 +++++++++ example/koa/README.md | 8 +++ example/koa/index.js | 84 +++++++++++++++++++++++++ example/koa/locales/de/translation.json | 5 ++ example/koa/locales/en/translation.json | 5 ++ example/koa/locales/fr/translation.json | 5 ++ example/koa/package.json | 20 ++++++ index.d.ts | 2 + lib/httpFunctions.js | 5 ++ lib/index.js | 15 +++++ package.json | 27 ++++---- test/addRoute.koa.js | 53 ++++++++++++++++ test/getResourcesHandler.koa.js | 53 ++++++++++++++++ test/middleware.koa.js | 53 ++++++++++++++++ test/missingKeyHandler.koa.js | 40 ++++++++++++ 16 files changed, 395 insertions(+), 13 deletions(-) create mode 100644 example/koa/README.md create mode 100755 example/koa/index.js create mode 100644 example/koa/locales/de/translation.json create mode 100755 example/koa/locales/en/translation.json create mode 100644 example/koa/locales/fr/translation.json create mode 100755 example/koa/package.json create mode 100644 test/addRoute.koa.js create mode 100644 test/getResourcesHandler.koa.js create mode 100644 test/middleware.koa.js create mode 100644 test/missingKeyHandler.koa.js diff --git a/CHANGELOG.md b/CHANGELOG.md index db3c7d4..a0a017a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [v3.4.0](https://github.com/i18next/i18next-http-middleware/compare/v3.3.2...v3.4.0) +- support koa [#61](https://github.com/i18next/i18next-http-middleware/issues/61) + ## [v3.3.2](https://github.com/i18next/i18next-http-middleware/compare/v3.3.1...v3.3.2) - deno: oak fix [#59](https://github.com/i18next/i18next-http-middleware/issues/59) diff --git a/README.md b/README.md index 6a28cf4..ba619cf 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,36 @@ server.route({ ``` +### Koa usage + +```js +var i18next = require('i18next') +var middleware = require('i18next-http-middleware') +const Koa = require('koa') +const router = require('@koa/router')() + +i18next.use(middleware.LanguageDetector).init({ + preload: ['en', 'de', 'it'], + ...otherOptions +}) + +var app = new Koa() +app.use(i18nextMiddleware.koaPlugin(i18next, { + ignoreRoutes: ['/foo'] // or function(req, res, options, i18next) { /* return true to ignore */ } +})) + +// in your request handler +router.get('/myRoute', ctx => { + ctx.body = JSON.stringify({ + 'ctx.language': ctx.language, + 'ctx.i18n.language': ctx.i18n.language, + 'ctx.i18n.languages': ctx.i18n.languages, + 'ctx.i18n.languages[0]': ctx.i18n.languages[0], + 'ctx.t("home.title")': ctx.t('home.title') + }, null, 2) +}) +``` + ### Deno usage #### abc diff --git a/example/koa/README.md b/example/koa/README.md new file mode 100644 index 0000000..20b49c7 --- /dev/null +++ b/example/koa/README.md @@ -0,0 +1,8 @@ +# run the sample + +``` +$ npm i +$ npm start +``` + +open: http://localhost:8080 diff --git a/example/koa/index.js b/example/koa/index.js new file mode 100755 index 0000000..9c9af2b --- /dev/null +++ b/example/koa/index.js @@ -0,0 +1,84 @@ +const Koa = require('koa') +const router = require('@koa/router')() +const koaBody = require('koa-body').default +const serve = require('koa-static') +const mount = require('koa-mount') +const i18next = require('i18next') +// const i18nextMiddleware = require('i18next-http-middleware') +const i18nextMiddleware = require('../../../i18next-http-middleware') +const Backend = require('i18next-fs-backend') +// const Backend = require('../../../i18next-fs-backend') + +const app = new Koa() +app.use(koaBody({ + jsonLimit: '1kb' +})) +const port = process.env.PORT || 8080 + +i18next + .use(Backend) + // .use(languageDetector) + .use(i18nextMiddleware.LanguageDetector) + .init({ + // debug: true, + // detection: { + // order: ['customDetector'] + // }, + backend: { + // eslint-disable-next-line no-path-concat + loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json', + // eslint-disable-next-line no-path-concat + addPath: __dirname + '/locales/{{lng}}/{{ns}}.missing.json' + }, + fallbackLng: 'en', + // nonExplicitSupportedLngs: true, + // supportedLngs: ['en', 'de'], + load: 'languageOnly', + saveMissing: true + }) + +app.use(i18nextMiddleware.koaPlugin(i18next)) + +router.get('/', ctx => { + ctx.body = JSON.stringify({ + 'ctx.language': ctx.language, + 'ctx.i18n.language': ctx.i18n.language, + 'ctx.i18n.languages': ctx.i18n.languages, + 'ctx.i18n.languages[0]': ctx.i18n.languages[0], + 'ctx.t("home.title")': ctx.t('home.title') + }, null, 2) +}) + + +router.get('/missingtest', ctx => { + ctx.t('nonExisting', 'some default value') + ctx.body = 'check the locales files...' +}) + +// loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}}.json +app.use(mount('/locales', serve('./locales'))) + +// or instead of static +// router.get('/locales/:lng/:ns', i18nextMiddleware.getResourcesHandler(i18next)) +// loadPath for client: http://localhost:8080/locales/{{lng}}/{{ns}} + +// missing keys make sure the body is parsed (i.e. with [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions)) +router.post('/locales/add/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next)) +// The client can be configured with i18next-http-backend, for example like this: +// import HttpBackend from 'i18next-http-backend' +// i18next.use(HttpBackend).init({ +// lng: 'en', +// fallbackLng: 'en', +// backend: { +// loadPath: 'http://localhost:8080/locales/{{lng}}/{{ns}}.json', +// addPath: 'http://localhost:8080/locales/add/{{lng}}/{{ns}}' +// } +// }) + +app.use(router.routes()) + +app.listen(port, () => { + console.log(`Server is listening on port ${port}`) +}) + +// curl localhost:8080 -H 'Accept-Language: de-de' diff --git a/example/koa/locales/de/translation.json b/example/koa/locales/de/translation.json new file mode 100644 index 0000000..b34d14d --- /dev/null +++ b/example/koa/locales/de/translation.json @@ -0,0 +1,5 @@ +{ + "home": { + "title": "Hallo Welt!" + } +} diff --git a/example/koa/locales/en/translation.json b/example/koa/locales/en/translation.json new file mode 100755 index 0000000..f3952b4 --- /dev/null +++ b/example/koa/locales/en/translation.json @@ -0,0 +1,5 @@ +{ + "home": { + "title": "Hello World!" + } +} diff --git a/example/koa/locales/fr/translation.json b/example/koa/locales/fr/translation.json new file mode 100644 index 0000000..7b96e4d --- /dev/null +++ b/example/koa/locales/fr/translation.json @@ -0,0 +1,5 @@ +{ + "home": { + "title": "Bonjour le monde!" + } +} diff --git a/example/koa/package.json b/example/koa/package.json new file mode 100755 index 0000000..7f51306 --- /dev/null +++ b/example/koa/package.json @@ -0,0 +1,20 @@ +{ + "name": "i18next-express-basic", + "version": "1.0.0", + "description": "Node Express server with i18next.", + "main": "index.js", + "type": "commonjs", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@koa/router": "12.0.0", + "i18next": "23.5.1", + "i18next-fs-backend": "2.2.0", + "i18next-http-middleware": "3.3.2", + "koa": "2.14.2", + "koa-body": "6.0.1", + "koa-mount": "4.0.0", + "koa-static": "5.0.0" + } +} diff --git a/index.d.ts b/index.d.ts index 1ebb841..b7e946c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -75,6 +75,8 @@ type IgnoreRoutesFunction = ( export function handle(i18next: I18next, options?: HandleOptions): Handler; +export function koaPlugin(i18next: I18next, options?: HandleOptions): (context: unknown, next: Function) => any; + export function plugin( instance: any, options: HandleOptions & { i18next?: I18next }, diff --git a/lib/httpFunctions.js b/lib/httpFunctions.js index 05fee71..00647c5 100644 --- a/lib/httpFunctions.js +++ b/lib/httpFunctions.js @@ -74,6 +74,7 @@ export const getBody = (req) => { if (req.json) return req.json if (req.body) return req.body if (req.payload) return req.payload + if (req.request && req.request.body) return req.request.body console.log('no possibility found to get body') return {} } @@ -94,6 +95,9 @@ export const setHeader = (res, name, value) => { if (res.headers && typeof res.headers.set === 'function') { return res.headers.set(name, value) } + if (typeof res.set === 'function') { + return res.set(name, value) + } console.log('no possibility found to set header') } export const setContentType = (res, type) => { @@ -109,6 +113,7 @@ export const setStatus = (res, code) => { } export const send = (res, body) => { if (typeof res.send === 'function') return res.send(body) + if (res.request && res.response && res.app) res.body = body return body } export const getSession = (req) => { diff --git a/lib/index.js b/lib/index.js index e3e941b..4a6ee5c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,6 +15,12 @@ const checkForCombinedReqRes = (req, res, next) => { res = req if (!next) next = () => {} } + } else if (!next && typeof res === 'function' && req.req && req.res) { + return { + req, + res: req, + next: res + } } return { req, res, next } } @@ -119,6 +125,14 @@ export function plugin (instance, options, next) { return next() } +export function koaPlugin (i18next, options) { + const middleware = handle(i18next, options) + return async function koaMiddleware (ctx, next) { + await new Promise((resolve) => middleware(ctx, ctx, resolve)) + await next() + } +} + export const hapiPlugin = { name: 'i18next-http-middleware', version: '1', @@ -291,6 +305,7 @@ export function addRoute (i18next, route, lngs, app, verb, fc) { export default { plugin, hapiPlugin, + koaPlugin, handle, getResourcesHandler, missingKeyHandler, diff --git a/package.json b/package.json index 823e8d5..5aaae4d 100644 --- a/package.json +++ b/package.json @@ -27,28 +27,29 @@ } }, "module": "./esm/index.js", - "dependencies": {}, "devDependencies": { - "@babel/cli": "7.21.5", - "@babel/core": "7.21.8", - "@babel/preset-env": "7.21.5", - "@hapi/hapi": "^20.2.2", - "@types/express-serve-static-core": "^4.17.35", + "@babel/cli": "7.23.0", + "@babel/core": "7.23.0", + "@babel/preset-env": "7.22.20", + "@hapi/hapi": "^21.3.2", + "@types/express-serve-static-core": "^4.17.37", + "@koa/router": "12.0.0", + "koa": "2.14.2", "babel-plugin-add-module-exports": "1.0.4", - "eslint": "8.41.0", - "eslint-config-standard": "17.0.0", - "eslint-plugin-import": "2.27.5", - "eslint-plugin-n": "15.7.0", + "eslint": "8.50.0", + "eslint-config-standard": "17.1.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-n": "16.1.0", "eslint-plugin-promise": "6.1.1", "eslint-plugin-require-path-exists": "1.1.9", "eslint-plugin-standard": "5.0.0", "expect.js": "0.3.1", "express": "4.18.2", - "fastify": "4.17.0", - "i18next": "22.5.0", + "fastify": "4.23.2", + "i18next": "23.5.1", "mocha": "10.2.0", "supertest": "6.3.3", - "tsd": "0.28.1", + "tsd": "0.29.0", "uglify-js": "3.17.4" }, "description": "i18next-http-middleware is a middleware to be used with Node.js web frameworks like express or Fastify and also for Deno.", diff --git a/test/addRoute.koa.js b/test/addRoute.koa.js new file mode 100644 index 0000000..a380d59 --- /dev/null +++ b/test/addRoute.koa.js @@ -0,0 +1,53 @@ +import expect from 'expect.js' +import i18nextMiddleware from '../index.js' +import i18next from 'i18next' +import Koa from 'koa' +import Router from '@koa/router' +import request from 'supertest' + +const router = Router() +i18next.init({ + fallbackLng: 'en', + preload: ['en', 'de'], + saveMissing: true +}) + +describe('addRoute koa', () => { + describe('and handling a request', () => { + const app = new Koa() + app.use(i18nextMiddleware.koaPlugin(i18next)) + let server + + before((done) => { + server = app.listen(7002, done) + }) + after((done) => server.close(done)) + + it('should return the appropriate resource', (done) => { + app.use(i18nextMiddleware.handle(i18next)) + const handle = (ctx) => { + expect(ctx).to.have.property('lng', 'en') + expect(ctx).to.have.property('locale', 'en') + expect(ctx).to.have.property('language', 'en') + expect(ctx).to.have.property('languages') + expect(ctx.languages).to.eql(['en']) + expect(ctx).to.have.property('i18n') + expect(ctx).to.have.property('t') + expect(ctx.t('key')).to.eql('key') + ctx.body = ctx.t('key') + } + i18nextMiddleware.addRoute(i18next, '/myroute/:lng/:ns', ['en'], router, 'get', handle) + + app.use(router.routes()) + + request(server) + .get('/myroute/en/test') + .expect('Content-Language', 'en') + .expect(200, (err, res) => { + expect(err).not.to.be.ok() + expect(res.text).to.eql('key') + done() + }) + }) + }) +}) diff --git a/test/getResourcesHandler.koa.js b/test/getResourcesHandler.koa.js new file mode 100644 index 0000000..6d2379e --- /dev/null +++ b/test/getResourcesHandler.koa.js @@ -0,0 +1,53 @@ +import expect from 'expect.js' +import i18nextMiddleware from '../index.js' +import i18next from 'i18next' +import Koa from 'koa' +import Router from '@koa/router' +import request from 'supertest' + +const router = Router() +i18next.init({ + fallbackLng: 'en', + preload: ['en', 'de'], + saveMissing: true, + resources: { + en: { + translation: { hi: 'there' } + } + } +}) + +describe('getResourcesHandler koa', () => { + describe('handling a request', () => { + const app = new Koa() + let server + + before((done) => { + server = app.listen(7002, done) + }) + after((done) => server.close(done)) + + it('should return the appropriate resource', (done) => { + router.get('/', i18nextMiddleware.getResourcesHandler(i18next)) + + app.use(router.routes()) + + request(server) + .get('/') + .query({ + lng: 'en', + ns: 'translation' + }) + .expect('content-type', /json/) + .expect('cache-control', 'no-cache') + .expect('pragma', 'no-cache') + .expect(200, (err, res) => { + expect(err).not.to.be.ok() + expect(res.body).to.have.property('en') + expect(res.body.en).to.have.property('translation') + expect(res.body.en.translation).to.have.property('hi', 'there') + done() + }) + }) + }) +}) diff --git a/test/middleware.koa.js b/test/middleware.koa.js new file mode 100644 index 0000000..8dd6691 --- /dev/null +++ b/test/middleware.koa.js @@ -0,0 +1,53 @@ +import expect from 'expect.js' +import i18nextMiddleware from '../index.js' +import i18next from 'i18next' +import Koa from 'koa' +import Router from '@koa/router' +import request from 'supertest' + +const router = Router() +i18next.init({ + fallbackLng: 'en', + preload: ['en', 'de'], + saveMissing: true +}) + +describe('middleware koa', () => { + describe('handling an empty request', () => { + const app = new Koa() + app.use(i18nextMiddleware.koaPlugin(i18next)) + let server + + before((done) => { + server = app.listen(7002, done) + }) + after((done) => server.close(done)) + + it('should extend request and response', (done) => { + router.get('/', (ctx) => { + expect(ctx).to.have.property('lng', 'en') + expect(ctx).to.have.property('locale', 'en') + expect(ctx).to.have.property('language', 'en') + expect(ctx).to.have.property('languages') + expect(ctx.languages).to.eql(['en']) + expect(ctx).to.have.property('i18n') + expect(ctx).to.have.property('t') + + expect(ctx.t('key')).to.eql('key') + ctx.body = ctx.t('key') + }) + + app.use(router.routes()) + + request(server) + .get('/') + .expect('Content-Language', 'en') + .expect(200, (err, res) => { + expect(err).not.to.be.ok() + expect(res.text).to.eql('key') + + done() + }) + }) + }) +}) diff --git a/test/missingKeyHandler.koa.js b/test/missingKeyHandler.koa.js new file mode 100644 index 0000000..2734047 --- /dev/null +++ b/test/missingKeyHandler.koa.js @@ -0,0 +1,40 @@ +import expect from 'expect.js' +import i18nextMiddleware from '../index.js' +import i18next from 'i18next' +import Koa from 'koa' +import Router from '@koa/router' +import request from 'supertest' + +const router = Router() +i18next.init({ + fallbackLng: 'en', + preload: ['en', 'de'], + saveMissing: true +}) + +describe('missingKeyHandler koa', () => { + describe('handling a request', () => { + const app = new Koa() + let server + + before((done) => { + server = app.listen(7002, done) + }) + after((done) => server.close(done)) + + it('should work', (done) => { + router.post('/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18next)) + + app.use(router.routes()) + + request(server) + .post('/en/translation') + .send({ miss: 'key' }) + .expect(200, (err, res) => { + expect(err).not.to.be.ok() + expect(res.text).to.eql('ok') + done() + }) + }) + }) +})