'use strict';
var fs = require('fs');
var path = require('path');
var util = require('util');
var Utility = require('../system/utility.js');
var NEUTRAL = 'default';
var NS_ROOT = '$default';
var NS_BO = '$bo';
var locales = {};
var getCurrentLocale = function () { return NEUTRAL; };
var isInitialized = false;
//region Read locales
function readProjectLocales (pathOfLocales) {
// Read default namespace.
readLocales(NS_ROOT, pathOfLocales);
// Read other namespaces.
fs.readdirSync(pathOfLocales).filter(function (directoryName) {
return fs.statSync(path.join(pathOfLocales, directoryName)).isDirectory() &&
path.extname(directoryName) !== NS_ROOT &&
path.extname(directoryName) !== NS_BO;
}).forEach(function (directoryName) {
readLocales(directoryName, path.join(pathOfLocales, directoryName));
});
}
function readLocales (namespace, localePath) {
locales[namespace] = {};
fs.readdirSync(localePath).filter(function (fileName) {
return fs.statSync(path.join(localePath, fileName)).isFile() && path.extname(fileName) === '.json';
}).forEach(function (fileName) {
var filePath = path.join(localePath, fileName);
locales[namespace][path.basename(fileName, '.json')] = require(filePath);
});
mergeLocales(namespace);
}
function mergeLocales (namespace) {
for (var langExt in locales[namespace]) {
if( locales[namespace].hasOwnProperty( langExt ) ) {
var ix = langExt.indexOf('.');
if (ix > 0) {
// It is an extension.
var langBase = langExt.substr(0, ix);
var objExt = locales[namespace][langExt];
var objBase = locales[namespace][langBase];
if (!objBase)
objBase = { };
// Copy items to base language.
for (var key in objExt) {
if( objExt.hasOwnProperty( key ) ) {
if (objBase[key])
console.log('Duplicated locale: ' + namespace + '["' + langExt + '"].' + key +
' => ' + namespace + '.' + langBase + '.' + key);
else
objBase[key] = objExt[key];
}
}
// Remove extension.
delete locales[namespace][langExt];
}
}
}
}
// Read business-objects locales.
var bolDirectories = ['node_modules/business-objects/locales', 'source/locales', 'locales'];
for (var i = 0; i < bolDirectories.length; i++) {
var bolDirectory = bolDirectories[i];
if (fs.existsSync(path.join(process.cwd(), bolDirectory))) {
readLocales(NS_BO, path.join(process.cwd(), bolDirectory));
break;
}
}
//endregion
//region Custom error
/**
* @classdesc Represents an internationalization error.
* @description Creates an internationalization error object.
*
* @memberof bo
* @constructor
* @param {string} [message] - Human-readable description of the error.
* @param {...*} [messageParams] - Optional interpolation parameters of the message.
*
* @extends {Error}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error Error} for further information.
*/
function I18nError() {
Error.call(this);
var i18nLocales = new i18n('$bo', 'I18nError');
/**
* The name of the error type.
* @type {string}
* @default I18nError
*/
this.name = this.constructor.name;
/**
* Human-readable description of the error.
* @type {string}
*/
this.message = i18nLocales.get.apply(i18nLocales, arguments.length ? arguments : ['default']);
}
util.inherits(I18nError, Error);
//endregion
//region Define message handler
/**
* @classdesc Provide methods to get localized messages.
* @description Creates a new message localizer object.
*
* @memberof bo
* @constructor
* @param {string} [namespace=$default] - The namespace of the messages.
* @param {string} [keyRoot] - The key root of the messages.
*
* @throws {@link bo.I18nError i18n error}: The namespace must be a string value or null.
* @throws {@link bo.I18nError i18n error}: The key root must be a string value or null.
*/
var i18n = function (namespace, keyRoot) {
namespace = Utility.isOptionalString(namespace, 'namespace', I18nError);
keyRoot = Utility.isOptionalString(keyRoot, 'keyRoot', I18nError);
this.namespace = namespace || NS_ROOT;
this.keyRoot = keyRoot || '';
if (this.keyRoot && this.keyRoot.substr(-1) !== '.')
this.keyRoot += '.';
// Immutable object.
Object.freeze(this);
};
/**
* Reads the localized messages of the project.
*
* @function bo.i18n.initialize
* @param {string} pathOfLocales - The relative path of the directory that contains the project messages.
* @param {external.getLocale} [getLocale] - A function that returns the current locale.
*
* @throws {@link bo.I18nError i18n error}: The path of locales must be a non-empty string.
* @throws {@link bo.I18nError i18n error}: The path of locales is not a valid directory path.
* @throws {@link bo.I18nError i18n error}: The locale getter must be a function.
*/
i18n.initialize = function (pathOfLocales, getLocale) {
if (isInitialized)
throw new I18nError('ready');
readProjectLocales(
Utility.getDirectory(pathOfLocales, 'pathOfLocales', I18nError)
);
if (getLocale) {
if (typeof getLocale === 'function')
getCurrentLocale = getLocale;
else
throw new I18nError('function', 'getLocale');
}
isInitialized = true;
};
/* locale*namespace:key1.key2.key3 */
/**
* Gets a localized message of a given identifier.
* The message identifier has the pattern: [locale*][namespace:]key1[[.key2]...]
* Examples:
* <dl>
* <dt>australia</dt>
* <dd>Simple key with default namespace and current locale.</dd>
* <dt>europe.spain.andalusia</dt>
* <dd>Extended key with default namespace and current locale.</dd>
* <dt>geography:australia</dt>
* <dd>Simple key with specified namespace and current locale.</dd>
* <dt>geography:europe.spain.andalusia</dt>
* <dd>Extended key with specified namespace and current locale.</dd>
* <dt>hu\*australia</dt>
* <dd>Simple key with default namespace and specified language.</dd>
* <dt>hu-HU\*europe.spain.andalusia</dt>
* <dd>Extended key with default namespace and specified language with region.</dd>
* <dt>hu\*geography:australia</dt>
* <dd>Simple key with specified namespace and specified language.</dd>
* <dt>hu-HU\*geography:europe.spain.andalusia</dt>
* <dd>Extended key with specified namespace and specified language with region.</dd>
* </dl>
* If localizer is created with a namespace and the message identifier omits the namespace,
* then the localizer namespace will applied. Examples for initial namespace <i>economy</i>:
* <dl>
* <dt>australia</dt>
* <dd>It is interpreted as <i>economy:australia</i></dd>
* <dt>hu-HU\*europe.spain.andalusia</dt>
* <dd>It is interpreted as <i>hu-HU\*economy:europe.spain.andalusia</i></dd>
* <dt>geography:australia</dt>
* <dd>It remains <i>geography:australia</i></dd>
* <dt>hu-HU\*geography:europe.spain.andalusia</dt>
* <dd>It remains <i>hu-HU\*geography:europe.spain.andalusia</i></dd>
* </dl>
* If localizer is created with a key root and the message identifier omits the namespace,
* then the key root is inserted before the key part of the specified identifier.
* Examples for initial key root <i>earth</i>:
* <dl>
* <dt>australia</dt>
* <dd>It is interpreted as <i>earth.australia</i></dd>
* <dt>hu-HU\*europe.spain.andalusia</dt>
* <dd>It is interpreted as <i>hu-HU\*earth.europe.spain.andalusia</i></dd>
* <dt>geography:australia</dt>
* <dd>It remains <i>geography:australia</i></dd>
* <dt>hu-HU\*geography:europe.spain.andalusia</dt>
* <dd>It remains <i>hu-HU\*geography:europe.spain.andalusia</i></dd>
* </dl>
*
* @function bo.i18n#get
* @param {string} messageId - The identifier of the required message.
* @param {...*} [messageParams] - Optional interpolation parameters of the message.
* @returns {string} The localized message for the current locale, if not found
* then the message for the default locale, otherwise the message key.
*
* @throws {@link bo.I18nError i18n error}: The message key must be a non-empty string.
*/
i18n.prototype.get = function (messageId, messageParams) {
var locale, namespace, messageKey;
var asterisk = messageId.indexOf('*');
var colon = messageId.indexOf(':');
// Determine locale.
if (asterisk > -1)
locale = messageId.substr(0, asterisk);
asterisk++;
locale = locale || getCurrentLocale() || NEUTRAL;
// Determine namespace.
if (colon > -1)
namespace = messageId.substring(asterisk, colon);
colon++;
namespace = namespace || this.namespace;
// Determine message key.
messageKey = messageId.substr(Math.max(asterisk, colon));
if (!colon)
messageKey = this.keyRoot + messageKey;
var keys = messageKey.split('.').filter(function (key) {
return key.trim().length > 0;
});
if (!keys.length)
throw new I18nError('messageId');
// If message is not found then the identifier is returned.
var message = messageId;
var found = false;
var messageArgs = arguments;
// Replace message parameters with passed arguments.
function replacer(match) {
var index = new Number(match.substr(1, match.length - 2)) + 1;
var replacement = '';
if (index < messageArgs.length) {
var arg = messageArgs[index];
if (arg !== undefined && arg !== null)
replacement = typeof arg === 'function' ?
(arg.name ? arg.name : '<< unknown >>') :
arg.toString();
}
return replacement;
}
// Find the message in the tree.
function readMessage(messages) {
var base = messages;
for (var i = 0; i < keys.length; i++) {
if (!base[keys[i]])
return false;
if (i + 1 === keys.length) {
message = base[keys[i]];
return true;
} else
base = base[keys[i]];
}
}
// Use the required message set.
var ns = locales[namespace];
// When namespace is valid...
if (ns) {
// Get message of specific locale.
if (ns[locale])
found = readMessage(ns[locale]);
// Get message of general locale.
if (!found) {
var general = locale.substr(0, locale.lastIndexOf('-'));
if (general && ns[general])
found = readMessage(ns[general]);
}
// Get message of default locale.
if (!found && locale !== NEUTRAL && ns[NEUTRAL])
found = readMessage(ns[NEUTRAL]);
}
// Format message with optional arguments.
if (found && messageArgs.length > 1) {
message = message.replace(/{\d+}/g, replacer);
}
return message;
};
//endregion
module.exports = i18n;