@dwp/govuk-casa
Version:
A framework for building GOVUK Collect-And-Submit-Applications
123 lines (104 loc) • 3.71 kB
JavaScript
import { createInstance } from "i18next";
import { LanguageDetector, handle } from "i18next-http-middleware";
import { resolve, basename } from "node:path";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import deepmerge from "deepmerge";
import { load as jsYamlLoad } from "js-yaml";
import logger from "../lib/logger.js";
/**
* @typedef {import("express").RequestHandler} RequestHandler
* @access private
*/
const log = logger("middleware:i18n");
const loadJson = (file) => {
// Strip out newlines (this is a legacy feature which we're keeping for
// backwards compatibility).
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
const json = readFileSync(file, "utf8");
return JSON.parse(json.replace(/[\r\n]/g, ""));
};
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
const loadYaml = (file) => jsYamlLoad(readFileSync(file, "utf8"));
const extract = (file) => {
const ext = /.yaml$/i.test(file) ? ".yaml" : ".json";
const data = ext === ".yaml" ? loadYaml(file) : loadJson(file);
return {
ns: basename(file, ext),
data,
};
};
const loadResources = (languages, directories) => {
const store = Object.create(null);
for (const language of languages) {
// ESLint disabled as `store`, `language` and `ns` are all dev-controlled,
// and this function is only called once, at boot-time.
/* eslint-disable security/detect-object-injection */
store[language] = Object.create(null);
for (const basedir of directories) {
const dir = resolve(basedir, language);
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
if (!existsSync(dir)) {
continue;
}
log.info("Loading %s language from %s ...", language, dir);
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
for (const file of readdirSync(dir)) {
const { ns, data } = extract(resolve(dir, file));
if (store[language][ns] === undefined) {
store[language][ns] = Object.create(null);
}
store[language][ns] = deepmerge(store[language][ns], data);
}
}
/* eslint-enable security/detect-object-injection */
}
return store;
};
/**
* Internationalisation middleware.
*
* @param {object} opts Options
* @param {string[]} [opts.languages] Language codes
* @param {string[]} [opts.fallbackLng] Fallback language
* @param {string[]} [opts.directories] Source translations directories
* @returns {RequestHandler[]} Middleware functions
*/
export default function i18nMiddleware({
languages = ["en", "cy"],
fallbackLng = false,
directories = [],
}) {
// Load _all_ translations, from all directories into memory.
const resources = loadResources(languages, directories);
// Configure i18next
const i18nInstance = createInstance();
i18nInstance.use(LanguageDetector).init({
initAsync: false, // because we need synchronous loading
supportedLngs: languages,
fallbackLng,
defaultNS: "common",
// debug: true,
// All translation resources
resources,
// LanguageDetector options
detection: {
lookupQuerystring: "lang",
lookupSession: "language",
order: ["querystring", "session"],
},
});
// 2 middleware: one to read/set the session language, and one to enhance the
// req/res objects with i18n features
return [
(req, res, next) => {
if (!req.session.language) {
req.session.language = languages[0];
}
if (req?.query.lang && languages.includes(req.query.lang)) {
req.session.language = String(req.query.lang);
}
next();
},
handle(i18nInstance),
];
}