Reference Source

coral-spectrum/coral-utils/src/scripts/I18nProvider.js

/**
 * Copyright 2019 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

/**
 Enumeration for {@link i18n} locales.

 @typedef {Object} I18nLocalesEnum

 @property {String} en
 English (U.S.)
 @property {String} en-us
 English (U.S.)
 @property {String} cs
 Czech (Czechia)
 @property {String} cs-cz
 Czech (Czechia)
 @property {String} da
 Danish (Denmark)
 @property {String} da-dk
 Danish (Denmark)
 @property {String} de
 German (Germany)
 @property {String} de-de
 German (Germany)
 @property {String} es
 Spanish (Spain)
 @property {String} es-es
 Spanish (Spain)
 @property {String} fi
 Finnish (Finland)
 @property {String} fi-fi
 Finnish (Finland)
 @property {String} fr
 French (France)
 @property {String} fr-fr
 French (France)
 @property {String} it
 Italian (Italy)
 @property {String} it-it
 Italian (Italy)
 @property {String} ja
 Japanese (Japan)
 @property {String} ja-jp
 Japanese (Japan)
 @property {String} ko
 Korean (Korea)
 @property {String} ko-kr
 Korean (Korea)
 @property {String} nb
 Norwegian Bokmål (Norway)
 @property {String} nb-no
 Norwegian Bokmål (Norway)
 @property {String} nl
 Dutch (Netherlands)
 @property {String} nl-nl
 Dutch (Netherlands)
 @property {String} pl
 Polish (Poland)
 @property {String} pl-pl
 Polish (Poland)
 @property {String} pt
 Portuguese (Brazil)
 @property {String} pt-br
 Portuguese (Brazil)
 @property {String} ru
 Russian (Russia)
 @property {String} ru-ru
 Russian (Russia)
 @property {String} sv
 Swedish (Sweden)
 @property {String} sv-se
 Swedish (Sweden)
 @property {String} tr
 Turkish (Turkey)
 @property {String} tr-tr
 Turkish (Turkey)
 @property {String} zh-cn
 Simplified Chinese
 @property {String} zh-hans-cn
 Simplified Chinese
 @property {String} zh-hans
 Simplified Chinese
 @property {String} zh-tw
 Traditional Chinese
 @property {String} zh-hant-tw
 Traditional Chinese
 @property {String} zh-hant
 Traditional Chinese
 */
const locales = {
  'en': 'en-US',
  'en-us': 'en-US',
  'cs': 'cs-CZ',
  'cs-cz': 'cs-CZ',
  'da': 'da-DK',
  'da-dk': 'da-DK',
  'de': 'de-DE',
  'de-de': 'de-DE',
  'es': 'es-ES',
  'es-es': 'es-ES',
  'fi': 'fi-FI',
  'fi-fi': 'fi-FI',
  'fr': 'fr-FR',
  'fr-fr': 'fr-FR',
  'it': 'it-IT',
  'it-it': 'it-IT',
  'ja': 'ja-JP',
  'ja-jp': 'ja-JP',
  'ko': 'ko-KR',
  'ko-kr': 'ko-KR',
  'nb': 'nb-NO',
  'nb-no': 'nb-NO',
  'nl': 'nl-NL',
  'pl': 'pl-PL',
  'pl-pl': 'pl-PL',
  'nl-nl': 'nl-NL',
  'pt': 'pt-BR',
  'pt-br': 'pt-BR',
  'ru': 'ru-RU',
  'ru-ru': 'ru-RU',
  'sv': 'sv-SE',
  'sv-se': 'sv-SE',
  'tr': 'tr-TR',
  'tr-tr': 'tr-TR',
  'zh-cn': 'zh-CN',
  'zh-hans-cn': 'zh-CN',
  'zh-hans': 'zh-CN',
  'zh-tw': 'zh-TW',
  'zh-hant-tw': 'zh-TW',
  'zh-hant': 'zh-TW'
};

/**
 Used to store i18n strings.

 @type {Object}
 @property {String} strings.generic
 */
const strings = {
  generic: {}
};

/**
 I18n service to get/set localized strings.
 */
class I18nProvider {
  /**
   @param {Object} [options]
   Options for this combo handler.
   @param {String} [options.locale]
   The <code>locale</code> property defines the locale of the I18nProvider.
   */
  constructor(options) {
    options = options || {};

    // Default locale
    this._locale = 'en-US';

    if (options.locale) {
      this._locale = options.locale;
    }

    this._evaluate = /(\{.+?\})/g;
  }

  /**
   Sets a localized string.

   @param {String} key the key to set
   @param {String} value the value associated with the given key.
   @example
   Coral.i18n.set('English string', 'Translated string');
   Coral.i18n.set('English string: {0}', 'Translated string: {0}');
   Coral.i18n.set('English string: {0}, {1}, and {2}', 'Translated string: {2}, {0}, and {1}');
   Coral.i18n.set('English string: {name}', 'Translated string: {name}');
   Coral.i18n.set('English string: {name1}, {name2}, and {name3}', 'Translated string: {name3}, {name1}, and {name2}');
   */

  /**
   Sets multiple localized strings.

   @param {Array<String, String>} map  a key-value map to add to the strings dictionary.
   @example
   Coral.i18n.set([
   ['English string 1', 'Translated string 1'],
   ['English string 2', 'Translated string 2'],
   ['English string 1 with {0} items','Translated string 1 with {0} items'],
   ['English string 2 with {0} items','Translated string 2 with {0} items'],
   ['English string 1: {0}, {1}, and {2}','Translated string 1: {2}, {0}, and {1}'],
   ['English string 2: {0}, {1}, and {2}','Translated string 2: {2}, {0}, and {1}'],
   ['English string 1: {name}', 'Translated string 1: {name}'],
   ['English string 2: {name}', 'Translated string 2: {name}'],
   ['English string 1: {name1}, {name2}, and {name3}', 'Translated string 1: {name3}, {name1}, and {name2}'],
   ['English string 2: {name1}, {name2}, and {name3}', 'Translated string 2: {name3}, {name1}, and {name2}']
   ]);
   */

  /**
   Sets a localized string, using translation hint.

   @param {String} key the key to set
   @param {String} value the value associated with the given key.
   @param {String} translation_hint the translation hint associated with the given key.
   @example
   Coral.i18n.set('English string', 'Translated string 1', 'Translation hint 1');
   Coral.i18n.set('English string', 'Translated string 2', 'Translation hint 2');
   Coral.i18n.set('English string with {0} items' , 'Translated string 1 with {0} items', 'Translation hint 1');
   Coral.i18n.set('English string with {0} items' , 'Translated string 2 with {0} items', 'Translation hint 2');
   Coral.i18n.set('English string: {0}, {1}, and {2}', 'Translated string 1: {2}, {0}, and {1}', 'Translation hint 1');
   Coral.i18n.set('English string: {0}, {1}, and {2}', 'Translated string 2: {2}, {0}, and {1}', 'Translation hint 2');
   Coral.i18n.set('English string: {name}', 'Translated string 1: {name}', 'Translation hint 1');
   Coral.i18n.set('English string: {name}', 'Translated string 2: {name}', 'Translation hint 2');
   Coral.i18n.set('English string: {name1}, {name2}, and {name3}', 'Translated string 1: {name3}, {name1}, and {name2}', 'Translation hint 1');
   Coral.i18n.set('English string: {name1}, {name2}, and {name3}', 'Translated string 2: {name3}, {name1}, and {name2}', 'Translation hint 2');
   */

  /**
   Sets multiple localized strings, using translation hints.

   @param {Array<String, String, String>} map
   A key-value object map to add to the strings dictionary.
   @example
   Coral.i18n.set([
   ['English string', 'Translated string 1', 'Translation hint 1'],
   ['English string', 'Translated string 2', 'Translation hint 2'],
   ['English string with {0} items', 'Translated string 1 with {0} items', 'Translation hint 1'],
   ['English string with {0} items', 'Translated string 2 with {0} items', 'Translation hint 2'],
   ['English string with {0}, {1} and {2} items', 'Translated string 1 with {0}, {1} and {2} items', 'Translation hint 1'],
   ['English string with {0}, {1} and {2} items', 'Translated string 2 with {0}, {1} and {2} items', 'Translation hint 2'],
   ['English string: {name}', 'Translated string 1: {name}', 'Translation hint 1'],
   ['English string: {name}', 'Translated string 2: {name}', 'Translation hint 2'],
   ['English string: {name1}, {name2}, and {name3}', 'Translated string 1: {name3}, {name1}, and {name2}', 'Translation hint 1'],
   ['English string: {name1}, {name2}, and {name3}', 'Translated string 2: {name3}, {name1}, and {name2}', 'Translation hint 2']
   ]);
   */
  // eslint-disable-next-line func-names
  set(...args) {
    strings.generic[this._locale] = strings.generic[this._locale] || {};

    let key, value, translationHint;
    if (args.length === 0) {
      // Return empty string if called without arguments
      return '';
    } else if (args.length === 1) {
      if (!args[0]) {
        throw new Error('Coral.i18n.set: Single argument must be an array of arrays of key/value/(translation hint).');
      }
      // multiple keys
      else if (typeof args[0] === 'object' && typeof args[0][0] === 'object') {
        for (let i = 0 ; i < args[0].length ; i++) {
          key = args[0][i][0];
          value = args[0][i][1];
          translationHint = args[0][i][2];

          if (translationHint) {
            key = `${key}/[translation hint:${translationHint.replace(/&period;/g, '.')}]`;
          }
          strings.generic[this._locale][key] = value;
        }
      } else {
        throw new Error('Coral.i18n.set: Single argument must be an array of key-value pairs.');
      }
    }
    // single key, no translation hint
    else if (args.length === 2) {
      if (typeof args[0] === 'string' && !!args[0] && typeof args[1] === 'string' && !!args[1]) {
        key = args[0];
        value = args[1];
        strings.generic[this._locale][key] = value;
      } else {
        throw new Error('Coral.i18n.set: Both arguments must be non-empty string values.');
      }
    }
    // single key, with translation hint
    else if (args.length === 3) {
      if (typeof args[0] === 'string' && typeof args[1] === 'string' && typeof args[2] === 'string') {
        key = args[0];
        value = args[1];
        translationHint = args[2];

        if (translationHint !== 'null') {
          key = `${key}/[translation hint:${translationHint.replace(/&period;/g, '.')}]`;
        }

        strings.generic[this._locale][key] = value;
      } else {
        throw new Error('Coral.i18n.set: All arguments must be of string type.');
      }
    } else {
      throw new Error('Coral.i18n.set: Too many arguments provided.');
    }

    return this;
  }

  /**
   Gets a localized string, using named arguments, and translation hint.

   @param {String} key the key of the string to retrieve
   @param {Object} args one more named arguments
   @param {String} translation_hint context information for translators
   @returns {String} the localized string with arguments
   @example
   Coral.i18n.get('English string: {name}', { name: 'foo' }, 'Translation hint 1'); // => 'Translated string 1: foo'
   Coral.i18n.get('English string: {name}', { name: 'foo' }, 'Translation hint 2'); // => 'Translated string 2: foo'
   Coral.i18n.get('English string: {name1}, {name2}, and {name3}', { name1: 'foo', name2: 'bar', name3: 'qux' }, 'Translation hint 1'); // => 'Translated string 1: qux, foo, and bar'
   Coral.i18n.get('English string: {name1}, {name2}, and {name3}', { name1: 'foo', name2: 'bar', name3: 'qux' }, 'Translation hint 2'); // => 'Translated string 2: qux, foo, and bar'
   */

  /**
   Gets a localized string, using arguments, and translation hint.

   @param {String} key the key of the string to retrieve
   @param {String} args one more arguments
   @param {String} translation_hint context information for translators
   @returns {String} the localized string with arguments
   @example
   Coral.i18n.get('English string: {0}', 10, 'Translation hint 1'); // => 'Translated string 1: 10')
   Coral.i18n.get('English string: {0}', 10, 'Translation hint 2'); // => 'Translated string 2: 10')
   Coral.i18n.get('English string: {0}, {1}, and {2}', 10, 20, 30, 'Translation hint 1'); // => 'Translated string 1: 30, 10, and 20'
   Coral.i18n.get('English string: {0}, {1}, and {2}', 10, 20, 30, 'Translation hint 2'); // => 'Translated string 2: 30, 10, and 20'
   */

  /**
   Gets a localized string, using translation hint.

   @param {String} key the key of the string to retrieve
   @param {String} translation_hint context information for translators
   @returns {String} the localized string
   @example
   Coral.i18n.get('English string', 'Translation hint 1'); // => 'Translated string 1'
   Coral.i18n.get('English string', 'Translation hint 2'); // => 'Translated string 2'
   */

  /**
   Gets a localized string, using named arguments.

   @param {String} key the key of the string to retrieve
   @param {Object} args one more named arguments
   @returns {String} the localized string with arguments
   @example
   Coral.i18n.get('English string: {name}', { name: 'foo' }); // => 'Translated string: foo'
   Coral.i18n.get('English string: {name1}, {name2}, and {name3}', { name1: 'foo', name2: 'bar', name3: 'qux' }); // => 'Translated string: qux, foo, and bar'
   */

  /**
   Gets a localized string, using arguments.

   @param {String} key the key of the string to retrieve
   @param {String} args one more arguments
   @returns {String} the localized string with arguments
   @example
   Coral.i18n.get('English string: {0}', 10); // => 'Translated string: 10'
   Coral.i18n.get('English string: {0}, {1}, and {2}', 10, 20, 30); // => 'Translated string: 30, 10, and 20'
   */
  /**
   Gets a localized string.

   @param {String} key the key of the string to retrieve
   @returns {String} the localized string
   @example
   Coral.i18n.get('English string'); // => 'Translated String'
   */
  // eslint-disable-next-line func-names
  get(...args) {
    if (args.length === 0) {
      // Return empty string if called without arguments
      return '';
    }

    // The first argument is always the key
    // Aladdin server stores periods in keys as HTML entities, so we need to match this
    let key = args[0].replace('.', '&period;');

    // The number of required variables can be determined by parsing the string
    const placeholderMatches = key.match(this._evaluate);
    const variablePlaceholderCount = placeholderMatches ? placeholderMatches.length : 0;

    // The hint we'll use to translate
    let translationHint = '';

    let variables = {};
    let variableCount = 0;
    let i;

    // Verify the number of provided arguments matches the placeholder count
    if (args[1] !== null && typeof args[1] === 'object') {
      variables = args[1];

      // Check if provided variables object is complete
      let placeholderName = '';
      for (i = 0 ; i < placeholderMatches.length ; i++) {
        placeholderName = placeholderMatches[i].slice(1).slice(0, -1);
        if (variables[placeholderName] === null || typeof variables[placeholderName] === 'undefined') {
          throw new Error(`Coral.i18n.get: Named key "${placeholderName}" not present in provided object.`);
        }
      }

      // If an additional argument is present, it's the translation hint
      if (typeof args[2] === 'string') {
        translationHint = args[2];
      }
    } else {
      // Assume no translation hint
      variableCount = args.length - 1;

      if (variableCount === variablePlaceholderCount + 1) {
        // If we've got an extra argument, assume it's a translation hint
        translationHint = args[args.length - 1];
      } else if (variableCount !== variablePlaceholderCount) {
        throw new Error(`Coral.i18n.get: Number of variable placeholders (${variablePlaceholderCount}) does not match number of variables (${variableCount}).`);
      }

      // Build variables object
      for (i = 0 ; i < variableCount ; i++) {
        variables[i] = args[i + 1];
      }
    }

    // Include translation hint
    if (translationHint) {
      key = `${key}/[translation hint:${translationHint}]`;
    }

    // Fetch the string
    let str = key;
    for (const component in strings) {
      if (typeof strings[component] !== 'undefined' &&
        typeof strings[component][this._locale] !== 'undefined') {
        str = strings[component][this._locale][key] || str;
      }
    }

    // Optimization for a string with no placeholder
    // e.g. Coral.i18n.get('English string');
    if (variablePlaceholderCount === 0) {
      return str;
    }

    // Replace all variables
    return str.replace(this._evaluate, (name) => {
      name = name.slice(1).slice(0, -1);
      return variables[name];
    });

    // @todo use .toLocaleString(Coral.i18n.locale) in a future release
  }

  /**
   I18n current locale value. See {@link I18nLocalesEnum}.
   @type {String}
   */
  get locale() {
    return this._locale;
  }

  set locale(newLocale) {
    this._locale = newLocale;
  }
}

// sets default locale, based on document lang attribute, if it exists, or en-US otherwise
const docLang = document.documentElement.lang.toLowerCase();
const locale = locales[docLang] || 'en-US';

/**
 An i18n service.

 @type {I18nProvider}
 */
const i18n = new I18nProvider({locale});

export {i18n, strings};