UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

195 lines (170 loc) 7.69 kB
import globalize from './internal/globalize'; import * as logger from './internal/log'; /* eslint brace-style: off, complexity:off, eqeqeq:off, max-depth:off, no-cond-assign:off, no-unused-vars:off */ /** * Replaces tokens in a string with arguments, similar to Java's MessageFormat. * Tokens are in the form {0}, {1}, {2}, etc. * * This version also provides support for simple choice formats (excluding floating point numbers) of the form * {0,choice,0#0 issues|1#1 issue|1<{0,number} issues} * * Number format is currently not implemented, tokens of the form {0,number} will simply be printed as {0} * * @method format * @param message the message to replace tokens in * @param arg (optional) replacement value for token {0}, with subsequent arguments being {1}, etc. * @return {String} the message with the tokens replaced * @usage formatString("This is a {0} test", "simple"); */ function formatString(message) { var apos = /'(?!')/g; // founds "'", but not "''" // TODO: does not work for floating point numbers! var simpleFormat = /^\d+$/; var numberFormat = /^(\d+),number$/; // TODO: incomplete, as doesn't support floating point numbers var choiceFormat = /^(\d+),choice,(.+)/; var choicePart = /^(\d+)([#<])(.+)/; // 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 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 isInvalidFormat = false; 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 (parts == null) { isInvalidFormat = true; continue; } // 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]; } if (isInvalidFormat) { logger.error( 'The format "' + format + '" from message "' + message + '" is invalid.' ); } // run result through format, as the parts might contain substitutes themselves var formatArgs = [res].concat(Array.prototype.slice.call(args, 1)); res = formatString.apply(null, 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 _formatString = function (message) { var args = arguments; var res = ''; if (!message) { return res; } var match = _performTokenRegex(message); 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 _formatString.apply(null, arguments); } globalize('format', formatString); export default formatString;