localized-string
Version:
Fetches and formats translated strings.
193 lines (173 loc) • 6.87 kB
JavaScript
;
var fs = require('fs');
function format(message, args) {
var apos = /'(?!')/g; // founds "'", but not "''"
var simpleFormat = /^\d+$/;
var numberFormat = /^(\d+),number$/; // TODO: incomplete, as doesn't support floating point numbers
var choiceFormat = /^(\d+)\,choice\,(.+)/;
var choicePart = /^(\d+)([#<])(.+)/; // TODO: does not work for floating point numbers!
// we are caching RegExps, so will not spend time on recreating them on each call
// formats a value, currently choice and simple replacement are implemented, proper
var getParamValue = function(format, args) {
// simple substitute
/*jshint boss:true */
var res = '';
var match;
if (match = format.match(simpleFormat)) { // TODO: heavy guns for checking whether format is a simple number...
res = args.length > format ? args[format] : ''; // use the argument as is, or use '' if not found
}
// number format
else if (match = format.match(numberFormat)) {
// TODO: doesn't actually format the number...
res = args.length > ++match[1] ? args[match[1]] : '';
}
// choice format
else if (match = format.match(choiceFormat)) {
// format: "0,choice,0#0 issues|1#1 issue|1<{0,number} issues"
// match[0]: "0,choice,0#0 issues|1#1 issue|1<{0,number} issues"
// match[1]: "0"
// match[2]: "0#0 issues|1#1 issue|1<{0,number} issues"
// get the argument value we base the choice on
var value = (args.length > match[1] ? args[match[1]] : null);
if (value !== null) {
// go through all options, checking against the number, according to following formula,
// if X < the first entry then the first entry is returned, if X > last entry, the last entry is returned
//
// X matches j if and only if limit[j] <= X < limit[j+1]
//
var options = match[2].split('|');
var prevOptionValue = null; // holds last passed option
for (var i = 0; i < options.length; i++) {
// option: "0#0 issues"
// part[0]: "0#0 issues"
// part[1]: "0"
// part[2]: "#"
// part[3]" "0 issues";
var parts = options[i].match(choicePart);
// if value is smaller, we take the previous value, or the current if no previous exists
var argValue = parseInt(parts[1], 10);
if (value < argValue) {
if (prevOptionValue) {
res = prevOptionValue;
break;
} else {
res = parts[3];
break;
}
}
// if value is equal the condition, and the match is equality match we accept it
if (value == argValue && parts[2] == '#') {
res = parts[3];
break;
} else {
// value is greater the condition, fall through to next iteration
}
// check whether we are the last option, in which case accept it even if the option does not match
if (i == options.length - 1) {
res = parts[3];
}
// retain current option
prevOptionValue = parts[3];
}
// run result through format, as the parts might contain substitutes themselves
var formatArgs = [res].concat(Array.prototype.slice.call(args, 1));
res = format(formatArgs);
}
}
return res;
};
// drop in replacement for the token regex
// splits the message to return the next accurance of a i18n placeholder.
// Does not use regexps as we need to support nested placeholders
// text between single ticks ' are ignored
var _performTokenRegex = function(message) {
var tick = false;
var openIndex = -1;
var openCount = 0;
for (var i = 0; i < message.length; i++) {
// handle ticks
var c = message.charAt(i);
if (c == '\'') {
// toggle
tick = !tick;
}
// skip if we are between ticks
if (tick) {
continue;
}
// check open brackets
if (c === '{') {
if (openCount === 0) {
openIndex = i;
}
openCount++;
} else if (c === '}') {
if (openCount > 0) {
openCount--;
if (openCount === 0) {
// we found a bracket match - generate the result array (
var match = [];
match.push(message.substring(0, i + 1)); // from begin to match
match.push(message.substring(0, openIndex)); // everything until match start
match.push(message.substring(openIndex + 1, i)); // matched content
return match;
}
}
}
}
return null;
};
var _format = function(message, args) {
var res = '';
var match = _performTokenRegex(message); //message.match(token);
while (match) {
// reduce message to string after match
message = message.substring(match[0].length);
// add value before match to result
res += match[1].replace(apos, '');
// add formatted parameter
res += getParamValue(match[2], args);
// check for next match
match = _performTokenRegex(message); //message.match(token);
}
// add remaining message to result
res += message.replace(apos, '');
return res;
};
return _format(message, args);
}
/* Everything above here is more or less lifted from Atlassian's AUI code (https://bitbucket.org/atlassian/aui/overview)
but slighly modified to work within the Node context since it was designed as a browser module */
var languages = [];
module.exports = function(translations) {
if (typeof translations === 'object') {
languages = translations;
} else if (typeof translations === 'string') {
let lastPathIndex = translations.lastIndexOf('/');
let path = translations.substr(0, lastPathIndex + 1);
let translationFilename = translations.substr(lastPathIndex + 1, translations.length);
let files = fs.readdirSync(path);
let fileRegex = new RegExp('^' + translationFilename + '_([a-z]{2}_[A-Z]{2})$');
for (let i = 0; i < files.length; i++) {
let moduleName = files[i].substr(0, files[i].lastIndexOf('.'));
let match = moduleName.match(fileRegex);
if (match) {
languages[match[1]] = require('./' + path + match[0]);
} else if (moduleName === translationFilename) {
languages[''] = require('./' + path + moduleName);
}
}
} else {
throw new Error('Invalid parameter type for translations, expected object or string but got ' +
typeof translations);
}
return {
getText: function(language, key, args) {
var out = languages[language][key];
if (Object.prototype.toString.call(args) !== '[object Array]') {
args = [args];
}
return out !== undefined ? format(out, args) : key;
}
};
};