Last active
July 20, 2016 07:15
-
-
Save adriengibrat/1d17d2997bbcad29d1e846f89f2a33c6 to your computer and use it in GitHub Desktop.
messageformat with format & debug
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var mf = (function () { | |
'use strict'; | |
function warn () { | |
/* eslint no-console: off, prefer-spread: off */ | |
console.warn.apply(console, arguments) | |
} | |
function error (message) { | |
/* eslint no-var: off */ | |
var index = 0 | |
var args = arguments | |
var types = {s: String, i: Number, o: JSON.stringify} | |
throw Error(message.replace(/%([soi])/g, function (placeholder, type) { | |
return args.hasOwnProperty(++index) ? types[type](args[index]) : placeholder | |
})) | |
} | |
function expression (code) { | |
return Object.assign(new Function(), { toString: function () { return code; } }) | |
} | |
function source (object) { | |
var withFn = function (_, value) { return 'function' === typeof value ? value.toString() : value; } | |
var stringify = function (value) { return JSON.stringify(value, withFn); } | |
function sourcify (json, value) { | |
if ('function' === typeof value) // /!\ replace only once | |
return json.replace(stringify(value), value.toString()) | |
if (Array.isArray(value)) | |
value.forEach(function (item) { json = sourcify(json, item) }) | |
else if (value && 'object' === typeof value) | |
Object.keys(value).forEach(function (key) { json = sourcify(json, value[key]) }) | |
return json | |
} | |
return sourcify(stringify(object), object) | |
} | |
var ESC = "'" | |
var NUM = new RegExp((ESC + "*#$")) | |
var UNESC = new RegExp(("" + ESC + ESC), 'g') | |
function cleanup (array) { // @todo move cleanup logic in matching ? | |
return array.reduce(function (cleaned, item, index, array) { | |
if ('string' === typeof item) // remove (skip) first/last empty strings & unescape | |
return cleaned.concat(/^\s*$/.test(item) && (0 === index || index === array.length - 1) ? | |
[] : item.replace(UNESC, ESC)) | |
return cleaned.concat([cleanup(item)]) // recusive clean | |
}, []) | |
} | |
// recursive explode by matching pair of tokens | |
function matching (open, close, escape, string, start, end) { | |
if ( start === void 0 ) start = 0; | |
if ( end === void 0 ) end = string.length; | |
var deep = [] | |
var buffer = [] | |
for (var offset = start, token = string.charAt(offset); offset < end; token = string.charAt(++offset)) | |
// @todo test edge cases | |
// https://github.com/format-message/format-message/tree/master/packages/message-format#quote-escaping-rules | |
// http://userguide.icu-project.org/formatparse/messages#TOC-Quoting-Escaping | |
if (escape === token && /[\{\}]/.test(string.charAt(offset + 1))) { | |
for (token = string.charAt(++offset); offset < end; token = string.charAt(++offset)) | |
if (escape === token && escape !== string.charAt(offset + 1)) | |
break // swallow until next (not escaped) escape char | |
} else if (open === token) { | |
if (!deep.length) // prepend first token | |
buffer.push(string.slice(start, offset)) | |
deep.push(offset) | |
} else if (close === token) { | |
if (!deep.length) | |
matching.error(("missing a \"" + open + "\" before the \"" + close + "\""), string, offset) | |
var begin = deep.pop() | |
if (deep.length) // skip sub matching pairs | |
continue | |
buffer.push.apply(// found first match, recurse | |
buffer, [ matching(open, close, escape, string, begin + 1, offset) ].concat( matching(open, close, escape, string, offset + 1, end) ) // till end | |
) | |
break | |
} else if (!deep.length && '#' === token) { | |
var text = string.slice(start, offset + 1) | |
if (text.match(NUM)[0].length % 2) { // not escaped | |
buffer.push.apply( | |
buffer, [ text.slice(0, -1) // text and # | |
, ['#'] ].concat( matching(open, close, escape, string, offset + 1, end) ) // till end | |
) | |
break | |
} | |
} | |
if (!buffer.length) | |
buffer.push(string.slice(start, end)) | |
if (deep.length) | |
matching.error(("missing a \"" + close + "\" matching the \"" + open + "\""), string, start + deep.shift()) | |
return buffer | |
} | |
matching.error = function (message, string, offset) { | |
// eslint-disable-next-line no-console | |
console.warn(((string.slice(0, offset)) + "%c" + (string.substr(offset, 1))), 'background: red; color: white', string.slice(offset + 1) || '<-') | |
var lines = string.slice(0, offset).split(/\r?\n/) | |
error('%s at line %i, column %i (offset %i).', message, lines.length, lines.pop().length + 1, offset) | |
} | |
function bootstrap (settings, runtime, tags, module) { | |
if (runtime.hasOwnProperty(module.name) || tags.hasOwnProperty(module.name)) | |
return // skip if already initialized | |
module.dependencies.forEach(function (dependency) { // init dependencies | |
bootstrap(settings, runtime, tags, compile[dependency]) | |
}) | |
var init = module(settings) // init module | |
Object.defineProperty(runtime, module.name, { value: init.runtime, enumerable: true }) /*, writeable: false, configurable: false*/ | |
Object.defineProperty(tags, module.name, { value: init.tag }) /*, enumerable: false, writeable: false, configurable: false*/ | |
} | |
function compile (settings, runtime, tags, params) { | |
if ('string' === typeof params) | |
return params | |
if ('#' === params[0] && 1 === params.length) // leaves # alone | |
return params[0] | |
var compiler = compile.bind(null, settings, runtime, tags) | |
params = params.map(function (param) { return 'string' === typeof param ? param.trim() : param.map(compiler); }) // recurse | |
if ('string' === typeof params[0]) // explode first param | |
params.splice.apply(params, [ 0, 1 ].concat( params[0].trim().split(/\s*,\s*/) )) | |
var tag = params.splice(1, 1).pop() || 'variable' | |
if (!compile[tag]) | |
error('unkown module %o, unable to compile', tag) | |
bootstrap(settings, runtime, tags, compile[tag]) | |
if (!tags[tag]) | |
error('module %o does not have tag, unable to compile', tag) | |
return tags[tag].apply(tags, [ runtime ].concat( params )) | |
} | |
function add (module) { | |
var dependencies = [], len = arguments.length - 1; | |
while ( len-- > 0 ) dependencies[ len ] = arguments[ len + 1 ]; | |
if (!module || 'function' !== typeof module || !module.name) | |
error('you must provide a module as a named function') | |
if (compile.hasOwnProperty(module.name)) | |
error('%o module is already defined', module.name) | |
compile[module.name] = Object.assign(module, { | |
dependencies: (module.dependencies || dependencies).map(function (dependency) { | |
if (!compile.hasOwnProperty(dependency)) | |
error('add %o dependency before requiring it in %o module', dependency, module.name) | |
return dependency | |
}) | |
}) | |
} | |
function locale (settings) { | |
return { runtime: settings.locale || 'en' } | |
} | |
function debug (settings) { | |
var debug = settings.debug ? 'function' === typeof settings && settings.debug || warn : false | |
return { runtime: debug && expression(("_debug_ || " + debug)) } | |
} | |
variable.dependencies = ['locale', 'debug'] | |
function variable (settings) { | |
return { | |
runtime: new Function(("key, data" + (settings.debug ? ', id' : '')) | |
, "if (data && {}.hasOwnProperty.call(data, key)) return data[key];" | |
+ (settings.debug ? "debug('missing %o key in %o data for message %o with %o locale', key, data, id, locale);" : '') | |
+ "return '{' + key + '}'" | |
) | |
, tag: function (_, key) { return expression(("variable(" + (source(key)) + ", data" + (settings.debug ? ', id' : '') + ")")); } | |
} | |
} | |
function compileOptions (runtime, type, key, params, offset) { | |
var plural = type.replace(/^select/, '') | |
var number = plural && expression(("num(" + (source(key)) + ", data, " + (source(offset)) + (runtime.debug ? ', id' : '') + ")")) | |
var options = params.reduce(function (options, param, index, params) { | |
if ('string' === typeof param) | |
options[param.replace(/^=(\d+)$/, '$1')] = expression( | |
params[index + 1] | |
.map(function (part) { return '#' === part && number || part; }) // replace # in text | |
.map(source).join(' + ') | |
) | |
return options | |
}, {}) | |
// check options | |
var keys = Object.keys(options) | |
if (!options.hasOwnProperty('other')) | |
error('mandatory %s "other" option is missing (found %o) in message %o with %o locale' | |
, type, keys, runtime.id, runtime.locale) | |
var types = runtime[type] && runtime[type].types // only plural/selectordinal have known types | |
if (types) { | |
var unknown = keys.filter(isNaN).filter(function (option) { return -1 === types.indexOf(option); }) | |
unknown.length && warn('unknown %s %o (locale %o only accepts %o) found in message %o with %o locale' | |
, type, unknown, runtime.locale, types, runtime.id, runtime.locale) | |
var forgotten = types.filter(function (option) { return !options.hasOwnProperty(option); }) | |
forgotten.length && warn('%s %o not specified (locale %o use %o) in message %o with %o locale' | |
, type, forgotten, runtime.locale, types, runtime.id, runtime.locale) | |
} | |
return source(options) | |
} | |
function compileSelect (type, runtime, key) { | |
var params = [], len = arguments.length - 3; | |
while ( len-- > 0 ) params[ len ] = arguments[ len + 3 ]; | |
var offset = 0 | |
var args = '' | |
if ('select' !== type) { // plural | |
params[0] = params[0].replace(/^offset:\s*(.+?)\s+/, function (_, number) { | |
if (isNaN(number)) | |
error('%s offset must be a number, invalid %o offset found in message %o with %o locale' | |
, type, number, runtime.id, runtime.locale) | |
offset = Number(number) | |
return '' | |
}) | |
args += ", " + type + ", " + offset | |
} | |
args += runtime.debug ? ((args ? '' : ', null, null') + ", id") : '' | |
return expression(("select(" + (source(key)) + ", data, " + (compileOptions(runtime, type, key, params, offset)) + args + ")")) | |
} | |
select.dependencies = ['variable'] | |
function select (settings) { | |
var debug = settings.debug ? ', id' : '' | |
return { | |
runtime: new Function(("key, data, options, plural, offset" + debug) | |
, "var value = variable(key, data" + debug + "); " | |
+ 'if({}.hasOwnProperty.call(options, value))' | |
+ 'return options[value];' | |
+ 'return plural && options[plural(value - offset)] || options.other' | |
) | |
, tag: compileSelect.bind(null, 'select') | |
} | |
} | |
function num (settings) { | |
var debug = settings.debug ? ', id' : '' | |
return { | |
runtime: new Function(("key, data, offset" + debug) | |
, "var num = data && (data[key] - offset);" | |
+ "return isNaN(num) ? variable(key, data" + debug + ") : num" | |
) | |
} | |
} | |
function noplural () { return 'other' } | |
plural.dependencies = ['locale', 'select', 'num'] | |
function localePlural (settings) { | |
return localePlural.hasOwnProperty(settings.locale) && localePlural[settings.locale] || noplural | |
} | |
if ('function' === typeof plurals) | |
Object.assign(localePlural, plurals()) | |
function plural (settings) { | |
return { | |
runtime: localePlural(settings) | |
, tag: compileSelect.bind(null, 'plural') | |
} | |
} | |
selectordinal.dependencies = ['locale', 'select', 'num'] | |
function localeOrdinal (settings) { | |
return localeOrdinal.hasOwnProperty(settings.locale) && localeOrdinal[settings.locale] || noplural | |
} | |
if ('function' === typeof ordinals) | |
Object.assign(localeOrdinal, ordinals()) | |
function selectordinal (settings) { | |
return { | |
runtime: localeOrdinal(settings) | |
, tag: compileSelect.bind(null, 'selectordinal') | |
} | |
} | |
formats.dependencies = ['currency'] | |
function formats (settings) { | |
return { runtime: settings.formats || {} } | |
} | |
function invalid () { | |
return { runtime: '%o key in %o data is an invalid %s (should be a valid %o) for message %o with %o locale' } | |
} | |
intl.dependencies = ['locale', 'formats', 'invalid', 'debug'] | |
function intl (settings) { | |
return { | |
runtime: new Function('intl, formats, _locale' | |
, "try {" | |
+ "if (!Intl[intl].supportedLocalesOf(locale).length) {" | |
+ "locale = 'object' === typeof navigator && navigator.language || 'en'" | |
+ (settings.debug ? ";debug('formating %o locale is not supported, fallback to %o locale', _locale, locale)" : '') | |
+ "}" | |
+ "} catch (e) {" | |
+ (settings.debug ? "debug('Intl.%s not supported, formating will be broken. Use a polyfill:', intl, 'https://www.npmjs.com/package/intl');" : '') | |
+ "for (var format in formats)" | |
+ "if (formats.hasOwnProperty(format))" | |
+ "formats[format].cache = function (value) {" | |
+ "return value.toLocaleString(locale, formats[format])" | |
+ "}" | |
+ "}" | |
) | |
} | |
} | |
function format (settings, type, intl, formats) { | |
var Type, check | |
intl.replace(/(Date|Number)(Time)?Format/, function (_, Constructor, dateTime) { | |
Type = Constructor | |
check = "isNaN(valid" + (dateTime ? '.getTime()' : '') + ")" | |
}) | |
var invalid = settings.debug && ("debug(invalid, key, data, " + (source(type)) + ", " + (source(Type)) + ", id, locale),") | |
var support = "intl(" + (source(intl)) + ", formats." + type + ", locale)" | |
// const support = `setTimeout(function () { support(${source(intl)}, formats.${type}, locale) })` | |
var debug = settings.debug ? ', id' : '' | |
var body = "function(key, data, format" + debug + ") { " | |
+ "var value = variable(key, data" + debug + ")" | |
+ ", valid = new " + Type + "(value)" | |
+ ", options = formats." + type + "[format] || formats." + type + ".default" | |
+ ", " + type + " = options.cache || (options.cache = new Intl." + intl + "(locale, options).format);" | |
+ "return " + check + " ? (" + (invalid || '') + "value) : " + type + "(valid) }" | |
return Object.assign(expression(("(" + support + ", " + body + ")")), { | |
formats: Object.assign(formats, { // ensure default format, allow default to be a text alias | |
default: 'string' !== typeof formats.default && formats.default | |
|| formats[formats.default] || formats[Object.keys(formats).shift()] | |
}) | |
}) | |
} | |
function compileFormat (formater, runtime, key, format) { | |
var formats = runtime[formater].formats | |
if (format && !formats.hasOwnProperty(format)) | |
warn('unknow %o %s format (supported are %o), will fallback to default %s format in message %o with %o locale' | |
, format, formater, Object.keys(formats), formater, runtime.id, runtime.locale) | |
runtime.formats || (runtime.formats = {}) // add used formats to runtime | |
var used = runtime.formats[formater] || (runtime.formats[formater] = {}) | |
var current = format || 'default' | |
if (!used.hasOwnProperty(current)) | |
used[current] = formats[current] | |
var debug = runtime.debug ? ', id' : '' | |
var args = format ? (", " + (source(format)) + debug) : debug ? (", null" + debug) : '' | |
return expression((formater + "(" + (source(key)) + ", data" + args + ")")) | |
} | |
date.dependencies = ['intl'] | |
function date (settings) { | |
return { | |
runtime: format(settings, 'date', 'DateTimeFormat', settings.date || { | |
default: 'medium' | |
, short: { month: 'numeric', day: 'numeric', year: '2-digit' } | |
, medium: { month: 'short', day: 'numeric', year: 'numeric' } | |
, long: { month: 'long', day: 'numeric', year: 'numeric' } | |
, full: { month: 'long', day: 'numeric', year: 'numeric', weekday: 'long' } | |
}) | |
, tag: compileFormat.bind(null, 'date') | |
} | |
} | |
time.dependencies = ['intl'] | |
function time (settings) { | |
return { | |
runtime: format(settings, 'time', 'DateTimeFormat', settings.time || { | |
default: 'medium' | |
, short: { hour: 'numeric', minute: 'numeric' } | |
, medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' } | |
, long: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' } | |
, full: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' } | |
}) | |
, tag: compileFormat.bind(null, 'time') | |
} | |
} | |
function currency (settings) { | |
return { runtime: settings.currency || 'USD' } | |
} | |
number.dependencies = ['intl', 'currency'] | |
function number (settings) { | |
return { | |
runtime: format(settings, 'number', 'NumberFormat', settings.number || { | |
default: 'decimal' | |
, decimal: { style: 'decimal' } | |
, integer: { style: 'decimal', maximumFractionDigits: 0 } | |
, currency: { style: 'currency', currency: expression('(currency)') } | |
, percent: { style: 'percent' } | |
}) | |
, tag: compileFormat.bind(null, 'number') | |
} | |
} | |
function fallback () { | |
return { | |
runtime: new Function("key, data, fallback", "return data && data[key] || fallback") | |
, tag: function (_, key, fallback) { return expression(("fallback(" + (source(key)) + ", data, " + (source(fallback)) + ")")); } | |
} | |
} | |
add(locale) | |
add(debug) | |
add(variable) // dependencies: locale, debug | |
add(select) // dependency: variable | |
add(num) | |
add(plural) // dependency: locale, select, num | |
add(selectordinal) // dependency: locale, select, num | |
add(currency) | |
add(formats) // dependency: currency | |
add(invalid) | |
add(intl) // dependencies: locale, debug, formats, invalid | |
add(date) // dependency: intl | |
add(time) // dependency: intl | |
add(number) // dependencies: intl, currency | |
add(fallback) | |
Object.assign(mf, {add: add, warn: warn, error: error, expression: expression, source: source}) | |
function vars (runtime) { | |
var vars = Object.keys(runtime).reduce(function (declarations, name) { return declarations.concat((name + " = " + (source(runtime[name])))); } | |
, []) | |
return vars.length ? ("var " + (vars.join(', ')) + "; ") : '' | |
} | |
function mf (messages, settings) { | |
if ( settings === void 0 ) settings = {}; | |
if (!messages) | |
error('you must provide messages (map of {id: message})') | |
var runtime = {} | |
var compiler = compile.bind(null, settings, runtime, {}) | |
var compiled = Object.keys(messages).reduce(function (compiled, id) { | |
runtime.id = id | |
var parts = cleanup(matching('{', '}', ESC, messages[id])).map(compiler) | |
var debug = runtime.debug ? ("var id = " + (source(id)) + ";") : '' | |
compiled[id] = new Function('data', (debug + "return " + (parts.map(source).join(' + ')))) | |
return compiled | |
}, {}) | |
delete runtime.id | |
return new Function('_debug_', ((vars(runtime)) + "return " + (source(compiled)))) | |
} | |
return mf; | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment