i18next-http-middleware
Version:
i18next-http-middleware is a middleware to be used with Node.js web frameworks like express or Fastify and also for Deno.
319 lines (273 loc) • 9.12 kB
JavaScript
import * as utils from './utils.js'
import LD from './LanguageDetector.js'
import { extendOptionsWithDefaults } from './httpFunctions.js'
export const LanguageDetector = LD
const checkForCombinedReqRes = (req, res, next) => {
if (!res) {
if (req.request && req.response) {
res = req.response
if (!req.request.ctx) req.request.ctx = req
req = req.request
if (!next) next = () => {}
} else if (req.respond) {
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 }
}
export function handle (i18next, options = {}) {
extendOptionsWithDefaults(options)
return function i18nextMiddleware (rq, rs, n) {
const { req, res, next } = checkForCombinedReqRes(rq, rs, n)
if (typeof options.ignoreRoutes === 'function') {
if (options.ignoreRoutes(req, res, options, i18next)) {
return next()
}
} else {
const ignores =
(options.ignoreRoutes instanceof Array && options.ignoreRoutes) || []
for (let i = 0; i < ignores.length; i++) {
if (options.getPath(req).indexOf(ignores[i]) > -1) return next()
}
}
const i18n = i18next.cloneInstance({ initAsync: false, initImmediate: false })
i18n.on('languageChanged', lng => {
// Keep language in sync
req.language = req.locale = req.lng = lng
if (options.attachLocals) res.locals = res.locals || {}
if (res.locals) {
res.locals.language = lng
res.locals.languageDir = i18next.dir(lng)
res.locals.resolvedLanguage = i18n.resolvedLanguage
}
if (lng && options.getHeader(res, 'Content-Language') !== lng) {
options.setHeader(res, 'Content-Language', utils.escape(lng))
}
req.languages = i18next.services.languageUtils.toResolveHierarchy(lng)
req.resolvedLanguage = i18n.resolvedLanguage
if (i18next.services.languageDetector) {
i18next.services.languageDetector.cacheUserLanguage(req, res, lng)
}
})
let lng = req.lng
if (!lng && i18next.services.languageDetector) {
lng = i18next.services.languageDetector.detect(req, res)
}
// set locale
req.language = req.locale = req.lng = lng
if (lng && options.getHeader(res, 'Content-Language') !== lng) {
options.setHeader(res, 'Content-Language', utils.escape(lng))
}
req.languages = i18next.services.languageUtils.toResolveHierarchy(lng)
req.resolvedLanguage = i18n.resolvedLanguage
// trigger sync to instance - might trigger async load!
i18n.changeLanguage(lng || i18next.options.fallbackLng[0])
if (req.i18nextLookupName === 'path' && options.removeLngFromUrl) {
options.setUrl(
req,
utils.removeLngFromUrl(
options.getUrl(req),
i18next.services.languageDetector.options.lookupFromPathIndex
)
)
}
const t = i18n.t.bind(i18n)
const exists = i18n.exists.bind(i18n)
// assert for req
req.i18n = i18n
req.t = t
// assert for res -> template
if (options.attachLocals) res.locals = res.locals || {}
if (res.locals) {
res.locals.t = t
res.locals.exists = exists
res.locals.i18n = i18n
res.locals.language = lng
res.locals.resolvedLanguage = i18n.resolvedLanguage
res.locals.languageDir = i18next.dir(lng)
}
if (i18next.services.languageDetector) {
i18next.services.languageDetector.cacheUserLanguage(req, res, lng)
}
// load resources
if (!req.lng) return next()
i18next.loadLanguages(req.lng, () => next())
}
}
export function plugin (instance, options, next) {
options.attachLocals = true
const middleware = handle(options.i18next, options)
instance.addHook('preHandler', (request, reply, next) =>
middleware(request, reply, 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',
register: (server, options) => {
options.attachLocals = true
const middleware = handle(options.i18next, {
...options
})
server.ext('onPreAuth', (request, h) => {
middleware(
request,
request.raw.res || h.response() || request.Response,
() => h.continue
)
return h.continue
})
}
}
plugin[Symbol.for('skip-override')] = true
export function getResourcesHandler (i18next, options = {}) {
extendOptionsWithDefaults(options)
const maxAge = options.maxAge || 60 * 60 * 24 * 30
return function (rq, rs) {
const { req, res } = checkForCombinedReqRes(rq, rs)
if (!i18next.services.backendConnector) {
options.setStatus(res, 404)
return options.send(
res,
'i18next-express-middleware:: no backend configured'
)
}
const resources = {}
options.setContentType(res, 'application/json')
if (
options.cache !== undefined
? options.cache
: typeof process !== 'undefined' &&
process.env &&
process.env.NODE_ENV === 'production'
) {
options.setHeader(res, 'Cache-Control', 'public, max-age=' + maxAge)
options.setHeader(
res,
'Expires',
new Date(new Date().getTime() + maxAge * 1000).toUTCString()
)
} else {
options.setHeader(res, 'Pragma', 'no-cache')
options.setHeader(res, 'Cache-Control', 'no-cache')
}
// first check query
let languages = options.getQuery(req)[options.lngParam || 'lng']
? options.getQuery(req)[options.lngParam || 'lng'].split(' ')
: []
let namespaces = options.getQuery(req)[options.nsParam || 'ns']
? options.getQuery(req)[options.nsParam || 'ns'].split(' ')
: []
// then check route params
if (languages.length === 0 && namespaces.length === 0) {
languages = options.getParams(req)[options.lngParam || 'lng']
? options.getParams(req)[options.lngParam || 'lng'].split(' ')
: []
namespaces = options.getParams(req)[options.nsParam || 'ns']
? options.getParams(req)[options.nsParam || 'ns'].split(' ')
: []
}
// extend ns
namespaces.forEach(ns => {
if (i18next.options.ns && i18next.options.ns.indexOf(ns) < 0) {
i18next.options.ns.push(ns)
}
})
i18next.services.backendConnector.load(languages, namespaces, function () {
languages.forEach(lng => {
namespaces.forEach(ns => {
utils.setPath(
resources,
[lng, ns],
i18next.getResourceBundle(lng, ns)
)
})
})
})
return options.send(res, resources)
}
}
export function missingKeyHandler (i18next, options = {}) {
extendOptionsWithDefaults(options)
return function (rq, rs) {
const { req, res } = checkForCombinedReqRes(rq, rs)
const lng = options.getParams(req)[options.lngParam || 'lng']
const ns = options.getParams(req)[options.nsParam || 'ns']
if (!i18next.services.backendConnector) {
options.setStatus(res, 404)
return options.send(
res,
'i18next-express-middleware:: no backend configured'
)
}
const body = options.getBody(req)
if (typeof body === 'function') {
const promise = body()
if (promise && typeof promise.then === 'function') {
return new Promise(resolve => {
promise.then(b => {
for (const m in b) {
i18next.services.backendConnector.saveMissing([lng], ns, m, b[m])
}
resolve(options.send(res, 'ok'))
})
})
}
}
for (const m in body) {
i18next.services.backendConnector.saveMissing([lng], ns, m, body[m])
}
return options.send(res, 'ok')
}
}
export function addRoute (i18next, route, lngs, app, verb, fc) {
if (typeof verb === 'function') {
fc = verb
verb = 'get'
}
// Combine `fc` and possible more callbacks to one array
const callbacks = [fc].concat(Array.prototype.slice.call(arguments, 6))
for (let i = 0, li = lngs.length; i < li; i++) {
const parts = String(route).split('/')
const locRoute = []
for (let y = 0, ly = parts.length; y < ly; y++) {
const part = parts[y]
// if the route includes the parameter :lng
// this is replaced with the value of the language
if (part === ':lng') {
locRoute.push(lngs[i])
} else if (part.indexOf(':') === 0 || part === '') {
locRoute.push(part)
} else {
locRoute.push(i18next.t(part, { lng: lngs[i] }))
}
}
const routes = [locRoute.join('/')]
app[verb || 'get'].apply(app, routes.concat(callbacks))
}
}
export default {
plugin,
hapiPlugin,
koaPlugin,
handle,
getResourcesHandler,
missingKeyHandler,
addRoute,
LanguageDetector
}