lup-language
Version:
Node express middleware for detecting requested language
813 lines (812 loc) • 41.9 kB
JavaScript
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,
};
;