Source: br-i18n/src/br/i18n/Translator.js

"use strict";

/**
* @module br/i18n/Translator
*/

var Errors = require('br/Errors');
var LocalisedNumber = require('./LocalisedNumber');
var fell = require('fell');
var log = fell.getLogger('br.i18n.Translator');
var I18nStore = require('./I18nStore');
// LocalisedDate and LocalisedTime use br/i18n which depends on this class,
// so they have to be required where they are used or there would be a circular
// dependency.

var regExp = /\@\{(.*?)\}/m;
var TEST_DATE_FORMAT_SHORT = "d-m-Y";
var TEST_DATE_FORMAT_LONG = "D, d M, Y, h:i:s A";


/**
* @class
* @alias module:br/i18n/Translator
*
* @classdesc
* <p>The class within the <code>br.I18N</code> package that is responsible
* for translating localization tokens in the form of
* <code>&#64;{key.name}</code> into translated text.</p>
*
* <p>This class should not be instantiated directly. To access i18n functions
* use the [br/i18n]{@link module:br/i18n} class which returns the
* [br/i18n/I18N]{@link module:br/i18n/I18N} accessor class.
* For example <code>require("br/i18n").i18n("some.i18n.key")</code>.</p>
*/
function Translator(messageDefinitions, useLocale) {
	var defaultLocale = Object.keys(require('service!br.app-meta-service').getLocales())[0];

	/** @private */
	this.localizationPrefs = {};

	I18nStore.initialize(messageDefinitions, useLocale || 'en', defaultLocale);
}

Translator.MESSAGES = {
	UNTRANSLATED_TOKEN_LOG_MSG: 'A translation has not been provided for the i18n key "{0}" in the "{1}" locale'
};

Translator.prototype.setLocale = function(locale) {
	I18nStore.locale = locale;
};

/**
* Translate is used to convert raw localization tokens in the form
* <code>&#64;{key.name}</code> into translated text.
*
* <p>By default this method also converts reserved XML characters (<,>,",',&)
* into XML entity references (> into &gt; etc). If you require raw text
* translation without the XML entity reference conversion, pass a type of
* "text" as an argument to this method.</p>
*
* @param {String} sText The string within which to replace localization tokens.
* @param {String} sType The type of text to translate (defaults to "xml", pass
*      "text" for translation without XML entity reference conversion).
* @function
* @this Translator
* @returns A string with localization tokens replaced with the current locale's
*         messages.
*/
Translator.prototype.translate = function(text, type) {
	var message;
	var match = regExp.exec(text);
	type = type || "xml";
	while (match) {
		message = this._getTranslationForKey(match[1]);
		if (type == "xml") {
			message = this.convertXMLEntityChars(message);
		}
		text = text.replace(match[0], message);
		match = regExp.exec(text);
	}
	return text;
};

/**
 * Returns whether a given localization token is contained in either the current locale or the default locale.
 *
 * <p>Usage: <code>Translator.getTranslator().tokenExists("br.core.field.start.date")</code></p>
 *
 * @param {String} sText The token name
 * @type boolean
 * @returns <code>true</code> if the localization token exists in the current locale or the default locale's
 *         translation set, otherwise <code>false</code>.
 */
Translator.prototype.tokenExists = function(token) {
	return I18nStore.tokenExists(token);
};

/**
* Converts XML reserved characters (<,>,",',&) into XML entity references.
*
* @param {String} text The string within which to replace localization tokens.
* @type String
* @returns A string with every XML reserved character replaced with it's
*         corresponding XML entity reference.
*/
Translator.prototype.convertXMLEntityChars = function(text) {
	text = text.replace(/&/g, "&amp;");
	text = text.replace(/</g, "&lt;");
	text = text.replace(/>/g, "&gt;");
	text = text.replace(/\"/g, "&quot;");
	text = text.replace(/\'/g, "&apos;");

	return text;
};

/**
 * The <code>getMessage</code> method replaces a token with it's translation.
 * Additionally, you can supply extra template arguments that the particular
 * translation might need. For example, a given translations may be
 * ${dialog.message.amountWarning} = "you have [template.key.amount] dollars
 * left in account [template.key.account]". You would call
 * <code>br.i18n("dialog.message.amountWarning",
 * {"template.key.amount":"43234", "template.key.account":"testAccount"});</code>
 * to get the fully translated message "you have 43234 dollars left in account
 * testAccount"
 *
 * @param {String} token The token to be translated.
 * @param {Map} templateArgs The *optional* template arguments a translation
 *            may require.
 * @type String
 * @returns A string with message tokens replaced with the current locale's
 *         messages, possibly with additional substitutions for any template
 *         arguments.
 */
Translator.prototype.getMessage = function(token, templateArgs) {
	templateArgs = templateArgs || {};
	var text = this._getTranslationForKeyOrUndefined(token);
	if (text != null) {
		for (var key in templateArgs) {
			var regEx = new RegExp("\\[" + key + "\\]", "g");
			text = text.replace(regEx, templateArgs[key]);
		}
	}
	return formatTranslationResponseIfTranslationWasUnknown(token, text);
};

/**
 * Returns the current date format string for use in displaying the current date format or for
 * other components that require it to format dates.
 *
 * The string is either the default for the locale or if the user has
 * set a preference then that is returned instead.
 *
 * @type String
 * @returns The date format string, e.g. YYYY-mm-dd.
 */
Translator.prototype.getDateFormat = function() {
	return this.localizationPrefs.dateFormat || this._getTranslationForKey("br.i18n.date.format");
};

/**
 * Returns the shorter version of the current date format string for use in displaying the current date format or for
 * other components that require it to format dates.
 *
 * The string is either the default for the locale or if the user has
 * set a preference then that is returned instead.
 *
 * @type String
 * @returns The date format string, e.g. d/m/Y.
 */
Translator.prototype.getShortDateFormat = function() {
	return this.localizationPrefs.shortDateFormat || this._getTranslationForKey("br.i18n.date.format.typed");
};

/**
 * Formats a JavaScript date object according to the locale date format
 * string or another passed in date format string. If no date format string is
 * supplied, this function will default to the date format string referenced by
 * the localization property <code>br.i18n.date.format</code>.
 *
 * <p>Try using the following:</p>
 * <pre>
 * var oTranslator = Translator.getTranslator();
 * oTranslator.formatDate(myDateObject);
 * </pre>
 *
 * <p>Note that this method will also translate any month names
 * (including abbreviated month names) in the date string to the local equivalents.
 * In order for this translation to work correctly, two sets of localization
 * properties need to be set-up.</p>
 *
 * <p>For translation of long month names define localization properties of the
 * form:
 * date.month.january=January<br/>
 *
 * For translation of abbreviated month names define localization properties of
 * the form:
 * date.month.short.january=Jan</p>
 *
 * @param {Date} date A Date object to output as a formatted string.
 * @param {String} dateFormat An optional date format to use. The date formats
 *               supported are the same as those used by the Moment.js Date object.
 *               Refer to the Moment.js API documentation for further details.
 * @type String
 * @returns The formatted date string.
 */
Translator.prototype.formatDate = function(date, dateFormat) {
	if (!dateFormat) {
		dateFormat = this.getDateFormat();
	}

	var localisedDate = new (require('./LocalisedDate'))(date);
	return localisedDate.format(dateFormat);
};

/**
 * Formats the time appropriately for the locale.
 *
 * <p>By specifying a time separator character (':' for example) as the value
 * of the localization property <code>br.i18n.time.format.separator</code>, a time such
 * as '102001' will be formatted as '10:20:01'.</p>
 *
 * <p>Try using the following:</p>
 * <pre>
 * var oTranslator = Translator.getTranslator();
 * oTranslator.formatTime(102001);
 * </pre>
 *
 * @throws {br.Errors} A LocalisedTime object could not be
 *         instantiated from: <code>vTime</code>.
 * @param {Variant} time An integer or string representing the time.
 * @returns A formatted time string.
 *
 * @type String
 */
Translator.prototype.formatTime = function(time) {
	var localisedTime = new (require('./LocalisedTime'))(time);
	return localisedTime.format();
};

/**
 * Formats the number appropriately for the locale.
 *
 * <p>By specifying a number grouping separator character (',' for example) as
 * the value of the localization property <code>br.i18n.number.grouping.separator</code>,
 * a number such as '1000000' will be formatted as '1,000,000'.</p>
 *
 * <p>Try using the following:</p>
 * <pre>
 * var oTranslator = Translator.getTranslator();
 * oTranslator.formatNumber(1000000);
 * </pre>
 *
 * @throws {br.Errors} A LocalisedNumber object could not be
 *         instantiated from: <code>vNumber</code>.
 * @param {Variant} number A number or a string representing the number.
 * @returns A formatted string representation of the number.
 *
 * @type String
 */
Translator.prototype.formatNumber = function(number, thousandsSeparator) {
	var localisedNumber = new LocalisedNumber(number);
	if (!thousandsSeparator) {
		thousandsSeparator = this.localizationPrefs.thousandsSeparator ||
				this._getTranslationForKey("br.i18n.number.grouping.separator");
	}
	var decimalRadixCharacter = this.localizationPrefs.decimalRadixCharacter ||
			this._getTranslationForKey("br.i18n.decimal.radix.character");

	return localisedNumber.format(thousandsSeparator, decimalRadixCharacter);
};

/**
 * Parses the number appropriately for the locale, by removing the thousands seperators.
 *
 * <p>By specifying a number grouping separator character (',' for example) as the value of the localization property
 *  <code>br.i18n.number.grouping.separator</code>, a number such as '1,000,000' will be parsed as '1000000'.</p>
 *
 * <p>Try using the following:</p>
 * <pre>
 * var translator = Translator.getTranslator();
 * oTranslator.parseNumber('1,000,000.00');
 * </pre>
 *
 * @param {String} number A string representing the number.
 * @param {String} thousandsSeparator (optional) A string representing thousands separator.
 *
 * @returns {Number} A parsed number or null if the value can't be parsed.
 */
Translator.prototype.parseNumber = function(number, thousandsSeparator) {
	if (!thousandsSeparator) {
		thousandsSeparator = this.localizationPrefs.thousandsSeparator ||
				this._getTranslationForKey('br.i18n.number.grouping.separator');
	}

	var decimalPlaceCharacter = this.localizationPrefs.decimalRadixCharacter ||
			this._getTranslationForKey("br.i18n.decimal.radix.character");

	thousandsSeparator = thousandsSeparator.replace(/[-[\]*+?.,\\^$|#\s]/g, "\\$&");
	var regEx = new RegExp(thousandsSeparator, "g");
	number = number.replace(regEx, '');
	number = number.replace(decimalPlaceCharacter, '.');

	var numberLength = number.length;

	if (number[numberLength - 1] === decimalPlaceCharacter) {
		number = number.substr(0, numberLength - 1);
	}

	if (isNaN(number)) {
		return null;
	}

	return Number(number);
};

/**
 * Strings non numeric characters from the specified string.
 *
 * @param {String} value the string to strip the non numeric values from.
 *
 * @returns The string without numeric characters
 * @type String
 */
Translator.prototype.stripNonNumericCharacters = function(value) {
	var length = value.length;
	var joiner = [];
	var isDecimalPointFound = false;
	var decimalPlaceCharacter = this.localizationPrefs.decimalRadixCharacter || this._getTranslationForKey("br.i18n.decimal.radix.character");

	for (var i = 0; i < length; i++) {
		var thisChar = value.charAt(i);
		if (isNaN(thisChar) === true) {
			if (thisChar === decimalPlaceCharacter) {
				if (isDecimalPointFound == false) {
					joiner.push(".");
					isDecimalPointFound = true;
				}
			}
		} else {
			joiner.push(thisChar);
		}
	}
	return joiner.join("");
};

/**
 * Sets localization preferences for the <code>Translator</code>.
 *
 * @param {Map} localizationPrefs A map containing the localization preferences.
 */
Translator.prototype.setLocalizationPreferences = function(localizationPrefs) {
	this.localizationPrefs = localizationPrefs;
};

/** @private */
Translator.prototype._getTranslationForKey = function(token) {
	var text = this._getTranslationForKeyOrUndefined(token);
	return formatTranslationResponseIfTranslationWasUnknown(token, text);
};

/** @private */
Translator.prototype._getTranslationForKeyOrUndefined = function(token) {
	if (!this.tokenExists(token)) {
		var logConsole = (window.jstestdriver) ? jstestdriver.console : window.console;
		if (logConsole && logConsole.warn && !window.suppressI18nWarnings) {
		    logConsole.warn('Unable to find a replacement for the i18n key "' + token + '"');
		}
	}

	var message = I18nStore.getTranslation(token);

	if (typeof message === 'undefined') {
		log.warn(Translator.MESSAGES.UNTRANSLATED_TOKEN_LOG_MSG, token, I18nStore.locale);
		if (!require('service!br.app-meta-service').isDev()) {
			message = I18nStore.getDefaultTranslation(token);
		}
	}

	return message;
};

function formatTranslationResponseIfTranslationWasUnknown(key, text) {
	return (text) ? text : "??? " + key + " ???";
}

module.exports = Translator;