Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 61 additions & 31 deletions lib/translator/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,73 @@
* @module paper/lib/translator/filter
*/

/*
* Internal method to filter an object containing string keys using the given keyPrefix
*
* @private
* @param {Object.<string, string>} obj
* @param {string} keyPrefix
* @returns {Object.<string, string>}
*/
function filterByKeyPrefix(obj, keyPrefix) {
const result = {};
for (const key in obj) {
if (typeof key === 'string' && key.startsWith(keyPrefix)) {
result[key] = obj[key];
}
}
return result;
}


/**
* Filter translation and locales by translation key
* Filter translation and locales of the given language object by translation key prefix.
* This is used by the `langJson` helper. This method expects an object in the format
* returned by `translator/transformer.js:transform()`.
*
* The incoming language object looks like this:
* {
* locale: 'en',
* locales: {
* 'salutations.welcome': 'en',
* 'salutations.hello': 'en',
* 'salutations.bye': 'en',
* items: 'en',
* },
* translations: {
* 'salutations.welcome': 'Welcome',
* 'salutations.hello': 'Hello {name}',
* 'salutations.bye': 'Bye bye',
* items: '{count, plural, one{1 Item} other{# Items}}',
* }
* }
*
* The return value, assuming `keyFilter` of `salutations`, would look like this:
* {
* locale: 'en',
* locales: {
* 'salutations.welcome': 'en',
* 'salutations.hello': 'en',
* 'salutations.bye': 'en',
* },
* translations: {
* 'salutations.welcome': 'Welcome',
* 'salutations.hello': 'Hello {name}',
* 'salutations.bye': 'Bye bye',
* }
* }
* @param {Object.<string, string|Object>} language
* @param {string} keyFilter
* @returns {Object.<string, string|Object>}
*/
function filterByKey(language, keyFilter) {
return Object.entries(language)
.reduce(
(result, [key, value]) => {
switch (key) {
case 'translations':
case 'locales':
result[key] =
Object.entries(value)
.reduce(
(filteredInnerObject, [innerKey, innerValue]) => {
if (innerKey.startsWith(keyFilter)) {
filteredInnerObject[innerKey] = innerValue;
}

return filteredInnerObject;
},
{}
);

return result;
default:
result[key] = language[key];

return result;
}
},
{}
);
function filterLanguageObject(language, keyFilter) {
return {
locale: language.locale,
locales: filterByKeyPrefix(language.locales, keyFilter),
translations: filterByKeyPrefix(language.translations, keyFilter),
};
}

module.exports = {
filterByKey: filterByKey,
filterByKey: filterLanguageObject,
};
47 changes: 36 additions & 11 deletions lib/translator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,47 @@ const DEFAULT_LOCALE = 'en';

/**
* Translator constructor
*
* @constructor
* @param {string} acceptLanguage
* @param {Object} allTranslations
* @param {string} acceptLanguage The Accept-Language header to be parsed to determine language to use
* @param {Object} allTranslations Object containing all translations coming from theme
* @param {Object} logger
*/
function Translator(acceptLanguage, allTranslations, logger = console) {
this.logger = logger;

const languages = Transformer.transform(allTranslations, DEFAULT_LOCALE, this.logger);
/**
* @private
* @type {Object}
*/
this._logger = logger;

/**
* @private
* @type {string}
*/
this._locale = LocaleParser.getPreferredLocale(acceptLanguage, languages, DEFAULT_LOCALE);
this._locale = LocaleParser.getPreferredLocale(acceptLanguage, Object.keys(allTranslations), DEFAULT_LOCALE);

/**
* @private
* @type {Object.<string, string>}
*
* Looks like this:
* {
* locale: 'en',
* locales: {
* 'salutations.welcome': 'en',
* 'salutations.hello': 'en',
* 'salutations.bye': 'en',
* items: 'en',
* }
* translations: {
* 'salutations.welcome': 'Welcome',
* 'salutations.hello': 'Hello {name}',
* 'salutations.bye': 'Bye bye',
* items: '{count, plural, one{1 Item} other{# Items}}',
* }
* }
*/
this._language = languages[this._locale] || {};
this._language = Transformer.transform(allTranslations, this._locale, DEFAULT_LOCALE, logger) || {};

/**
* @private
Expand All @@ -55,6 +75,7 @@ function Translator(acceptLanguage, allTranslations, logger = console) {

/**
* Translator factory method
*
* @static
* @param {string} acceptLanguage
* @param {Object} allTranslations
Expand All @@ -67,6 +88,7 @@ Translator.create = function (acceptLanguage, allTranslations, logger = console)

/**
* Get translated string
*
* @param {string} key
* @param {Object} parameters
* @returns {string}
Expand All @@ -84,14 +106,14 @@ Translator.prototype.translate = function (key, parameters) {
try {
return this._formatFunctions[key](parameters);
} catch (err) {
this.logger.error(err);

this._logger.error(err);
return '';
}
};

/**
* Get locale name
*
* @returns {string} Translation locale
*/
Translator.prototype.getLocale = function () {
Expand All @@ -100,6 +122,7 @@ Translator.prototype.getLocale = function () {

/**
* Get language object
*
* @param {string} [keyFilter]
* @returns {Object} Language object
*/
Expand All @@ -113,6 +136,7 @@ Translator.prototype.getLanguage = function (keyFilter) {

/**
* Get formatter
*
* @private
* @param {string} locale
* @returns {MessageFormat} Return cached or new MessageFormat
Expand All @@ -125,6 +149,8 @@ Translator.prototype._getFormatter = function (locale) {

/**
* Compile a translation template and return a formatter function
*
* @private
* @param {string} key
* @return {Function}
*/
Expand All @@ -137,8 +163,7 @@ Translator.prototype._compileTemplate = function (key) {
return formatter.compile(language.translations[key]);
} catch (err) {
if (err.name === 'SyntaxError') {
this.logger.error(`Language File Syntax Error: ${err.message} for key "${key}"`, err.expected);

this._logger.error(`Language File Syntax Error: ${err.message} for key "${key}"`, err.expected);
return () => '';
}

Expand Down
28 changes: 16 additions & 12 deletions lib/translator/locale-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,32 @@ const AcceptLanguageParser = require('accept-language-parser');
const MessageFormat = require('messageformat');

/**
* Get preferred locale
* @param {string} acceptLanguage
* @param {Object} languages
* @param {string} defaultLocale
* Parse the Accept-Language header and return the preferred locale after matching
* up against the list of supported locales.
*
* @param {string} acceptLanguage The Accept-Language header
* @param {Array} supportedLocales A list of supported locales
* @param {string} defaultLocale The default fallback locale
* @returns {string}
*/
function getPreferredLocale(acceptLanguage, languages, defaultLocale) {
const locale = getLocales(acceptLanguage).find(locale => languages[locale]) || defaultLocale;
function getPreferredLocale(acceptLanguage, supportedLocales, defaultLocale) {
const requestedLocales = getLocales(acceptLanguage);
const preferredLocale = requestedLocales.find(locale => supportedLocales.includes(locale)) || defaultLocale;

// Make sure that MessageFormat supports it
try {
new MessageFormat(locale);

return locale;
new MessageFormat(preferredLocale);
return preferredLocale;
} catch (err) {
return defaultLocale;
}
}

/**
* Parse locale header
* @param {string} acceptLanguage
* @returns {string[]} Locales
* Parse Accept-Language header and return a list of locales
*
* @param {string} acceptLanguage The Accept-Language header
* @returns {string[]} List of locale identifiers
*/
function getLocales(acceptLanguage) {
const localeObjects = AcceptLanguageParser.parse(acceptLanguage);
Expand Down
Loading