Add page localization (#7128)

* added localized pages; added refactored page templates; adapted test cases; introduced localization test cases

* added changelog entry

* fixed test description typo

* fixed bug in PromiseRouter where headers are not added for text reponse

* added page parameters in page headers for programmatic use

* refactored tests for PublicAPIRouter

* added mustache lib for template rendering

* fixed fs.promises module reference

* fixed template placeholder typo

* changed redirect response to provide headers instead of query parameters

* fix lint

* fixed syntax errors and typos in html templates

* removed obsolete URI encoding

* added locale inferring from request body and header

* added end-to-end localizaton test

* added server option validation; refactored pages server option

* fixed invalid redirect URL for no locale matching file

* added end-to-end localizaton tests

* adapted tests to new response content

* re-added PublicAPIRouter; added PagesRouter as experimental feature

* refactored PagesRouter test structure

* added configuration option for custom path to pages

* added configuration option for custom endpoint to pages

* fixed lint

* added tests

* added a distinct page for invalid password reset link

* renamed generic page invalidLink to expiredVerificationLink

* improved HTML files documentation

* improved HTML files documentation

* changed changelog entry for experimental feature

* improved file naming to make it more descriptive

* fixed file naming and env parameter naming

* added readme entry

* fixed readme TOC - hasn't been updated in a while

* added localization with JSON resource

* added JSON localization to feature pages (password reset, email verification)

* updated readme

* updated readme

* optimized JSON localization for feature pages; added e2e test case

* fixed readme typo

* minor refactoring of existing tests

* fixed bug where Object type was not recognized as config key type

* added feature config placeholders

* prettier

* added passing locale to page config placeholder callback

* refactored passing locale to placeholder to pass test

* added config placeholder feature to README

* fixed typo in README
This commit is contained in:
Manuel
2021-02-09 14:03:57 +01:00
committed by GitHub
parent e3ed6e4600
commit 7f47b0427e
41 changed files with 4335 additions and 2903 deletions

123
src/Utils.js Normal file
View File

@@ -0,0 +1,123 @@
/**
* utils.js
* @file General purpose utilities
* @description General purpose utilities.
*/
const path = require('path');
const fs = require('fs').promises;
/**
* The general purpose utilities.
*/
class Utils {
/**
* @function getLocalizedPath
* @description Returns a localized file path accoring to the locale.
*
* Localized files are searched in subfolders of a given path, e.g.
*
* root/
* ├── base/ // base path to files
* │ ├── example.html // default file
* │ └── de/ // de language folder
* │ │ └── example.html // de localized file
* │ └── de-AT/ // de-AT locale folder
* │ │ └── example.html // de-AT localized file
*
* Files are matched with the locale in the following order:
* 1. Locale match, e.g. locale `de-AT` matches file in folder `de-AT`.
* 2. Language match, e.g. locale `de-AT` matches file in folder `de`.
* 3. Default; file in base folder is returned.
*
* @param {String} defaultPath The absolute file path, which is also
* the default path returned if localization is not available.
* @param {String} locale The locale.
* @returns {Promise<Object>} The object contains:
* - `path`: The path to the localized file, or the original path if
* localization is not available.
* - `subdir`: The subdirectory of the localized file, or undefined if
* there is no matching localized file.
*/
static async getLocalizedPath(defaultPath, locale) {
// Get file name and paths
const file = path.basename(defaultPath);
const basePath = path.dirname(defaultPath);
// If locale is not set return default file
if (!locale) {
return { path: defaultPath };
}
// Check file for locale exists
const localePath = path.join(basePath, locale, file);
const localeFileExists = await Utils.fileExists(localePath);
// If file for locale exists return file
if (localeFileExists) {
return { path: localePath, subdir: locale };
}
// Check file for language exists
const language = locale.split('-')[0];
const languagePath = path.join(basePath, language, file);
const languageFileExists = await Utils.fileExists(languagePath);
// If file for language exists return file
if (languageFileExists) {
return { path: languagePath, subdir: language };
}
// Return default file
return { path: defaultPath };
}
/**
* @function fileExists
* @description Checks whether a file exists.
* @param {String} path The file path.
* @returns {Promise<Boolean>} Is true if the file can be accessed, false otherwise.
*/
static async fileExists(path) {
try {
await fs.access(path);
return true;
} catch (e) {
return false;
}
}
/**
* @function isPath
* @description Evaluates whether a string is a file path (as opposed to a URL for example).
* @param {String} s The string to evaluate.
* @returns {Boolean} Returns true if the evaluated string is a path.
*/
static isPath(s) {
return /(^\/)|(^\.\/)|(^\.\.\/)/.test(s);
}
/**
* Flattens an object and crates new keys with custom delimiters.
* @param {Object} obj The object to flatten.
* @param {String} [delimiter='.'] The delimiter of the newly generated keys.
* @param {Object} result
* @returns {Object} The flattened object.
**/
static flattenObject(obj, parentKey, delimiter = '.', result = {}) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const newKey = parentKey ? parentKey + delimiter + key : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
this.flattenObject(obj[key], newKey, delimiter, result);
} else {
result[newKey] = obj[key];
}
}
}
return result;
}
}
module.exports = Utils;