From ef4333cfa599435fc53fc79e8e2b68083b3b4ba0 Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Wed, 11 May 2016 10:16:11 +0200 Subject: [PATCH 1/6] Message: add support for custom formatters Most globalize modules can now be used directly from within messages. Also fixes selectOrdinal not using the correct plural function. The messageformat compiler and runtime are forked from messageformat.js. Fixes #563 --- Gruntfile.js | 79 +++---- examples/amd-bower/main.js | 7 +- examples/node-npm/main.js | 1 + examples/plain-javascript/index.html | 8 + package.json | 4 +- .../validate/parameter-type/plural-type.js | 4 +- src/core.js | 6 + src/currency.js | 8 + src/date.js | 10 + src/message-runtime.js | 9 +- src/message.js | 57 ++++- src/message/compiler.js | 207 ++++++++++++++++++ src/message/formatter-runtime-bind.js | 27 ++- src/message/formatter-runtime.js | 92 ++++++++ src/number.js | 8 + src/plural.js | 21 +- src/plural/generator-fn.js | 4 +- src/relative-time.js | 8 + src/unit.js | 8 + test/functional/message/format-message.js | 4 +- test/functional/message/message-formatter.js | 27 ++- 21 files changed, 504 insertions(+), 95 deletions(-) create mode 100644 src/message/compiler.js create mode 100644 src/message/formatter-runtime.js diff --git a/Gruntfile.js b/Gruntfile.js index 090e044ce..f7616189c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -132,7 +132,8 @@ module.exports = function( grunt ) { paths: { cldr: "../external/cldrjs/dist/cldr", "make-plural": "../external/make-plural/make-plural", - messageformat: "../external/messageformat/messageformat", + "messageformat-parser": "../node_modules/messageformat-parser/parser", + "reserved-words": "../node_modules/reserved-words/lib/reserved-words", "zoned-date-time": "../node_modules/zoned-date-time/src/zoned-date-time" }, shim: { @@ -151,8 +152,7 @@ module.exports = function( grunt ) { // Only for root id's (the ones in src, not in src's subpaths). Note there's no // conditional code checking for this type. onBuildWrite: function( id, path, contents ) { - var messageformat, - name = camelCase( id.replace( /util\/|common\//, "" ) ); + var name = camelCase( id.replace( /util\/|common\//, "" ) ); // MakePlural if ( ( /make-plural/ ).test( id ) ) { @@ -200,68 +200,39 @@ module.exports = function( grunt ) { "/* jshint ignore:end */" ].join( "\n" ) ); - // messageformat - } else if ( ( /messageformat/ ).test( id ) ) { - return contents + // messageformat-parser + } else if ( ( /messageformat-parser/.test( id ) ) ) { - // Remove browserify wrappers. - .replace( /^\(function\(f\)\{if\(typeof exports==="object"&&type.*/, "" ) - .replace( "},{}],2:[function(require,module,exports){", "" ) - .replace( /\},\{"\.\/messageformat-parser":1,"make-plural\/plural.*/, "" ) - .replace( /\},\{\}\]\},\{\},\[2\]\)\(2\)[\s\S]*?$/, "" ) - - // Set `MessageFormat.plurals` and remove `make-plural/plurals` - // completely. This is populated by Globalize on demand. - .replace( /var _cp = \[[\s\S]*?$/, "" ) - .replace( - "MessageFormat.plurals = require('make-plural/plurals')", - "MessageFormat.plurals = {}" - ) - - // Set `MessageFormat._parse` - .replace( - "MessageFormat._parse = require('./messageformat-parser').parse;", - "" - ) - .replace( /module\.exports = \(function\(\) \{([\s\S]*?)\n\}\)\(\);/, [ - "MessageFormat._parse = (function() {", - "$1", - "}()).parse;" + return contents + .replace( /^/, [ + "var Parser;", + "/* jshint ignore:start */\n", + "Parser = (function() {" ].join( "\n" ) ) + .replace( "module.exports = ", "return " ) + .replace( /$/, [ + "}());", + "/* jshint ignore:end */" + ].join( "\n" ) ); - // Remove unused code. - .replace( /if \(!pluralFunc\) \{\n[\s\S]*?\n \}/, "" ) - .replace( /if \(!locale\) \{\n[\s\S]*? \}\n/, "this.lc = [locale];" ) - .replace( /(MessageFormat\.formatters) = \{[\s\S]*?\n\};/, "$1 = {};" ) - .replace( /MessageFormat\.prototype\.setIntlSupport[\s\S]*?\n\};/, "" ) + // reserved-words + } else if ( ( /reserved-words/.test( id ) ) ) { - // Wrap everything into a var assignment. - .replace( "module.exports = MessageFormat;", "" ) + return contents .replace( /^/, [ - "var MessageFormat;", - "/* jshint ignore:start */", - "MessageFormat = (function() {" + "var reserved;", + "/* jshint ignore:start */\n", + "reserved = (function() {", + "var exports = {};" ].join( "\n" ) ) + .replace( "var assert = require\('assert'\);", "" ) + .replace( /^\s*assert\(.*;\s*$/gm, "" ) .replace( /$/, [ - "return MessageFormat;", + "return exports;", "}());", "/* jshint ignore:end */" ].join( "\n" ) ); - // message-runtime - } else if ( ( /message-runtime/ ).test( id ) ) { - messageformat = require( "./external/messageformat/messageformat" ); - delete messageformat.prototype.runtime.fmt; - delete messageformat.prototype.runtime.pluralFuncs; - contents = contents.replace( "Globalize._messageFormat = {};", [ - "/* jshint ignore:start */", - "Globalize._messageFormat = (function() {", - messageformat.prototype.runtime.toString(), - "return {number: number, plural: plural, select: select};", - "}());", - "/* jshint ignore:end */" - ].join( "\n" ) ); - // ZonedDateTime } else if ( ( /zoned-date-time/ ).test( id ) ) { contents = contents.replace( diff --git a/examples/amd-bower/main.js b/examples/amd-bower/main.js index 1d44f831b..9ab900e32 100644 --- a/examples/amd-bower/main.js +++ b/examples/amd-bower/main.js @@ -41,6 +41,7 @@ require([ "json!cldr-data/supplemental/likelySubtags.json", "json!cldr-data/supplemental/metaZones.json", "json!cldr-data/supplemental/plurals.json", + "json!cldr-data/supplemental/ordinals.json", "json!cldr-data/supplemental/timeData.json", "json!cldr-data/supplemental/weekData.json", "json!messages/en.json", @@ -54,9 +55,8 @@ require([ "globalize/plural", "globalize/relative-time", "globalize/unit" -], function( Globalize, enGregorian, enCurrencies, enDateFields, enNumbers, - enTimeZoneNames, enUnits, currencyData, likelySubtags, metaZones, - pluralsData, timeData, weekData, messages, ianaTzData ) { +], function( Globalize, enGregorian, enCurrencies, enDateFields, enNumbers, enUnits, currencyData, + likelySubtags, pluralsData, ordinalsData, timeData, weekData, messages ) { var en, like, number; @@ -72,6 +72,7 @@ require([ likelySubtags, metaZones, pluralsData, + ordinalsData, timeData, weekData ); diff --git a/examples/node-npm/main.js b/examples/node-npm/main.js index 1ce052430..b0faf80ee 100644 --- a/examples/node-npm/main.js +++ b/examples/node-npm/main.js @@ -13,6 +13,7 @@ Globalize.load( require( "cldr-data/supplemental/likelySubtags" ), require( "cldr-data/supplemental/metaZones" ), require( "cldr-data/supplemental/plurals" ), + require( "cldr-data/supplemental/ordinals" ), require( "cldr-data/supplemental/timeData" ), require( "cldr-data/supplemental/weekData" ) ); diff --git a/examples/plain-javascript/index.html b/examples/plain-javascript/index.html index 900303448..c4c04a4b5 100644 --- a/examples/plain-javascript/index.html +++ b/examples/plain-javascript/index.html @@ -267,6 +267,14 @@

Demo output

"pluralRule-count-one": "i = 1 and v = 0 @integer 1", "pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …" } + }, + "plurals-type-ordinal": { + "en": { + "pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", + "pluralRule-count-two": "n % 10 = 2 and n % 100 != 12 @integer 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, …", + "pluralRule-count-few": "n % 10 = 3 and n % 100 != 13 @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, …", + "pluralRule-count-other": " @integer 0, 4~18, 100, 1000, 10000, 100000, 1000000, …" + } } } }); diff --git a/package.json b/package.json index dc2a4afeb..9dfce4d39 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,9 @@ "matchdep": "0.3.0", "mocha": "^3.3.0", "semver": "^5.3.0", - "zoned-date-time": "1.0.0" + "zoned-date-time": "1.0.0", + "messageformat-parser": "^1.0.0", + "reserved-words": "^0.1.1" }, "commitplease": { "nohook": true diff --git a/src/common/validate/parameter-type/plural-type.js b/src/common/validate/parameter-type/plural-type.js index 8991f1907..b0d66e40b 100644 --- a/src/common/validate/parameter-type/plural-type.js +++ b/src/common/validate/parameter-type/plural-type.js @@ -6,8 +6,8 @@ return function( value, name ) { validateParameterType( value, name, - value === undefined || value === "cardinal" || value === "ordinal", - "String \"cardinal\" or \"ordinal\"" + value === undefined || value === "cardinal" || value === "ordinal" || value === "both", + "String \"cardinal\" or \"ordinal\" or \"both\"" ); }; diff --git a/src/core.js b/src/core.js index 28a7b34ba..4cbf334fc 100644 --- a/src/core.js +++ b/src/core.js @@ -86,6 +86,12 @@ Globalize.locale = function( locale ) { return this.cldr; }; +Globalize._messageFmts = {}; + +Globalize.addMessageFormatterFunction = function( name, fn ) { + Globalize._messageFmts[name] = fn; +}; + /** * Optimization to avoid duplicating some internal functions across modules. */ diff --git a/src/currency.js b/src/currency.js index 977758038..5abde745f 100644 --- a/src/currency.js +++ b/src/currency.js @@ -91,6 +91,14 @@ Globalize.prototype.currencyFormatter = function( currency, options ) { return returnFn; }; +Globalize.addMessageFormatterFunction( "currency", function( currency, style ) { + var options = {}; + if ( style ) { + options.style = style; + } + return this.currencyFormatter( currency, options ); +}); + /** * .currencyParser( currency [, options] ) * diff --git a/src/date.js b/src/date.js index 16ec37a9e..496a27064 100644 --- a/src/date.js +++ b/src/date.js @@ -210,6 +210,16 @@ Globalize.prototype.dateToPartsFormatter = function( options ) { return returnFn; }; +[ "date", "time", "datetime" ].map(function( type ) { + Globalize.addMessageFormatterFunction( type, function( p ) { + var options = {}; + if ( p ) { + options[type] = p; + } + return this.dateFormatter( options ); + }); +}); + /** * .dateParser( options ) * diff --git a/src/message-runtime.js b/src/message-runtime.js index 6880351de..eae8bc9de 100644 --- a/src/message-runtime.js +++ b/src/message-runtime.js @@ -2,11 +2,14 @@ define([ "./common/runtime-key", "./common/validate/parameter-type/message-variables", "./core-runtime", - "./message/formatter-fn" -], function( runtimeKey, validateParameterTypeMessageVariables, Globalize, messageFormatterFn ) { + "./message/formatter-fn", + "./message/formatter-runtime" +], function( runtimeKey, validateParameterTypeMessageVariables, Globalize, messageFormatterFn, + messageFormatterRuntime +) { Globalize._messageFormatterFn = messageFormatterFn; -Globalize._messageFormat = {}; +Globalize._messageFormat = new messageFormatterRuntime(); // TODO setStrictNumber Globalize._validateParameterTypeMessageVariables = validateParameterTypeMessageVariables; Globalize.messageFormatter = diff --git a/src/message.js b/src/message.js index b5c4e4675..cc635154c 100644 --- a/src/message.js +++ b/src/message.js @@ -1,6 +1,5 @@ define([ "cldr", - "messageformat", "./common/create-error", "./common/create-error/plural-module-presence", "./common/runtime-bind", @@ -12,15 +11,18 @@ define([ "./common/validate/parameter-type", "./common/validate/parameter-type/plain-object", "./core", + "./message/compiler", + "./message/formatter-runtime", "./message/formatter-fn", "./message/formatter-runtime-bind", "./util/always-array", "cldr/event" -], function( Cldr, MessageFormat, createError, createErrorPluralModulePresence, runtimeBind, +], function( Cldr, createError, createErrorPluralModulePresence, runtimeBind, validateDefaultLocale, validateMessageBundle, validateMessagePresence, validateMessageType, validateParameterPresence, validateParameterType, validateParameterTypePlainObject, Globalize, - messageFormatterFn, messageFormatterRuntimeBind, alwaysArray ) { + messageCompiler, messageFormatterRuntime, messageFormatterFn, messageFormatterRuntimeBind, + alwaysArray ) { var slice = [].slice; @@ -82,17 +84,52 @@ Globalize.prototype.messageFormatter = function( path ) { } validateMessageType( path, message ); - // Is plural module present? Yes, use its generator. Nope, use an error generator. - pluralGenerator = this.plural !== undefined ? - this.pluralGenerator() : - createErrorPluralModulePresence; + var compiler = new messageCompiler( this, Globalize._messageFmts ); + var formatterSrc = compiler + .compile( message, cldr.locale ); + + var pluralType = false; + if ( compiler.pluralTypes.cardinal && compiler.pluralTypes.ordinal ) { + pluralType = "both"; + } else if ( compiler.pluralTypes.cardinal ) { + pluralType = "cardinal"; + } else if ( compiler.pluralTypes.ordinal ) { + pluralType = "ordinal"; + } + + if ( pluralType !== false ) { + + // Is plural module present? Yes, use its generator. Nope, use an error generator. + pluralGenerator = this.plural !== undefined ? + this.pluralGenerator( { type: pluralType } ) : + createErrorPluralModulePresence; + } - formatter = new MessageFormat( cldr.locale, pluralGenerator ).compile( message ); + var runtime = new messageFormatterRuntime( compiler.strictNumberSign ); + + /* jshint evil:true */ + formatter = new Function( + "number, plural, select, fmt", messageCompiler.funcname( cldr.locale ), + "return " + formatterSrc )( + runtime.number, runtime.plural, runtime.select, compiler.formatters, pluralGenerator + ); returnFn = messageFormatterFn( formatter ); - runtimeBind( args, cldr, returnFn, - [ messageFormatterRuntimeBind( cldr, formatter ), pluralGenerator ] ); + var runtimeArgs = [ + messageFormatterRuntimeBind( + cldr, formatter, compiler.runtime, pluralType, cldr.locale, compiler.formatters + ) + ]; + + if ( pluralGenerator ) { + runtimeArgs.push( pluralGenerator ); + } + runtimeArgs = runtimeArgs.concat( + compiler.formatters + ); + + runtimeBind( args, cldr, returnFn, runtimeArgs ); return returnFn; }; diff --git a/src/message/compiler.js b/src/message/compiler.js new file mode 100644 index 000000000..20293596d --- /dev/null +++ b/src/message/compiler.js @@ -0,0 +1,207 @@ +define([ + "../core", + "messageformat-parser", + "reserved-words" +], function( Globalize, Parser, reserved ) { + +/** Creates a new message compiler. + * + * @class + * @param {object} context - Context (this) for formatter functions + * @param {array} fmtFuncs - Formatter functions + * @property {object} runtime - Names of the core runtime functions used by the compiled functions + * @property {array} formatters - The formatter functions used by the compiled functions + * @property {object} pluralTypes - The plural types used by the compiled functions + */ +function Compiler( context, fmtFuncs ) { + this._globalize = Globalize; + this.fmt = fmtFuncs; + this.runtime = {}; + this.pluralTypes = { + ordinal: false, + cardinal: false + }; + this.formatters = []; + this.context = context; +} + +/** Enable or disable the addition of Unicode control characters to all input + * to preserve the integrity of the output when mixing LTR and RTL text. + * + * @see http://cldr.unicode.org/development/development-process/design-proposals/bidi-handling-of-structured-text + * + * @memberof Compiler + * @param {boolean} [enable=true] + * @returns {Compiler} The Compiler instance, to allow for chaining + */ +Compiler.prototype.setBiDiSupport = function( enable ) { + this.bidiSupport = !!enable || ( typeof enable === "undefined" ); + return this; +}; + +/** According to the ICU MessageFormat spec, a `#` character directly inside a + * `plural` or `selectordinal` statement should be replaced by the number + * matching the surrounding statement. By default, messageformat.js will + * replace `#` signs with the value of the nearest surrounding `plural` or + * `selectordinal` statement. + * + * Set this to true to follow the stricter ICU MessageFormat spec, and to + * throw a runtime error if `#` is used with non-numeric input. + * + * @memberof Compiler + * @param {boolean} [enable=true] + * @returns {Compiler} The Compiler instance, to allow for chaining + */ +Compiler.prototype.setStrictNumberSign = function( enable ) { + this.strictNumberSign = !!enable || ( typeof enable === "undefined" ); + + // TODO + // this.runtime.setStrictNumber(this.strictNumberSign); + return this; +}; + +/** Utility function for quoting an Object's key value if required + * + * Quotes the key if it contains invalid characters or is an + * ECMAScript 3rd Edition reserved word (for IE8). + */ +Compiler.propname = function( key, obj ) { + if ( /^[A-Z_$][0-9A-Z_$]*$/i.test( key ) && + [ "break", "continue", "delete", "else", "for", "function", "if", "in", "new", + "return", "this", "typeof", "var", "void", "while", "with", "case", "catch", + "default", "do", "finally", "instanceof", "switch", "throw", "try" ].indexOf( key ) < 0 ) { + return obj ? obj + "." + key : key; + } else { + var jkey = JSON.stringify( key ); + return obj ? obj + "[" + jkey + "]" : jkey; + } +}; + +/** Utility function for escaping a function name if required + */ +Compiler.funcname = function( key ) { + var fn = key.trim().replace( /\W+/g, "_" ); + return reserved.check( fn, "es2015", true ) || /^\d/.test( fn ) ? "_" + fn : fn; +}; + +/** Utility formatter function for enforcing Bidi Structured Text by using UCC + * + * List inlined from data extracted from CLDR v27 & v28 + * To verify/recreate, use the following: + * + * git clone https://github.com/unicode-cldr/cldr-misc-full.git + * cd cldr-misc-full/main/ + * grep characterOrder -r . | tr '"/' '\t' | cut -f2,6 | grep -C4 right-to-left + */ +Compiler.bidiMarkText = function( text, locale ) { + function isLocaleRTL( locale ) { + var rtlLanguages = [ "ar", "ckb", "fa", "he", "ks($|[^bfh])", "lrc", "mzn", + "pa-Arab", "ps", "ug", "ur", "uz-Arab", "yi" ]; + return new RegExp( "^" + rtlLanguages.join( "|^" ) ).test( locale ); + } + var mark = JSON.stringify( isLocaleRTL( locale ) ? "\u200F" : "\u200E" ); + return mark + " + " + text + " + " + mark; +}; + +/** @private */ +Compiler.prototype.cases = function( token, plural ) { + var needOther = true; + var r = token.cases.map( function( c ) { + if ( c.key === "other" ) { + needOther = false; + } + var s = c.tokens.map( function( tok ) { return this.token( tok, plural ); }, this ); + return Compiler.propname( c.key ) + ": " + ( s.join( " + " ) || "\"\"" ); + }, this ); + if ( needOther ) { + throw new Error( "No 'other' form found in " + JSON.stringify( token ) ); + } + return "{ " + r.join( ", " ) + " }"; +}; + +/** @private */ +Compiler.prototype.token = function( token, plural ) { + if ( typeof token === "string" ) { + return JSON.stringify( token ); + } + + var fn, args = [ Compiler.propname( token.arg, "d" ) ]; + switch ( token.type ) { + case "argument": + return this.bidiSupport ? Compiler.bidiMarkText( args[0], this.lc ) : args[0]; + + case "select": + fn = "select"; + args.push( this.cases( token, this.strictNumberSign ? null : plural ) ); + this.runtime.select = true; + break; + + case "selectordinal": + fn = "plural"; + args.push( 0, Compiler.funcname( this.lc ), this.cases( token, token ), 1 ); + this.runtime.plural = true; + this.pluralTypes.ordinal = true; + break; + + case "plural": + fn = "plural"; + args.push( + token.offset || 0, + Compiler.funcname( this.lc ), this.cases( token, token ) + ); + this.runtime.plural = true; + this.pluralTypes.cardinal = true; + break; + + case "function": + if ( !this.fmt[token.key] ) { + throw new Error( "Formatting function " + + JSON.stringify( token.key ) + " not found!" ); + } + fn = "fmt[" + JSON.stringify( this.formatters.length ) + "]"; + this.formatters.push( this.fmt[token.key].apply( this.context, token.params || [] ) ); + break; + + case "octothorpe": + if ( !plural ) { + return "\"#\""; + } + fn = "number"; + args = [ Compiler.propname( plural.arg, "d" ), JSON.stringify( plural.arg ) ]; + if ( plural.offset ) { + args.push( plural.offset ); + } + + this.runtime.number = true; + break; + } + + if ( !fn ) { + throw new Error( "Parser error for token " + JSON.stringify( token ) ); + } + return fn + "(" + args.join( ", " ) + ")"; +}; + +/** Recursively compile a string or a tree of strings to JavaScript function sources + * + * If `src` is an object with a key that is also present in `plurals`, the key + * in question will be used as the locale identifier for its value. To disable + * the compile-time checks for plural & selectordinal keys while maintaining + * multi-locale support, use falsy values in `plurals`. + * + * @param {string|object} src - the source for which the JS code should be generated + * @param {string} lc - the default locale + * @param {object} plurals - a map of pluralization keys for all available locales + */ +Compiler.prototype.compile = function( src, lc ) { + this.lc = lc; + + // TODO: pc is only needed for validation, disable for now. + var pc = { cardinal: [], ordinal: [] }; + var r = Parser.parse( src, pc ).map( function( token ) { return this.token( token ); }, this ); + return "function(d) { return " + ( r.join( " + " ) || "\"\"" ) + "; }"; +}; + +return Compiler; + +}); diff --git a/src/message/formatter-runtime-bind.js b/src/message/formatter-runtime-bind.js index c2b285d65..57fac327c 100644 --- a/src/message/formatter-runtime-bind.js +++ b/src/message/formatter-runtime-bind.js @@ -1,8 +1,7 @@ define(function() { -return function( cldr, messageformatter ) { - var locale = cldr.locale, - origToString = messageformatter.toString; +return function( cldr, messageformatter, runtime, pluralType, locale, embeddedFormatters ) { + var origToString = messageformatter.toString; messageformatter.toString = function() { var argNames, argValues, output, @@ -11,24 +10,28 @@ return function( cldr, messageformatter ) { // Properly adjust SlexAxton/messageformat.js compiled variables with Globalize variables: output = origToString.call( messageformatter ); - if ( /number\(/.test( output ) ) { + if ( runtime.number ) { args.number = "messageFormat.number"; } - if ( /plural\(/.test( output ) ) { + if ( runtime.plural ) { args.plural = "messageFormat.plural"; } - if ( /select\(/.test( output ) ) { + if ( runtime.select ) { args.select = "messageFormat.select"; } - output.replace( /pluralFuncs(\[([^\]]+)\]|\.([a-zA-Z]+))/, function( match ) { - args.pluralFuncs = "{" + - "\"" + locale + "\": Globalize(\"" + locale + "\").pluralGenerator()" + - "}"; - return match; - }); + if ( embeddedFormatters.length ) { + args.fmt = "[" + embeddedFormatters.map( function( fn ) { + return fn.generatorString(); + } ).join( ", " ) + "]"; + } + + if ( pluralType !== false ) { + args[locale] = "Globalize(\"" + locale + "\")." + + "pluralGenerator( { type: \"" + pluralType + "\" } )"; + } argNames = Object.keys( args ).join( ", " ); argValues = Object.keys( args ).map(function( key ) { diff --git a/src/message/formatter-runtime.js b/src/message/formatter-runtime.js new file mode 100644 index 000000000..c2c53d239 --- /dev/null +++ b/src/message/formatter-runtime.js @@ -0,0 +1,92 @@ +define([], function() { + +function Runtime( strictNumberSign ) { + this.setStrictNumber( strictNumberSign ); +} + +/** Utility function for `#` in plural rules + * + * Will throw an Error if `value` has a non-numeric value and `offset` is + * non-zero or {@link MessageFormat#setStrictNumberSign} is set. + * + * @function Runtime#number + * @param {number} value - The value to operate on + * @param {string} name - The name of the argument, used for error reporting + * @param {number} [offset=0] - An optional offset, set by the surrounding context + * @returns {number|string} The result of applying the offset to the input value + */ +function defaultNumber( value, name, offset ) { + if ( !offset ) { + return value; + } + if ( isNaN( value ) ) { + throw new Error( "Can't apply offset:" + offset + " to argument `" + name + + "` with non-numerical value " + JSON.stringify( value ) + "." ); + } + return value - offset; +} + +/** @private */ +function strictNumber( value, name, offset ) { + if ( isNaN( value ) ) { + throw new Error( "Argument `" + name + "` has non-numerical value " + + JSON.stringify( value ) + "." ); + } + return value - ( offset || 0 ); +} + +/** Set how strictly the {@link number} method parses its input. + * + * According to the ICU MessageFormat spec, `#` can only be used to replace a + * number input of a `plural` statement. By default, messageformat.js does not + * throw a runtime error if you use non-numeric argument with a `plural` rule, + * unless rule also includes a non-zero `offset`. + * + * This is called by {@link MessageFormat#setStrictNumberSign} to follow the + * stricter ICU MessageFormat spec. + * + * @param {boolean} [enable=false] + */ +Runtime.prototype.setStrictNumber = function( enable ) { + this.number = enable ? strictNumber : defaultNumber; +}; + +/** Utility function for `{N, plural|selectordinal, ...}` + * + * @param {number} value - The key to use to find a pluralization rule + * @param {number} offset - An offset to apply to `value` + * @param {function} lcfunc - A locale function from `pluralFuncs` + * @param {Object.} data - The object from which results are looked up + * @param {?boolean} isOrdinal - If true, use ordinal rather than cardinal rules + * @returns {string} The result of the pluralization + */ +Runtime.prototype.plural = function( value, offset, lcfunc, data, isOrdinal ) { + if ( {}.hasOwnProperty.call( data, value ) ) { + return data[value]; + } + if ( offset ) { + value -= offset; + } + var key = lcfunc( value, isOrdinal ); + if ( key in data ) { + return data[key]; + } + return data.other; +}; + +/** Utility function for `{N, select, ...}` + * + * @param {number} value - The key to use to find a selection + * @param {Object.} data - The object from which results are looked up + * @returns {string} The result of the select statement + */ +Runtime.prototype.select = function( value, data ) { + if ( {}.hasOwnProperty.call( data, value ) ) { + return data[value]; + } + return data.other; +}; + +return Runtime; + +}); diff --git a/src/number.js b/src/number.js index 4922e4b27..f4bef6870 100644 --- a/src/number.js +++ b/src/number.js @@ -100,6 +100,14 @@ Globalize.prototype.numberFormatter = function( options ) { return returnFn; }; +Globalize.addMessageFormatterFunction( "number", function( style ) { + var options = {}; + if ( style ) { + options.style = style; + } + return this.numberFormatter( options ); +}); + /** * .numberParser( [options] ) * diff --git a/src/plural.js b/src/plural.js index b9781546c..9895f0b2b 100644 --- a/src/plural.js +++ b/src/plural.js @@ -48,7 +48,7 @@ Globalize.prototype.plural = function( value, options ) { */ Globalize.pluralGenerator = Globalize.prototype.pluralGenerator = function( options ) { - var args, cldr, isOrdinal, plural, returnFn, type; + var args, cldr, isOrdinal, isCardinal, plural, returnFn, type; validateParameterTypePlainObject( options, "options" ); @@ -62,18 +62,29 @@ Globalize.prototype.pluralGenerator = function( options ) { validateDefaultLocale( cldr ); - isOrdinal = type === "ordinal"; + isOrdinal = type === "ordinal" || type === "both"; + isCardinal = type === "cardinal" || type === "both"; cldr.on( "get", validateCldr ); - cldr.supplemental([ "plurals-type-" + type, "{language}" ]); + if ( isOrdinal ) { + cldr.supplemental([ "plurals-type-ordinal", "{language}" ]); + } + if ( isCardinal ) { + cldr.supplemental([ "plurals-type-cardinal", "{language}" ]); + } cldr.off( "get", validateCldr ); MakePlural.rules = {}; - MakePlural.rules[ type ] = cldr.supplemental( "plurals-type-" + type ); + if ( isOrdinal ) { + MakePlural.rules.ordinal = cldr.supplemental( "plurals-type-ordinal" ); + } + if ( isCardinal ) { + MakePlural.rules.cardinal = cldr.supplemental( "plurals-type-cardinal" ); + } plural = new MakePlural( cldr.attributes.language, { "ordinals": isOrdinal, - "cardinals": !isOrdinal + "cardinals": isCardinal }); returnFn = pluralGeneratorFn( plural ); diff --git a/src/plural/generator-fn.js b/src/plural/generator-fn.js index 3c052396c..79cc58551 100644 --- a/src/plural/generator-fn.js +++ b/src/plural/generator-fn.js @@ -4,11 +4,11 @@ define([ ], function( validateParameterPresence, validateParameterTypeNumber ) { return function( plural ) { - return function pluralGenerator( value ) { + return function pluralGenerator( value, ord ) { validateParameterPresence( value, "value" ); validateParameterTypeNumber( value, "value" ); - return plural( value ); + return plural( value, ord ); }; }; diff --git a/src/relative-time.js b/src/relative-time.js index 4c36886af..557d2a2e9 100644 --- a/src/relative-time.js +++ b/src/relative-time.js @@ -74,6 +74,14 @@ Globalize.prototype.relativeTimeFormatter = function( unit, options ) { return returnFn; }; +Globalize.addMessageFormatterFunction( "relativetime", function( unit, form ) { + var options = {}; + if ( form ) { + options.form = form; + } + return this.relativeTimeFormatter( unit, options ); +}); + return Globalize; }); diff --git a/src/unit.js b/src/unit.js index 024b68e51..7349e22bd 100644 --- a/src/unit.js +++ b/src/unit.js @@ -69,6 +69,14 @@ Globalize.prototype.unitFormatter = function( unit, options ) { return returnFn; }; +Globalize.addMessageFormatterFunction( "unit", function( unit, form ) { + var options = {}; + if ( form ) { + options.form = form; + } + return this.unitFormatter( unit, options ); +}); + return Globalize; }); diff --git a/test/functional/message/format-message.js b/test/functional/message/format-message.js index 5cd71f2be..3c1f2dd16 100644 --- a/test/functional/message/format-message.js +++ b/test/functional/message/format-message.js @@ -2,16 +2,18 @@ define([ "globalize", "json!cldr-data/supplemental/likelySubtags.json", "json!cldr-data/supplemental/plurals.json", + "json!cldr-data/supplemental/ordinals.json", "../../util", "globalize/message", "globalize/plural" -], function( Globalize, likelySubtags, plurals, util ) { +], function( Globalize, likelySubtags, plurals, ordinals, util ) { QUnit.module( ".formatMessage( path [, variables] )", { setup: function() { Globalize.load( likelySubtags ); Globalize.load( plurals ); + Globalize.load( ordinals ); Globalize.loadMessages({ en: { greetings: { diff --git a/test/functional/message/message-formatter.js b/test/functional/message/message-formatter.js index 08224fd90..74fc8f294 100644 --- a/test/functional/message/message-formatter.js +++ b/test/functional/message/message-formatter.js @@ -2,12 +2,13 @@ define([ "globalize", "json!cldr-data/supplemental/likelySubtags.json", "json!cldr-data/supplemental/plurals.json", + "json!cldr-data/supplemental/ordinals.json", "../../util", "cldr/unresolved", "globalize/message", "globalize/plural" -], function( Globalize, likelySubtags, plurals, util ) { +], function( Globalize, likelySubtags, plurals, ordinals, util ) { QUnit.assert.messageFormatter = function( locale, path, variables, expected ) { if ( arguments.length === 3 ) { @@ -28,6 +29,7 @@ QUnit.module( ".messageFormatter( path )", { setup: function() { Globalize.load( likelySubtags ); Globalize.load( plurals ); + Globalize.load( ordinals ); Globalize.loadMessages({ root: { amen: "Amen" @@ -60,7 +62,11 @@ QUnit.module( ".messageFormatter( path )", { " one {one task}", " other {# tasks}", "} remaining" - ] + ], + ordinal: [ + "{cat, selectordinal, one{#st} two{#nd} few{#rd} other{#th} }", + "category" + ], }, "en-GB": {}, fr: {}, @@ -173,6 +179,23 @@ QUnit.test( "should support ICU message format", function( assert ) { }), "You and Beethoven liked this" ); assert.equal( like({ count: 3 }), "You and 2 others liked this" ); + + // Selectordinal + assert.messageFormatter( "en", "ordinal", { + cat: 1, + }, "1st category" ); + + assert.messageFormatter( "en", "ordinal", { + cat: 2, + }, "2nd category" ); + + assert.messageFormatter( "en", "ordinal", { + cat: 3, + }, "3rd category" ); + + assert.messageFormatter( "en", "ordinal", { + cat: 4, + }, "4th category" ); }); // Reference #473 From 781b9fb2cf19de14688f69c5a41ac1ca7b436c26 Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Tue, 22 Nov 2016 17:49:06 +0100 Subject: [PATCH 2/6] Message: Remove duplication in compiled formatters messageformatterFn now uses the runtimeArgs generated by globalize-compiler instead of having them duplicated by messageFormatterRuntimeBind. --- src/message.js | 15 ++++++---- src/message/formatter-fn.js | 1 + src/message/formatter-runtime-bind.js | 41 ++++++++++----------------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/message.js b/src/message.js index cc635154c..cdf8bfe37 100644 --- a/src/message.js +++ b/src/message.js @@ -109,16 +109,19 @@ Globalize.prototype.messageFormatter = function( path ) { /* jshint evil:true */ formatter = new Function( - "number, plural, select, fmt", messageCompiler.funcname( cldr.locale ), - "return " + formatterSrc )( - runtime.number, runtime.plural, runtime.select, compiler.formatters, pluralGenerator - ); + "number, plural, select", messageCompiler.funcname( cldr.locale ), + " var fmt = [].slice.call( arguments, 4 );\n" + + " return " + formatterSrc + "\n" + ); - returnFn = messageFormatterFn( formatter ); + returnFn = messageFormatterFn.apply( this, [ + formatter, + runtime.number, runtime.plural, runtime.select, pluralGenerator + ].concat( compiler.formatters ) ); var runtimeArgs = [ messageFormatterRuntimeBind( - cldr, formatter, compiler.runtime, pluralType, cldr.locale, compiler.formatters + formatter, formatterSrc, compiler.runtime, pluralType, cldr.locale, compiler.formatters ) ]; diff --git a/src/message/formatter-fn.js b/src/message/formatter-fn.js index 720f481cb..39e837058 100644 --- a/src/message/formatter-fn.js +++ b/src/message/formatter-fn.js @@ -3,6 +3,7 @@ define([ ], function( validateParameterTypeMessageVariables ) { return function( formatter ) { + formatter = formatter.apply( null, [].slice.call( arguments, 1 ) ); return function messageFormatter( variables ) { if ( typeof variables === "number" || typeof variables === "string" ) { variables = [].slice.call( arguments, 0 ); diff --git a/src/message/formatter-runtime-bind.js b/src/message/formatter-runtime-bind.js index 57fac327c..ff57f02d0 100644 --- a/src/message/formatter-runtime-bind.js +++ b/src/message/formatter-runtime-bind.js @@ -1,46 +1,35 @@ define(function() { -return function( cldr, messageformatter, runtime, pluralType, locale, embeddedFormatters ) { - var origToString = messageformatter.toString; +return function( messageformatter, formatterSrc, runtime, pluralType, locale, formatters ) { + var hasFormatters = formatters.length > 0; messageformatter.toString = function() { - var argNames, argValues, output, - args = {}; - - // Properly adjust SlexAxton/messageformat.js compiled variables with Globalize variables: - output = origToString.call( messageformatter ); + var locals = []; + var argCount = 0, + args = []; if ( runtime.number ) { - args.number = "messageFormat.number"; + locals.push( "var number = messageFormat.number;" ); } if ( runtime.plural ) { - args.plural = "messageFormat.plural"; + locals.push( "var plural = messageFormat.plural;" ); } if ( runtime.select ) { - args.select = "messageFormat.select"; - } - - if ( embeddedFormatters.length ) { - args.fmt = "[" + embeddedFormatters.map( function( fn ) { - return fn.generatorString(); - } ).join( ", " ) + "]"; + locals.push( "var select = messageFormat.select;" ); } if ( pluralType !== false ) { - args[locale] = "Globalize(\"" + locale + "\")." + - "pluralGenerator( { type: \"" + pluralType + "\" } )"; + args.push( locale ); + argCount++; } - argNames = Object.keys( args ).join( ", " ); - argValues = Object.keys( args ).map(function( key ) { - return args[ key ]; - }).join( ", " ); - - return "(function( " + argNames + " ) {\n" + - " return " + output + "\n" + - "})(" + argValues + ")"; + return "(function( " + args.join( ", " ) + " ) {\n" + + ( locals.length ? ( locals.join( "\n" ) + "\n" ) : "" ) + + ( hasFormatters ? " var fmt = [].slice.call( arguments, " + argCount + " );\n" : "" ) + + " return " + formatterSrc + "\n" + + "})"; }; return messageformatter; From c4f82d551ac0448bd9f2013525c07ba355a84adf Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Wed, 23 Nov 2016 12:31:50 +0100 Subject: [PATCH 3/6] Message: make messageFormatterFn backwards compatible The new version adds a string "call" parameter after the formatter function. Old bundles will not have this parameter, so messageFormatterFn will use the old behavior. --- src/message.js | 5 +++-- src/message/formatter-fn.js | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/message.js b/src/message.js index cdf8bfe37..0b0b86c04 100644 --- a/src/message.js +++ b/src/message.js @@ -115,14 +115,15 @@ Globalize.prototype.messageFormatter = function( path ) { ); returnFn = messageFormatterFn.apply( this, [ - formatter, + formatter, "call", runtime.number, runtime.plural, runtime.select, pluralGenerator ].concat( compiler.formatters ) ); var runtimeArgs = [ messageFormatterRuntimeBind( formatter, formatterSrc, compiler.runtime, pluralType, cldr.locale, compiler.formatters - ) + ), + "call" ]; if ( pluralGenerator ) { diff --git a/src/message/formatter-fn.js b/src/message/formatter-fn.js index 39e837058..a0205a258 100644 --- a/src/message/formatter-fn.js +++ b/src/message/formatter-fn.js @@ -2,8 +2,10 @@ define([ "../common/validate/parameter-type/message-variables" ], function( validateParameterTypeMessageVariables ) { -return function( formatter ) { - formatter = formatter.apply( null, [].slice.call( arguments, 1 ) ); +return function( formatter, shouldCall ) { + if ( shouldCall === "call" ) { + formatter = formatter.apply( null, [].slice.call( arguments, 2 ) ); + } return function messageFormatter( variables ) { if ( typeof variables === "number" || typeof variables === "string" ) { variables = [].slice.call( arguments, 0 ); From dd26eba6083874ae3ff8e425d46f8ecf7363cba6 Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Thu, 5 Jan 2017 18:12:35 +0100 Subject: [PATCH 4/6] Bower.json: remove messageformat.js No longer needed, since we use a forked version. --- bower.json | 1 - 1 file changed, 1 deletion(-) diff --git a/bower.json b/bower.json index 524c7b592..ffe347670 100644 --- a/bower.json +++ b/bower.json @@ -14,7 +14,6 @@ "cldr-data": ">=25", "es5-shim": "3.4.0", "make-plural": "eemeli/make-plural.js#3.0.0", - "messageformat": "SlexAxton/messageformat.js#v0.3.0-1", "qunit": "1.18.0", "requirejs": "2.1.20", "requirejs-plugins": "1.0.2", From d625e6b40d8801081c723c122203e64007c9e86e Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Wed, 7 Jun 2017 13:36:04 +0200 Subject: [PATCH 5/6] Message: Add tests for formatters. --- test/compiler/cases/message.js | 213 ++++++++++++++++++- test/functional/message/message-formatter.js | 121 ++++++++++- 2 files changed, 328 insertions(+), 6 deletions(-) diff --git a/test/compiler/cases/message.js b/test/compiler/cases/message.js index 16e81e883..5cae56c15 100644 --- a/test/compiler/cases/message.js +++ b/test/compiler/cases/message.js @@ -4,13 +4,85 @@ module.exports = { Globalize.load( // core - require( "../../../external/cldr-data/supplemental/likelySubtags.json" ) + require( "../../../external/cldr-data/supplemental/likelySubtags.json" ), + // date + require( "../../../external/cldr-data/main/en/ca-gregorian.json" ), + require( "../../../external/cldr-data/main/en/timeZoneNames.json" ), + require( "../../../external/cldr-data/supplemental/metaZones.json" ), + require( "../../../external/cldr-data/supplemental/timeData.json" ), + require( "../../../external/cldr-data/supplemental/weekData.json" ), + // number + require( "../../../external/cldr-data/main/en/numbers.json" ), + require( "../../../external/cldr-data/supplemental/numberingSystems.json" ), + // currency + require( "../../../external/cldr-data/main/en/currencies.json" ), + require( "../../../external/cldr-data/supplemental/currencyData.json" ), + // plural + require( "../../../external/cldr-data/supplemental/plurals.json" ), + require( "../../../external/cldr-data/supplemental/ordinals.json" ), + // relative time + require( "../../../external/cldr-data/main/en/dateFields.json" ), + // unit + require( "../../../external/cldr-data/main/en/units.json" ) ); Globalize.loadMessages({ en: { greetings: { - hello: "Hello, {name}" + hello: "Hello", + helloArray: "Hello, {0}", + helloArray2: "Hello, {0} and {1}", + helloName: "Hello, {name}" + }, + like: [ + "{count, plural, offset:1", + " =0 {Be the first to like this}", + " =1 {You liked this}", + " one {You and {someone} liked this}", + " other {You and # others liked this}", + "}" + ], + party: [ + "{hostGender, select,", + " female {{host} invites {guest} to her party}", + " male {{host} invites {guest} to his party}", + " other {{host} invites {guest} to their party}", + "}" + ], + task: [ + "You have {0, plural,", + " one {one task}", + " other {# tasks}", + "} remaining" + ], + ordinal: [ + "{cat, selectordinal, one{#st} two{#nd} few{#rd} other{#th} }", + "category" + ], + date: { + date: "date: {x, date, long}", + time: "time: {x, time, long}", + datetime: "datetime: {x, datetime, long}" + }, + relativetime: { + default: "relativetime: {x, relativetime, minute}", + short: "relativetime short: {x, relativetime, minute, short}", + narrow: "relativetime narrow: {x, relativetime, minute, narrow}" + }, + number: { + decimal: "number decimal: {x, number}", + percent: "number percent: {x, number, percent}" + }, + currency: { + symbol: "currency symbol: {x, currency, USD}", + accounting: "currency accounting: {x, currency, USD, accounting}", + code: "currency code: {x, currency, USD, code}", + name: "currency name: {x, currency, USD, name}" + }, + unit: { + long: "unit long: {x, unit, second, long}", + short: "unit short: {x, unit, second, short}", + narrow: "unit narrow: {x, unit, second, narrow}" } } }); @@ -18,11 +90,142 @@ module.exports = { return Globalize; }, cases: function( Globalize ) { + var date = new Date( 2010, 8, 15, 17, 35, 7, 369 ); + Globalize.locale( "en" ); return [ - { formatter: Globalize( "en" ).messageFormatter( "greetings/hello" ), args: [ { - name: "Beethoven" - } ] } + { + formatter: Globalize( "en" ).messageFormatter( "greetings/hello" ), + args: [] + }, + { + formatter: Globalize( "en" ).messageFormatter( "greetings/helloArray" ), + args: [ "Beethoven" ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "greetings/helloArray2" ), + args: [ [ "Beethoven", "Mozart" ] ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "greetings/helloName" ), + args: [ { name: "Beethoven" } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "task" ), + args: [ 123 ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "party" ), + args: [ { + guest: "Mozart", + host: "Beethoven", + hostGender: "male" + } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "like" ), + args: [ { count: 0 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "like" ), + args: [ { count: 1 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "like" ), + args: [ { count: 2, someone: "Beethoven" } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "like" ), + args: [ { count: 3 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "ordinal" ), + args: [ { cat: 1 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "ordinal" ), + args: [ { cat: 2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "ordinal" ), + args: [ { cat: 3 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "ordinal" ), + args: [ { cat: 4 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "date/date" ), + args: [ { x: date } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "date/time" ), + args: [ { x: date } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "date/datetime" ), + args: [ { x: date } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "relativetime/default" ), + args: [ { x: 2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "relativetime/default" ), + args: [ { x: -2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "relativetime/short" ), + args: [ { x: 2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "relativetime/short" ), + args: [ { x: -2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "relativetime/narrow" ), + args: [ { x: 2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "relativetime/narrow" ), + args: [ { x: -2 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "number/decimal" ), + args: [ { x: 0.5 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "number/percent" ), + args: [ { x: 0.5 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "currency/symbol" ), + args: [ { x: 100 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "currency/accounting" ), + args: [ { x: 100 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "currency/code" ), + args: [ { x: 100 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "currency/name" ), + args: [ { x: 100 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "unit/long" ), + args: [ { x: 42 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "unit/short" ), + args: [ { x: 42 } ] + }, + { + formatter: Globalize( "en" ).messageFormatter( "unit/narrow" ), + args: [ { x: 42 } ] + }, ]; } }; diff --git a/test/functional/message/message-formatter.js b/test/functional/message/message-formatter.js index 74fc8f294..26cc7c4c7 100644 --- a/test/functional/message/message-formatter.js +++ b/test/functional/message/message-formatter.js @@ -1,14 +1,43 @@ define([ "globalize", "json!cldr-data/supplemental/likelySubtags.json", + "json!cldr-data/supplemental/numberingSystems.json", + "json!cldr-data/supplemental/currencyData.json", "json!cldr-data/supplemental/plurals.json", "json!cldr-data/supplemental/ordinals.json", + "json!cldr-data/supplemental/timeData.json", + "json!cldr-data/supplemental/weekData.json", + "json!cldr-data/main/en/numbers.json", + "json!cldr-data/main/en/units.json", + "json!cldr-data/main/en/currencies.json", + "json!cldr-data/main/en/ca-gregorian.json", + "json!cldr-data/main/en/timeZoneNames.json", + "json!cldr-data/main/en/dateFields.json", + "json!iana-tz-data.json", "../../util", "cldr/unresolved", "globalize/message", "globalize/plural" -], function( Globalize, likelySubtags, plurals, ordinals, util ) { +], function( Globalize, likelySubtags, numberingSystems, currencyData, plurals, + ordinals, timeData, weekData, enNumbers, enUnitFields, enCurrencies, enCaGregorian, + enTimeZoneNames, enDateFields, ianaTimezoneData, util ) { + +function extraSetup() { + Globalize.load( + numberingSystems, + currencyData, + enNumbers, + enUnitFields, + enCurrencies, + timeData, + weekData, + enCaGregorian, + enTimeZoneNames, + enDateFields + ); + Globalize.loadTimeZone( ianaTimezoneData ); +} QUnit.assert.messageFormatter = function( locale, path, variables, expected ) { if ( arguments.length === 3 ) { @@ -67,6 +96,31 @@ QUnit.module( ".messageFormatter( path )", { "{cat, selectordinal, one{#st} two{#nd} few{#rd} other{#th} }", "category" ], + date: { + date: "date: {x, date, long}", + time: "time: {x, time, long}", + datetime: "datetime: {x, datetime, long}" + }, + relativetime: { + default: "relativetime: {x, relativetime, minute}", + short: "relativetime short: {x, relativetime, minute, short}", + narrow: "relativetime narrow: {x, relativetime, minute, narrow}" + }, + number: { + decimal: "number decimal: {x, number}", + percent: "number percent: {x, number, percent}" + }, + currency: { + symbol: "currency symbol: {x, currency, USD}", + accounting: "currency accounting: {x, currency, USD, accounting}", + code: "currency code: {x, currency, USD, code}", + name: "currency name: {x, currency, USD, name}" + }, + unit: { + long: "unit long: {x, unit, second, long}", + short: "unit short: {x, unit, second, short}", + narrow: "unit narrow: {x, unit, second, narrow}" + } }, "en-GB": {}, fr: {}, @@ -198,6 +252,71 @@ QUnit.test( "should support ICU message format", function( assert ) { }, "4th category" ); }); +QUnit.test( "should support formatters in messages", function( assert ) { + extraSetup(); + + var date = new Date( 2010, 8, 15, 17, 35, 7, 369 ); + + assert.messageFormatter( "en", "date/date", { + x: date, + }, "date: September 15, 2010" ); + assert.messageFormatter( "en", "date/time", { + x: date, + }, "time: 5:35:07 PM GMT+2" ); + assert.messageFormatter( "en", "date/datetime", { + x: date, + }, "datetime: September 15, 2010 at 5:35:07 PM GMT+2" ); + + assert.messageFormatter( "en", "relativetime/default", { + x: 2, + }, "relativetime: in 2 minutes" ); + assert.messageFormatter( "en", "relativetime/default", { + x: -2, + }, "relativetime: 2 minutes ago" ); + assert.messageFormatter( "en", "relativetime/short", { + x: 2, + }, "relativetime short: in 2 min." ); + assert.messageFormatter( "en", "relativetime/short", { + x: -2, + }, "relativetime short: 2 min. ago" ); + assert.messageFormatter( "en", "relativetime/narrow", { + x: 2, + }, "relativetime narrow: in 2 min." ); + assert.messageFormatter( "en", "relativetime/narrow", { + x: -2, + }, "relativetime narrow: 2 min. ago" ); + + assert.messageFormatter( "en", "number/decimal", { + x: 0.5, + }, "number decimal: 0.5" ); + assert.messageFormatter( "en", "number/percent", { + x: 0.5, + }, "number percent: 50%" ); + + assert.messageFormatter( "en", "currency/symbol", { + x: 100, + }, "currency symbol: $100.00" ); + assert.messageFormatter( "en", "currency/accounting", { + x: 100, + }, "currency accounting: $100.00" ); + assert.messageFormatter( "en", "currency/code", { + x: 100, + }, "currency code: 100.00 USD" ); + assert.messageFormatter( "en", "currency/name", { + x: 100, + }, "currency name: 100.00 US dollars" ); + + assert.messageFormatter( "en", "unit/long", { + x: 42, + }, "unit long: 42 seconds" ); + assert.messageFormatter( "en", "unit/short", { + x: 42, + }, "unit short: 42 sec" ); + assert.messageFormatter( "en", "unit/narrow", { + x: 42, + }, "unit narrow: 42s" ); +}); + // Reference #473 QUnit.test( "should NOT merge array data", function( assert ) { // Re-loading a message that uses array syntax. From c9d24603433dae150c9b2dea354088952cfc3f6b Mon Sep 17 00:00:00 2001 From: Nikola Kovacs Date: Mon, 3 Jul 2017 10:24:43 +0200 Subject: [PATCH 6/6] Message: add date raw and skeleton support. --- src/core.js | 7 ++++--- src/date.js | 16 +++++++++++++--- src/message/compiler.js | 4 ++++ src/util/formatterfn/options.js | 19 +++++++++++++++++++ test/compiler/cases/message.js | 6 +++++- test/functional/message/message-formatter.js | 18 +++++++++++++++++- 6 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/util/formatterfn/options.js diff --git a/src/core.js b/src/core.js index 4cbf334fc..255e40e56 100644 --- a/src/core.js +++ b/src/core.js @@ -17,12 +17,13 @@ define([ "./util/object/extend", "./util/regexp/escape", "./util/string/pad", + "./util/formatterfn/options", "cldr/event" ], function( Cldr, createError, formatMessage, runtimeBind, validate, validateCldr, validateDefaultLocale, validateParameterPresence, validateParameterRange, validateParameterType, validateParameterTypeLocale, validateParameterTypePlainObject, alwaysArray, alwaysCldr, - isPlainObject, objectExtend, regexpEscape, stringPad ) { + isPlainObject, objectExtend, regexpEscape, stringPad, formatterfnOptions ) { function validateLikelySubtags( cldr ) { cldr.once( "get", validateCldr ); @@ -88,8 +89,8 @@ Globalize.locale = function( locale ) { Globalize._messageFmts = {}; -Globalize.addMessageFormatterFunction = function( name, fn ) { - Globalize._messageFmts[name] = fn; +Globalize.addMessageFormatterFunction = function( name, fn, options ) { + Globalize._messageFmts[name] = formatterfnOptions( fn, options ); }; /** diff --git a/src/date.js b/src/date.js index 496a27064..659d0cbbb 100644 --- a/src/date.js +++ b/src/date.js @@ -55,12 +55,14 @@ function validateOptionsPreset( options ) { validateOptionsPresetEach( "datetime", options ); } +var presets = [ "short", "medium", "long", "full" ]; + function validateOptionsPresetEach( type, options ) { var value = options[ type ]; validate( "E_INVALID_OPTIONS", "Invalid `{{type}: \"{value}\"}`.", - value === undefined || [ "short", "medium", "long", "full" ].indexOf( value ) !== -1, + value === undefined || presets.indexOf( value ) !== -1, { type: type, value: value } ); } @@ -214,10 +216,18 @@ Globalize.prototype.dateToPartsFormatter = function( options ) { Globalize.addMessageFormatterFunction( type, function( p ) { var options = {}; if ( p ) { - options[type] = p; + var trimmed = p.trim(); + if ( presets.indexOf( trimmed ) !== -1 ) { + options[type] = trimmed; + } else if ( trimmed.indexOf( "skeleton" ) === 0 && trimmed.indexOf( "," ) !== -1 ) { + var splitArgs = p.split( ",", 2 ); + options.skeleton = splitArgs[1].trim(); + } else { + options.raw = p; + } } return this.dateFormatter( options ); - }); + }, { split: false, trim: false }); }); /** diff --git a/src/message/compiler.js b/src/message/compiler.js index 20293596d..d53527aa9 100644 --- a/src/message/compiler.js +++ b/src/message/compiler.js @@ -198,6 +198,10 @@ Compiler.prototype.compile = function( src, lc ) { // TODO: pc is only needed for validation, disable for now. var pc = { cardinal: [], ordinal: [] }; + + // Enable icu-compatible function parameter parsing so that commas + // can be used inside custom date formats. + pc.strictFunctionParams = true; var r = Parser.parse( src, pc ).map( function( token ) { return this.token( token ); }, this ); return "function(d) { return " + ( r.join( " + " ) || "\"\"" ) + "; }"; }; diff --git a/src/util/formatterfn/options.js b/src/util/formatterfn/options.js new file mode 100644 index 000000000..965648258 --- /dev/null +++ b/src/util/formatterfn/options.js @@ -0,0 +1,19 @@ +define(function() { + +return function( fn, options ) { + options = options || {}; + return function( ) { + var args = [].slice.call( arguments, 0 ); + if ( args.length === 1 && ( options.split === undefined || options.split === true ) ) { + args = args[0].split( "," ); + } + if ( options.trim === undefined || options.trim === true ) { + args = args.map( function( v ) { + return v.trim(); + }); + } + return fn.apply( this, args ); + }; +}; + +}); diff --git a/test/compiler/cases/message.js b/test/compiler/cases/message.js index 5cae56c15..2fe2e8d93 100644 --- a/test/compiler/cases/message.js +++ b/test/compiler/cases/message.js @@ -62,7 +62,11 @@ module.exports = { date: { date: "date: {x, date, long}", time: "time: {x, time, long}", - datetime: "datetime: {x, datetime, long}" + datetime: "datetime: {x, datetime, long}", + raw: "date raw: {x, date, y-M-d HH:mm:ss zzzz }", + rawComma: "date raw comma: {x, date, y-M-d, HH:mm:ss zzzz }", + skeleton: "date skeleton: {x, date, skeleton, GyMMMEdhms}", + skeletonInvalid: "date skeleton: {x, date, skeleton}" }, relativetime: { default: "relativetime: {x, relativetime, minute}", diff --git a/test/functional/message/message-formatter.js b/test/functional/message/message-formatter.js index 26cc7c4c7..b2757a12e 100644 --- a/test/functional/message/message-formatter.js +++ b/test/functional/message/message-formatter.js @@ -99,7 +99,11 @@ QUnit.module( ".messageFormatter( path )", { date: { date: "date: {x, date, long}", time: "time: {x, time, long}", - datetime: "datetime: {x, datetime, long}" + datetime: "datetime: {x, datetime, long}", + raw: "date raw: {x, date, y-M-d HH:mm:ss zzzz }", + rawComma: "date raw comma: {x, date, y-M-d, HH:mm:ss zzzz }", + skeleton: "date skeleton: {x, date, skeleton, GyMMMEdhms}", + skeletonInvalid: "date skeleton: {x, date, skeleton}" }, relativetime: { default: "relativetime: {x, relativetime, minute}", @@ -266,6 +270,18 @@ QUnit.test( "should support formatters in messages", function( assert ) { assert.messageFormatter( "en", "date/datetime", { x: date, }, "datetime: September 15, 2010 at 5:35:07 PM GMT+2" ); + assert.messageFormatter( "en", "date/raw", { + x: date, + }, "date raw: 2010-9-15 17:35:07 GMT+02:00 " ); + assert.messageFormatter( "en", "date/rawComma", { + x: date, + }, "date raw comma: 2010-9-15, 17:35:07 GMT+02:00 " ); + assert.messageFormatter( "en", "date/skeleton", { + x: date, + }, "date skeleton: Wed, Sep 15, 2010 AD, 5:35:07 PM" ); + assert.messageFormatter( "en", "date/skeletonInvalid", { + x: date, + }, "date skeleton: 7174l4ton" ); assert.messageFormatter( "en", "relativetime/default", { x: 2,