diff --git a/lib/translator/index.js b/lib/translator/index.js index b8a790d..ad97a68 100644 --- a/lib/translator/index.js +++ b/lib/translator/index.js @@ -24,16 +24,16 @@ const DEFAULT_LOCALE = 'en'; * @param {Object} logger * @param {Boolean} omitTransforming */ -function Translator(acceptLanguage, allTranslations, logger = console, omitTransforming = false) { +function Translator(acceptLanguage, allTranslations, logger = console) { this.logger = logger; - this.omitTransforming = omitTransforming; - const languages = this.omitTransforming ? allTranslations : Transformer.transform(allTranslations, DEFAULT_LOCALE, this.logger); + const locales = LocaleParser.getLocales(acceptLanguage); + const languages = Transformer.transform(allTranslations, locales, DEFAULT_LOCALE, this.logger); /** * @private * @type {string} */ - this._locale = LocaleParser.getPreferredLocale(acceptLanguage, languages, DEFAULT_LOCALE); + this._locale = LocaleParser.getPreferredLocale(locales, languages, DEFAULT_LOCALE); this.setLanguage(languages); diff --git a/lib/translator/locale-parser.js b/lib/translator/locale-parser.js index 95b52f9..344610c 100644 --- a/lib/translator/locale-parser.js +++ b/lib/translator/locale-parser.js @@ -8,13 +8,13 @@ const MessageFormat = require('messageformat'); /** * Get preferred locale - * @param {string} acceptLanguage + * @param {string[]} locales * @param {Object} languages * @param {string} defaultLocale * @returns {string} */ -function getPreferredLocale(acceptLanguage, languages, defaultLocale) { - const locale = getLocales(acceptLanguage).find(locale => languages[locale]) || defaultLocale; +function getPreferredLocale(locales, languages, defaultLocale) { + const locale = locales.find(locale => languages[locale]) || defaultLocale; try { new MessageFormat(locale); diff --git a/lib/translator/transformer.js b/lib/translator/transformer.js index 1a6da2d..f20c0cd 100644 --- a/lib/translator/transformer.js +++ b/lib/translator/transformer.js @@ -7,12 +7,13 @@ /** * Transform translations * @param {Object} allTranslations + * @param {string[]} locales * @param {string} defaultLocale * @param {Object} logger * @returns {Object.} Transformed translations */ -function transform(allTranslations, defaultLocale, logger = console) { - return cascade(flatten(allTranslations, logger), defaultLocale); +function transform(allTranslations, locales, defaultLocale, logger = console) { + return cascade(flatten(allTranslations, logger), locales, defaultLocale); } /** @@ -41,11 +42,14 @@ function flatten(allTranslations, logger = console) { /** * Cascade translations + * * @param {Object} allTranslations Flattened translations + * @param {string[]} locales * @param {string} defaultLocale * @returns {Object.} Language objects */ -function cascade(allTranslations, defaultLocale) { +function cascade(allTranslations, locales, defaultLocale) { + const availableLocales = addDefaultLocale(locales, defaultLocale); return Object.entries(allTranslations) .reduce( (result, [locale, translations]) => { @@ -55,6 +59,7 @@ function cascade(allTranslations, defaultLocale) { const regionCodes = locale.split('-'); for (let regionIndex = regionCodes.length - 1; regionIndex >= 0; regionIndex--) { + // keeping parent locale logic as a source of truth to track "available" keys const parentLocale = getParentLocale(regionCodes, regionIndex, defaultLocale); const parentTranslations = allTranslations[parentLocale] || {}; @@ -65,8 +70,11 @@ function cascade(allTranslations, defaultLocale) { result[locale].locales[key] = locale; result[locale].translations[key] = translations[key]; } else if (!result[locale].translations[key]) { - result[locale].locales[key] = parentLocale; - result[locale].translations[key] = parentTranslations[key]; + const preparedLocales = prepareLocales(availableLocales, locale); + const { nextAvailableLocale, nextAvailableTranslation } = getNextLocaleTranslation(preparedLocales, allTranslations, key); + // fallback to old logic in case no languages are present in lang header + result[locale].locales[key] = nextAvailableLocale || parentLocale; + result[locale].translations[key] = nextAvailableTranslation || parentTranslations[key]; } }); } @@ -77,6 +85,56 @@ function cascade(allTranslations, defaultLocale) { ); } +/** + * Adding default locale in case it's absent + * + * @param {string[]} locales + * @param {string} defaultLocale + * @returns {string[]} + */ + function addDefaultLocale(locales, defaultLocale) { + // the object will be mutated, so making a copy of it. + const copiedLocales = [...locales]; + if (locales[locales.length - 1] !== defaultLocale) { + copiedLocales.push(defaultLocale); + } + return copiedLocales; +} + +/** + * Adding default locale in case it's absent + * + * @param {string[]} availableLocales + * @param {string} currentLocale + * @returns {string[]} + */ +function prepareLocales(availableLocales, currentLocale) { + const localeIndex = availableLocales.findIndex(locale => locale == currentLocale); + const locales = availableLocales.slice(localeIndex + 1); + return locales; +} + +/** + * Returns next available pair (locale and translation) in the chain + * + * @param {string[]} locales + * @param {Object} allTranslations Flattened translations + * @param {string} key + * @returns {Object.} selected locale and translation + */ +function getNextLocaleTranslation(locales, allTranslations, key) { + for (const locale of locales) { + if (allTranslations[locale] && allTranslations[locale][key]) { + return { + nextAvailableLocale: locale, + nextAvailableTranslation: allTranslations[locale][key], + } + } + } + + return {}; +} + /** * Get parent locale * @private diff --git a/spec/lib/translator.js b/spec/lib/translator.js index 1b9e1c4..ac213d9 100644 --- a/spec/lib/translator.js +++ b/spec/lib/translator.js @@ -290,6 +290,59 @@ describe('Translator', () => { expect(translator.translate(key)).to.equal(translations.fr.level1.level2); done(); }) - }) + }); + + describe('multiple languages support in accept language header', () => { + it('should sucessfully translate phrase and fallback to de', done => { + translations = { + en: { + search: 'Search', + }, + 'es-mx': { + test: 'Test', + }, + de: { + search: 'German translation', + }, + }; + console.log('before'); + const translator = Translator.create('es-mx,es,de,en', translations); + expect(translator.translate('search')).to.equal(translations.de.search); + done(); + }); + + it('should sucessfully translate phrase and fallback to es-mx, when it is presented in header', done => { + translations = { + en: { + search: 'English translation', + }, + 'es-mx': { + search: 'Spanish Mexico Translation', + }, + de: { + search: 'German translation', + }, + }; + const translator = Translator.create('es,es-mx,de,en', translations); + expect(translator.translate('search')).to.equal(translations['es-mx'].search); + done(); + }); + it('should sucessfully translate phrase and fallback to en when no en in header', done => { + translations = { + en: { + search: 'English translation', + }, + 'es-mx': { + Test: 'Spanish Mexico Translation', + }, + pt: { + search: 'Portuguese translation', + }, + }; + const translator = Translator.create('es-mx,de', translations); + expect(translator.translate('search')).to.equal(translations.en.search); + done(); + }); + }); });