UNPKG

lup-language

Version:

Node express middleware for detecting requested language

813 lines (812 loc) 41.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LanguageRouter = exports.splitLocale = exports.getTranslationFileContentSync = exports.getTranslationFileContent = exports.getLanguageNames = exports.checkLanguage = exports.getLanguages = exports.getTranslation = exports.getTranslations = exports.reloadTranslationsSync = exports.reloadTranslations = exports.DEFAULTS = void 0; var lup_root_1 = require("lup-root"); var fs = require("fs"); var path = require("path"); /** Contains default settings that will be used if an option is not specified. */ exports.DEFAULTS = { LANGUAGE: 'en', LANGUAGES: ['en'], USE_NEXT_CONFIG_LANGUAGES: '', LANGUAGES_FROM_TRANSLATIONS_DIR: true, TRANSLATIONS_DIR: './translations/', LANGUAGE_DETECTION_METHODS: ['uri', 'cookie', 'http-accept'], REDIRECT_ROOT: false, REDIRECT_ROOT_RESPONSE_CODE: 302, COOKIE_NAME: 'L', COOKIE_EXPIRE: 5184000, COOKIE_PATH: '/', COOKIE_DOMAIN: null, COOKIE_UPDATE: true, REQUEST_ADD_LANGUAGE_ATTRIBUTE: 'lang', REQUEST_ADD_TRANSLATIONS_ATTRIBUTE: '', REQUEST_ADD_PATH_ATTRIBUTE: 'PATH', // 'path' is already used by express REQUEST_URL_REMOVE_LANGUAGE_PREFIX: true, }; // GLOBAL VARIABLES FOR CACHING var LANGUAGES = {}; // { translationsDir: [] } var DICTONARY = {}; // { translationsDir: {lang: {key: translation} } } /** * Reloads translations from files inside given directory. * @param translationsDir Relative path to directory containing JSON files with translations. * @returns Promise that resolves with a list of language codes that where found after translations have been reloaded from files. */ var reloadTranslations = function () { var args_1 = []; for (var _i = 0; _i < arguments.length; _i++) { args_1[_i] = arguments[_i]; } return __awaiter(void 0, __spreadArray([], args_1, true), void 0, function (translationsDir) { if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_a) { if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; // do not pre-initialize LANGUAGES and DICTONARY here so in multi-threaded environments all threads wait return [2 /*return*/, new Promise(function (resolve, reject) { var TRANSLATIONS_DIR = path.resolve(lup_root_1.ROOT, translationsDir).toString(); function scanFiles() { fs.readdir(TRANSLATIONS_DIR, null, function (err, files) { if (err) console.error(err); if (files.length === 0) { reject("No files found in '" + translationsDir + "' (" + TRANSLATIONS_DIR + ' | ' + lup_root_1.ROOT + ')'); } var dict = {}; var langs = new Set(); var globals = null; var remaining = files.length; var _loop_1 = function (i) { var file = files[i].toString(); // if not a json file skip it if (!file.endsWith('.json')) { if (--remaining === 0) { LANGUAGES[translationsDir] = Array.from(langs); DICTONARY[translationsDir] = dict; resolve(LANGUAGES[translationsDir]); return { value: void 0 }; } return "continue"; } // start reading json file var filePath = path.resolve(TRANSLATIONS_DIR, file).toString(); fs.readFile(filePath, {}, function (err2, data) { if (err2) console.error(err2); try { var json = JSON.parse(data.toString()); var lang = file.substring(0, file.length - '.json'.length); if (lang.startsWith('global')) { globals = json; for (var l in dict) for (var k in json) dict[l][k] = dict[l][k] || json[k]; } else { langs.add(lang); if (!dict[lang]) dict[lang] = {}; if (globals) for (var k in globals) dict[lang][k] = globals[k]; for (var k in json) dict[lang][k] = json[k]; } } catch (ex) { if (!err) console.error(ex); } if (--remaining === 0) { LANGUAGES[translationsDir] = Array.from(langs); LANGUAGES[translationsDir].sort(); DICTONARY[translationsDir] = dict; resolve(LANGUAGES[translationsDir]); } }); }; for (var i = 0; i < files.length; i++) { var state_1 = _loop_1(i); if (typeof state_1 === "object") return state_1.value; } }); } fs.access(TRANSLATIONS_DIR, function (err) { if (!err) scanFiles(); else fs.mkdir(TRANSLATIONS_DIR, { recursive: true }, function (err2) { if (err2) console.error(err2); scanFiles(); }); }); })]; }); }); }; exports.reloadTranslations = reloadTranslations; /** * Reloads translations from files inside given directory. * @param translationsDir Relative path to directory containing JSON files with translations. * @returns Promise that resolves with a list of language codes that where found after translations have been reloaded from files. */ var reloadTranslationsSync = function (translationsDir) { if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; // do not pre-initialize LANGUAGES and DICTONARY here so in multi-threaded environments all threads wait var TRANSLATIONS_DIR = path.resolve(lup_root_1.ROOT, translationsDir).toString(); // create translations dir if not exists fs.mkdirSync(TRANSLATIONS_DIR, { recursive: true }); var files = fs.readdirSync(TRANSLATIONS_DIR, null); if (files.length === 0) { throw new Error("No files found in '" + translationsDir + "' (" + TRANSLATIONS_DIR + ' | ' + lup_root_1.ROOT + ')'); } var dict = {}; var langs = new Set(); var globals = null; for (var i = 0; i < files.length; i++) { var file = files[i].toString(); // if not a json file skip it if (!file.endsWith('.json')) continue; // start reading json file var filePath = path.resolve(TRANSLATIONS_DIR, file).toString(); try { var data = fs.readFileSync(filePath, {}); var json = JSON.parse(data.toString()); var lang = file.substring(0, file.length - '.json'.length); if (lang.startsWith('global')) { globals = json; for (var l in dict) for (var k in json) dict[l][k] = dict[l][k] || json[k]; } else { langs.add(lang); if (!dict[lang]) dict[lang] = {}; if (globals) for (var k in globals) dict[lang][k] = globals[k]; for (var k in json) dict[lang][k] = json[k]; } } catch (ex) { console.error(ex); } } LANGUAGES[translationsDir] = Array.from(langs); LANGUAGES[translationsDir].sort(); DICTONARY[translationsDir] = dict; return LANGUAGES[translationsDir]; }; exports.reloadTranslationsSync = reloadTranslationsSync; var _getTranslations = function (lang, defaultLang, translationKeys, translationsDir) { if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } defaultLang = defaultLang || lang || exports.DEFAULTS.LANGUAGE; lang = lang || defaultLang; var idx = (lang === null || lang === void 0 ? void 0 : lang.lastIndexOf('-')) || -1; // country code stripping var altLang = idx >= 0 ? lang === null || lang === void 0 ? void 0 : lang.substring(0, idx) : defaultLang; var transKeys = !translationKeys ? new Set() : !(translationKeys instanceof Set) ? new Set(translationKeys) : translationKeys; var dictornary = DICTONARY[translationsDir] ? DICTONARY[translationsDir][lang] || DICTONARY[translationsDir][altLang] || DICTONARY[translationsDir][defaultLang] || {} : {}; var dict = transKeys.size !== 0 ? {} : dictornary; if (transKeys.size !== 0) transKeys.forEach(function (k) { return (dict[k] = dictornary[k] || k); }); return dict; }; /** * Returns a key/value array containing the translations in the given language. * @param lang Language code for which translations should be loaded. * @param defaultLang Default language code if given 'lang' is not supported. * @param translationKeys If not empty only translations with given keys will be included in output (if empty all translations will be included). * @param translationsDir Relative path to directory containing JSON files with translations (optional). * @returns Promise that resolves with the translations in the given language. */ var getTranslations = function (lang_1, defaultLang_1) { var args_1 = []; for (var _i = 2; _i < arguments.length; _i++) { args_1[_i - 2] = arguments[_i]; } return __awaiter(void 0, __spreadArray([lang_1, defaultLang_1], args_1, true), void 0, function (lang, defaultLang, translationKeys, translationsDir) { if (translationKeys === void 0) { translationKeys = []; } if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_a) { switch (_a.label) { case 0: if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; if (!!DICTONARY[translationsDir]) return [3 /*break*/, 2]; return [4 /*yield*/, (0, exports.reloadTranslations)(translationsDir)]; case 1: _a.sent(); _a.label = 2; case 2: return [2 /*return*/, _getTranslations(lang, defaultLang, translationKeys, translationsDir)]; } }); }); }; exports.getTranslations = getTranslations; /** * Returns a translation for a given key in the given language. * @param lang Language code of language the translation should be in. * @param defaultLang Default language code if given 'lang' is not supported. * @param translationKey Key that should be looked up in the translations. * @param translationDir Relative path to directory containing JSON files with translations (optional). * @returns Promise that resolves with the translation or with the given key if no translation found. */ var getTranslation = function (lang_1, defaultLang_1, translationKey_1) { var args_1 = []; for (var _i = 3; _i < arguments.length; _i++) { args_1[_i - 3] = arguments[_i]; } return __awaiter(void 0, __spreadArray([lang_1, defaultLang_1, translationKey_1], args_1, true), void 0, function (lang, defaultLang, translationKey, translationDir) { if (translationDir === void 0) { translationDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, (0, exports.getTranslations)(lang, defaultLang, [translationKey], translationDir)]; case 1: return [2 /*return*/, (_a.sent())[translationKey]]; } }); }); }; exports.getTranslation = getTranslation; /** * Returns loaded language codes found in translations directory. * @param translationsDir Relative path to directory containing JSON files with translations. * @returns Promise that resolves to a list of language codes. */ var getLanguages = function () { var args_1 = []; for (var _i = 0; _i < arguments.length; _i++) { args_1[_i] = arguments[_i]; } return __awaiter(void 0, __spreadArray([], args_1, true), void 0, function (translationsDir) { if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_a) { switch (_a.label) { case 0: if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; if (!!DICTONARY[translationsDir]) return [3 /*break*/, 2]; return [4 /*yield*/, (0, exports.reloadTranslations)(translationsDir)]; case 1: _a.sent(); _a.label = 2; case 2: return [2 /*return*/, __spreadArray([], LANGUAGES[translationsDir], true)]; } }); }); }; exports.getLanguages = getLanguages; /** * Checks if a given language is supported and returns the supported language code. * If the given language is not supported the default language will be returned. * * @param lang Language code that should be checked. * @param defaultLang Default language code if given 'lang' is not supported. * @param translationsDir Relative path to directory containing JSON files with translations to lookup supported translations (optional). * @return Promise that resolves to the supported language code or undefined if no language is supported and also no default language is given. */ var checkLanguage = function (lang_1, defaultLang_1) { var args_1 = []; for (var _i = 2; _i < arguments.length; _i++) { args_1[_i - 2] = arguments[_i]; } return __awaiter(void 0, __spreadArray([lang_1, defaultLang_1], args_1, true), void 0, function (lang, defaultLang, translationsDir) { var langs, i; if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_a) { switch (_a.label) { case 0: if (!lang) return [2 /*return*/, defaultLang]; lang = lang.trim().toLowerCase(); return [4 /*yield*/, (0, exports.getLanguages)(translationsDir)]; case 1: langs = _a.sent(); for (i = 0; i < langs.length; i++) { if (langs[i].toLowerCase() === lang) { return [2 /*return*/, langs[i]]; } } return [2 /*return*/, defaultLang]; } }); }); }; exports.checkLanguage = checkLanguage; /** * Returns a map of all found languages and their native name. * Looksup following keys in the translations 'LANGUAGE_NAME_<lang>'. * @param translationsDir Relative path to directory containing JSON files with translations * @returns Promise that resolves to a map of language codes and their native names. */ var getLanguageNames = function () { var args_1 = []; for (var _i = 0; _i < arguments.length; _i++) { args_1[_i] = arguments[_i]; } return __awaiter(void 0, __spreadArray([], args_1, true), void 0, function (translationsDir) { var dict, names, keyLocale, _a, _b, lang, keyGlobal; if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_c) { switch (_c.label) { case 0: if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; if (!!DICTONARY[translationsDir]) return [3 /*break*/, 2]; return [4 /*yield*/, (0, exports.reloadTranslations)(translationsDir)]; case 1: _c.sent(); _c.label = 2; case 2: dict = DICTONARY[translationsDir]; names = {}; keyLocale = 'LANGUAGE_NAME'; for (_a = 0, _b = LANGUAGES[translationsDir]; _a < _b.length; _a++) { lang = _b[_a]; keyGlobal = 'LANGUAGE_NAME_' + lang.toUpperCase(); names[lang] = dict[lang][keyLocale] || dict[lang][keyGlobal] || lang.toUpperCase(); } return [2 /*return*/, names]; } }); }); }; exports.getLanguageNames = getLanguageNames; /** * Loads the contents of a file loaded inside the translations directory. * @param fileName Name of the file the contents should be loaded (relative path inside the translations directory). * @param translationsDir Relative path to directory containing JSON files with translations. * @returns Promise that resolves to the contents of the file. */ var getTranslationFileContent = function (fileName_1) { var args_1 = []; for (var _i = 1; _i < arguments.length; _i++) { args_1[_i - 1] = arguments[_i]; } return __awaiter(void 0, __spreadArray([fileName_1], args_1, true), void 0, function (fileName, translationsDir) { if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } return __generator(this, function (_a) { if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; return [2 /*return*/, new Promise(function (resolve, reject) { var filePath = path.resolve(lup_root_1.ROOT, translationsDir, fileName).toString(); fs.readFile(filePath, {}, function (err, data) { if (data) resolve(data.toString()); else reject(err); }); })]; }); }); }; exports.getTranslationFileContent = getTranslationFileContent; /** * Loads the contents of a file loaded inside the translations directory. * @param fileName Name of the file the contents should be loaded (relative path inside the translations directory). * @param translationsDir Relative path to directory containing JSON files with translations. * @returns Contents of the file. */ var getTranslationFileContentSync = function (fileName, translationsDir) { if (translationsDir === void 0) { translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; } if (!translationsDir) translationsDir = exports.DEFAULTS.TRANSLATIONS_DIR; var filePath = path.resolve(lup_root_1.ROOT, translationsDir, fileName).toString(); return fs.readFileSync(filePath).toString(); }; exports.getTranslationFileContentSync = getTranslationFileContentSync; /** * Splits a given locale string into language and country code. * @param locale Locale string that should be split. * @returns Language iso code and optionally country iso code if provided. */ var splitLocale = function (locale) { var idx = locale.lastIndexOf('-'); return { languageIso: idx >= 0 ? locale.substring(0, idx) : locale, countryIso: idx >= 0 ? locale.substring(idx + 1) : undefined, }; }; exports.splitLocale = splitLocale; /** * Returns a HTTP request/response middleware for detecting request language and loading translation variables. * @param options Object containing options for behavior of the middleware. * @returns function(req, res, next) that is designed for being set as middleware to pre-handle incoming requests. */ var LanguageRouter = function (options) { var _a; var defaultLang = (options === null || options === void 0 ? void 0 : options.defaultLanguage) || exports.DEFAULTS.LANGUAGE; var languagesFromTranslations = (options === null || options === void 0 ? void 0 : options.languagesFromTranslationsDir) !== undefined ? options.languagesFromTranslationsDir : exports.DEFAULTS.LANGUAGES_FROM_TRANSLATIONS_DIR; var translationsDir = (options === null || options === void 0 ? void 0 : options.translationsDir) !== undefined ? options.translationsDir : exports.DEFAULTS.TRANSLATIONS_DIR; var languageDetectionMethods = (options === null || options === void 0 ? void 0 : options.languageDetectionMethods) || exports.DEFAULTS.LANGUAGE_DETECTION_METHODS; var redirectRoot = (options === null || options === void 0 ? void 0 : options.redirectRoot) !== undefined ? options.redirectRoot : exports.DEFAULTS.REDIRECT_ROOT; var redirectRootResponseCode = (_a = options === null || options === void 0 ? void 0 : options.redirectRootResponseCode) !== null && _a !== void 0 ? _a : exports.DEFAULTS.REDIRECT_ROOT_RESPONSE_CODE; var cookieName = (options === null || options === void 0 ? void 0 : options.cookieName) !== undefined ? options.cookieName : exports.DEFAULTS.COOKIE_NAME; var cookieExpire = (options === null || options === void 0 ? void 0 : options.cookieExpire) || exports.DEFAULTS.COOKIE_EXPIRE; var cookiePath = (options === null || options === void 0 ? void 0 : options.cookiePath) !== undefined ? options.cookiePath : exports.DEFAULTS.COOKIE_PATH; var cookieDomain = (options === null || options === void 0 ? void 0 : options.cookieDomain) !== undefined ? options.cookieDomain : exports.DEFAULTS.COOKIE_DOMAIN; var cookieUpdate = (options === null || options === void 0 ? void 0 : options.cookieUpdate) !== undefined ? options.cookieUpdate : exports.DEFAULTS.COOKIE_UPDATE; var languageAttr = (options === null || options === void 0 ? void 0 : options.requestAddLanguageAttribute) || exports.DEFAULTS.REQUEST_ADD_LANGUAGE_ATTRIBUTE; var translationsAttr = (options === null || options === void 0 ? void 0 : options.requestAddTranslationsAttribute) !== undefined ? options.requestAddTranslationsAttribute : exports.DEFAULTS.REQUEST_ADD_TRANSLATIONS_ATTRIBUTE; var pathAttr = (options === null || options === void 0 ? void 0 : options.requestAddPathAttribute) !== undefined ? options.requestAddPathAttribute : exports.DEFAULTS.REQUEST_ADD_PATH_ATTRIBUTE; var updateUrlParam = (options === null || options === void 0 ? void 0 : options.requestUrlRemoveLanguagePrefix) !== undefined ? options.requestUrlRemoveLanguagePrefix : exports.DEFAULTS.REQUEST_URL_REMOVE_LANGUAGE_PREFIX; // useNextConfigLanguages var languagesArr = []; // later converted to Set 'languages' if (options === null || options === void 0 ? void 0 : options.useNextConfigLanguages) languagesArr = languagesArr.concat(require((options === null || options === void 0 ? void 0 : options.useNextConfigLanguages) !== undefined ? options.useNextConfigLanguages : lup_root_1.ROOT + '/next.config.js').i18n .locales); if (options === null || options === void 0 ? void 0 : options.languages) languagesArr = languagesArr.concat(options.languages); else if (!(options === null || options === void 0 ? void 0 : options.useNextConfigLanguages)) languagesArr = languagesArr.concat(exports.DEFAULTS.LANGUAGES); var loadedLangs = false; var languagesSorted = []; var languagesSet = new Set(languagesArr); /** * Optionally preload LanguageRouter so first request can be handled faster * @returns Promise<void> that resolves when preloading is done. */ var preload = function () { return __awaiter(void 0, void 0, void 0, function () { var ls, _i, ls_1, l; return __generator(this, function (_a) { switch (_a.label) { case 0: if (loadedLangs) return [2 /*return*/]; loadedLangs = true; if (!translationsDir) return [3 /*break*/, 2]; return [4 /*yield*/, (0, exports.reloadTranslations)(translationsDir)]; case 1: ls = _a.sent(); if (languagesFromTranslations) for (_i = 0, ls_1 = ls; _i < ls_1.length; _i++) { l = ls_1[_i]; languagesSet.add(l); } _a.label = 2; case 2: languagesSet.forEach(function (l) { return languagesSorted.push(l); }); languagesSorted.sort(); return [2 /*return*/]; } }); }); }; var preloadSync = function () { if (loadedLangs) return; loadedLangs = true; if (translationsDir) { var ls = (0, exports.reloadTranslationsSync)(translationsDir); if (languagesFromTranslations) for (var _i = 0, ls_2 = ls; _i < ls_2.length; _i++) { var l = ls_2[_i]; languagesSet.add(l); } } languagesSet.forEach(function (l) { return languagesSorted.push(l); }); languagesSorted.sort(); }; function getHeaderValue(headers, key) { var _a, _b; if (!headers || !key) return null; var lowerKey = key.toLowerCase(); if (typeof headers.get === 'function') return (_a = headers.get(key)) !== null && _a !== void 0 ? _a : headers.get(lowerKey); if (typeof headers === 'object') return (_b = headers[key]) !== null && _b !== void 0 ? _b : headers[lowerKey]; var headersArr = Array.isArray(headers) ? headers : (typeof headers === 'string' ? headers.split('\n') : []); headers = {}; for (var i = 0; i < headersArr.length; i++) { var idx = headersArr[i].indexOf(':'); if (idx > 0) { headers[headersArr[i].substring(0, idx).trim()] = headersArr[i].substring(idx + 1).trim(); } else { headers[headersArr[i].trim()] = (i + 1 < headersArr.length) ? headersArr[i + 1].trim() : ''; i++; } } return getHeaderValue(headers, key); } function getCookieValue(cookies) { if (!cookies) return null; if (typeof cookies === 'object') return cookies[cookieName]; var cookiesArr = Array.isArray(cookies) ? cookies : (typeof cookies === 'string' ? cookies.split(';') : []); for (var i = 0; i < cookiesArr.length; i++) { var idx = cookiesArr[i].indexOf('='); if (idx > 0) { if (cookiesArr[i].substring(0, idx).trim() === cookieName) return cookiesArr[i].substring(idx + 1).trim(); } else { if (cookiesArr[i].trim() === cookieName && i + 1 < cookiesArr.length) return cookiesArr[i + 1].trim(); } } return null; } function detectLanguage(uri, headers) { var lang = null; var lowerUri = uri.toLowerCase(); var updatedUri = false; for (var _i = 0, languageDetectionMethods_1 = languageDetectionMethods; _i < languageDetectionMethods_1.length; _i++) { var detectionMethod = languageDetectionMethods_1[_i]; switch (detectionMethod) { case 'cookie': if (!cookieName) continue; lang = getCookieValue(getHeaderValue(headers, 'Cookie')); if (lang && !languagesSet.has(lang)) lang = null; break; case 'http-accept': var rawLangs = getHeaderValue(headers, 'Accept-Language'); if (!rawLangs) continue; var langs = rawLangs.split(/,|;/g).map(function (v) { return v.trim(); }).filter(function (v) { return v.length > 0 && !v.startsWith('q='); }); for (var i = 0; i < langs.length; i++) { if (languagesSet.has(langs[i])) { lang = langs[i]; break; } } break; case 'uri': updatedUri = true; var startIdx = uri.startsWith('/') ? 1 : 0; var endIdx = uri.indexOf('/', startIdx); lang = (endIdx > startIdx ? uri.substring(startIdx, endIdx) : uri.substring(startIdx)).toLowerCase(); if (lang && !languagesSet.has(lang)) { lang = null; } else { uri = endIdx > startIdx ? uri.substring(endIdx) : ''; } break; } if (lang) break; } lang = (lang || defaultLang).toLowerCase(); uri = updatedUri ? uri : (lowerUri.startsWith('/' + lang) ? uri.substring(lang.length + 1) : (lowerUri.startsWith(lang) ? uri.substring(lang.length) : uri)); var queryIdx = uri.indexOf('?'); return { uri: uri, lang: lang, pathUri: queryIdx >= 0 ? uri.substring(0, queryIdx) : uri }; } ; /** * Can be called inside a Next.js middleware to handle language detection and translation loading. * @param req NextRequest object that should be handled. * @returns Object containing redirect and redirectResponseCode if a redirect should be done. */ var nextJsMiddlewareHandler = function (req) { if (!loadedLangs) preloadSync(); var _a = detectLanguage(req.nextUrl.pathname, req.headers), uri = _a.uri, lang = _a.lang, pathUri = _a.pathUri; var isRoot = uri.length <= 1; var response = { language: lang, languages: __spreadArray([], languagesSorted, true), path: pathUri, }; // redirect root if not language prefixed if (redirectRoot && isRoot) { response.redirect = '/' + lang + '/'; response.redirectResponseCode = redirectRootResponseCode; } // update cookie if (cookieName && cookieUpdate) { response.cookie = { name: cookieName, value: lang, options: { expire: cookieExpire, domain: cookieDomain ? cookieDomain : undefined, path: cookiePath ? cookiePath : undefined, } }; } // add language attribute to request object if (languageAttr) { req[languageAttr] = lang; req[languageAttr + 's'] = __spreadArray([], languagesSorted, true); } // add translations attribute to request if (translationsAttr) { response.translations = _getTranslations(lang, defaultLang, [], translationsDir); req[translationsAttr] = response.translations; } // add path attribute to request object if (pathAttr) req[pathAttr] = pathUri; return response; }; /** * Handles language detection and translation loading for a standard HTTP request. * @param req Request object that should be handled. * @returns Object containing redirect and redirectResponseCode if a redirect should be done. */ var handleHttpRequest = function (req) { if (!loadedLangs) preloadSync(); var _a = detectLanguage(req.url, req.headers), uri = _a.uri, lang = _a.lang, pathUri = _a.pathUri; var isRoot = req.url.length <= 1; var response = { language: lang, languages: __spreadArray([], languagesSorted, true), path: pathUri, }; // redirect root if not language prefixed if (redirectRoot && isRoot) { response.redirect = '/' + lang + '/'; response.redirectResponseCode = redirectRootResponseCode; } // update cookie if (cookieName && cookieUpdate) { response.cookie = { name: cookieName, value: lang, options: { expire: cookieExpire, domain: cookieDomain ? cookieDomain : undefined, path: cookiePath ? cookiePath : undefined, } }; } // add language attribute to request object if (languageAttr) { req[languageAttr] = lang; req[languageAttr + 's'] = __spreadArray([], languagesSorted, true); } // add translations attribute to request if (translationsAttr) { response.translations = _getTranslations(lang, defaultLang, [], translationsDir); req[translationsAttr] = response.translations; } // add path attribute to request object if (pathAttr) req[pathAttr] = pathUri; return response; }; /** * Can be passed to an express app to handle language detection and translation loading. * @param req Request object that should be handled. * @param res Response object to which the response should be written. * @param next Function that should be called if the request should be passed to the next middleware. */ var handleExpress = function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { var _a, uri, lang, pathUri, isRoot, cookies, cook; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!!loadedLangs) return [3 /*break*/, 2]; return [4 /*yield*/, preload()]; case 1: _b.sent(); _b.label = 2; case 2: return [4 /*yield*/, detectLanguage(req.url, req.headers)]; case 3: _a = _b.sent(), uri = _a.uri, lang = _a.lang, pathUri = _a.pathUri; isRoot = req.url.length <= 1; // update cookie if (cookieName && cookieUpdate) { cookies = res.get('set-cookie') || []; cookies = res instanceof Array ? cookies : cookies.toString().length === 0 ? [] : [cookies]; cook = cookieName + '=' + lang + (cookieExpire ? '; Max-Age=' + cookieExpire : ''); cook += (cookiePath ? '; Path=' + cookiePath : '') + (cookieDomain ? '; Domain=' + cookieDomain : ''); cookies.push(cook); res.set('set-cookie', cookies); } // redirect root if not language prefixed if (redirectRoot && isRoot) { res.redirect(redirectRootResponseCode, exports.DEFAULTS.REDIRECT_ROOT_RESPONSE_CODE, '/' + lang + '/'); return [2 /*return*/]; } // add language attribute to request object if (languageAttr) { req[languageAttr] = lang; req[languageAttr + 's'] = __spreadArray([], languagesSorted, true); } // add translations attribute to request object if (translationsAttr) req[translationsAttr] = _getTranslations(lang, defaultLang, [], translationsDir); // add path attribute to request object if (pathAttr) req[pathAttr] = pathUri; // remove language prefix from url if (updateUrlParam) req.url = uri; if (next) next(); return [2 /*return*/]; } }); }); }; /** * Optionally preload LanguageRouter so first request can be handled faster. * @returns Promise<void> that resolves when preloading is done. */ handleExpress.preload = preload; /** * Optionally preload LanguageRouter so first request can be handled faster. */ handleExpress.preloadSync = preloadSync; /** * Can be called inside a Next.js middleware to handle language detection and translation loading. * @param req NextRequest object that should be handled. * @returns Object containing redirect and redirectResponseCode if a redirect should be done. */ handleExpress.nextJsMiddlewareHandler = nextJsMiddlewareHandler; return handleExpress; }; exports.LanguageRouter = LanguageRouter; exports.default = { DEFAULTS: exports.DEFAULTS, reloadTranslations: exports.reloadTranslations, getTranslation: exports.getTranslation, getTranslations: exports.getTranslations, getLanguageNames: exports.getLanguageNames, getLanguages: exports.getLanguages, getTranslationFileContent: exports.getTranslationFileContent, getTranslationFileContentSync: exports.getTranslationFileContentSync, LanguageRouter: exports.LanguageRouter, };