diff --git a/Gruntfile.js b/Gruntfile.js index 23380afa4..9a54e3e3f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -106,7 +106,8 @@ module.exports = function( grunt ) { optimize: "none", paths: { cldr: "../external/cldrjs/dist/cldr", - "make-plural": "../external/make-plural/make-plural" + "make-plural": "../external/make-plural/make-plural", + messageformat: "../external/messageformat/messageformat" }, skipSemiColonInsertion: true, skipModuleInsertion: true, @@ -121,16 +122,8 @@ module.exports = function( grunt ) { onBuildWrite: function( id, path, contents ) { var name = camelCase( id.replace( /util\/|common\//, "" ) ); - // CLDRPluralRuleParser - if ( (/CLDRPluralRuleParser/).test( id ) ) { - return contents - - // Replace UMD wrapper into var assignment. - .replace( /\(function\(root, factory\)[\s\S]*?}\(this, function\(\) {/, "var CLDRPluralRuleParser = (function() {" ) - .replace( /}\)\);\s+$/, "}());" ); - // MakePlural - } else if ( (/make-plural/).test( id ) ) { + if ( (/make-plural/).test( id ) ) { return contents // Replace its wrapper into var assignment. @@ -156,6 +149,22 @@ module.exports = function( grunt ) { // Remove MakePlural.load = function(.*) {...return MakePlural;.*}; .replace( /MakePlural.load = function\([\s\S]*?return MakePlural;\n};/, "" ); + + // messageformat + } else if ( (/messageformat/).test( id ) ) { + return contents + + // Replace its wrapper into var assignment. + .replace( /\(function \( root \) {/, [ + "var MessageFormat;", + "/* jshint ignore:start */", + "MessageFormat = (function() {" + ].join( "\n" ) ) + .replace( /if \(typeof exports !== 'undefined'[\s\S]*/, [ + "return MessageFormat;", + "}());", + "/* jshint ignore:end */" + ].join( "\n" ) ); } // 1, and 2: Remove define() wrap. @@ -358,7 +367,7 @@ module.exports = function( grunt ) { "jscs:source", // TODO fix issues, enable - // "jscs:test", + //"jscs:test", "test:unit", "clean", "requirejs", diff --git a/README.md b/README.md index 302409276..a46ae85d4 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ information on its usage. | globalize.js | 1.3KB | [Core library](#core) | | globalize/currency.js | +2.6KB | [Currency module](#currency_module) provides currency formatting and parsing | | globalize/date.js | +4.8KB | [Date module](#date_module) provides date formatting and parsing | -| globalize/message.js | +0.5KB | [Message module](#message_module) provides message translation | +| globalize/message.js | +5.5KB | [Message module](#message_module) provides ICU message format support | | globalize/number.js | +2.9KB | [Number module](#number_module) provides number formatting and parsing | | globalize/plural.js | +1.7KB | [Plural module](#plural_module) provides pluralization support | @@ -273,17 +273,23 @@ to you in different flavors): ### Message module -- **`Globalize.loadTranslations( json )`** +- **`Globalize.loadMessages( json )`** - Load translation data. + Load messages data. - [Read more...](doc/api/message/load-translation.md) + [Read more...](doc/api/message/load-messages.md) -- **`.translate( path )`** +- **`.messageFormatter( path ) ➡ function([ variables ])`** - Translate item given its path. + Return a function that formats a message (using ICU message format pattern) + given its path and a set of variables into a user-readable string. It supports + pluralization and gender inflections. - [Read more...](doc/api/message/translate.md) + [Read more...](doc/api/message/message-formatter.md) + +- **`.formatMessage( path [, variables ] )`** + + Alias to `.messageFormatter( path )([ variables ])`. ### Number module diff --git a/bower.json b/bower.json index 6df61372d..102a6f61b 100644 --- a/bower.json +++ b/bower.json @@ -15,6 +15,7 @@ "devDependencies": { "make-plural": "eemeli/make-plural.js#2.1.2", "es5-shim": "3.4.0", + "messageformat": "SlexAxton/messageformat.js#debeaf4", "qunit": "1.12.0", "requirejs": "2.1.9", "requirejs-plugins": "1.0.2", diff --git a/doc/api/message/load-messages.md b/doc/api/message/load-messages.md new file mode 100644 index 000000000..2be58174f --- /dev/null +++ b/doc/api/message/load-messages.md @@ -0,0 +1,102 @@ +## .loadMessages( json ) + +Load messages data. + +The first level of keys must be locales. For example: + +``` +{ + en: { + hello: "Hello" + }, + pt: { + hello: "Olá" + } +} +``` + +ICU MessageFormat pattern is supported: variable replacement, gender and plural +inflections. For more information see [`.messageFormatter( path ) ➡ function([ +variables ])`](./message-formatter.md). + +The provided messages are stored along side other cldr data, under the +"globalize-messages" key. This allows Globalize to reuse the traversal methods +provided by cldrjs. You can inspect this data using +`cldrjs.get("globalize-messages")`. + +### Parameters + +**json** + +JSON object of messages data. Keys can use any character, except `/`, `{` and +`}`. Values (i.e., the message content itself) can contain any character. + +### Example + +```javascript +Globalize.loadMessages({ + pt: { + greetings: { + hello: "Olá", + bye: "Tchau" + } + } +}); + +Globalize( "pt" ).formatMessage( "greetings/hello" ); +➡ Olá +``` + +#### Multiline strings + +Use Arrays as a convenience for multiline strings. The lines will be joined by a +space. + +```javascript +Globalize.loadMessages({ + en: { + longText: [ + "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eligendi non", + "quis exercitationem culpa nesciunt nihil aut nostrum explicabo", + "reprehenderit optio amet ab temporibus asperiores quasi cupiditate.", + "Voluptatum ducimus voluptates voluptas?" + ] + } +}); + +Globalize( "en" ).formatMessage( "longText" ); +➡ "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eligendi non quis exercitationem culpa nesciunt nihil aut nostrum explicabo reprehenderit optio amet ab temporibus asperiores quasi cupiditate. Voluptatum ducimus voluptates voluptas?" +``` + +#### Messages inheritance + +It's possible to inherit messages, for example: + +```javascript +Globalize.loadMessages({ + root: { + amen: "Amen" + }, + pt: { + amen: "Amém" + } +}); + +Globalize( "pt-PT" ).formatMessage( "amen" ); // "Amém" +Globalize( "de" ).formatMessage( "amen" ); // "Amen" +Globalize( "en" ).formatMessage( "amen" ); // "Amen" +Globalize( "en-GB" ).formatMessage( "amen" ); // "Amen" +Globalize( "fr" ).formatMessage( "amen" ); // "Amen" +``` + +Note that `pt-PT`, `de`, `en`, `en-GB`, and `fr` have never been defined. +`.formatMessage()` inherits `pt-PT` messages from `pt` (`pt-PT` ➡ `pt`), and +it inherits the other messages from root, eg. `en-GB` ➡ `en` ➡ `root`. Yes, +`root` is the last bundle of the parent lookup. + +Attention: On browsers, message inheritance only works if the optional +dependency `cldr/unresolved` is loaded. + +```html + +``` diff --git a/doc/api/message/load-translation.md b/doc/api/message/load-translation.md deleted file mode 100644 index ca2c84f7e..000000000 --- a/doc/api/message/load-translation.md +++ /dev/null @@ -1,68 +0,0 @@ -## .loadTranslations( json ) - -Load translation data. - -The first dregree keys must be locales. For example: - -``` -{ - en: { - hello: "Hello" - }, - pt: { - hello: "Olá" - } -} -``` - -### Parameters - -**json** - -JSON object of translation data. - -### Example - -```javascript -Globalize.loadTranslations({ - pt: { - greetings: { - hello: "Olá", - bye: "Tchau" - } - } -}); - -Globalize( "pt" ).translate( "greetings/hello" ); // Olá -``` - -It's possible to inherit translations, for example: - -```javascript -Globalize.loadTranslations({ - root: { - amen: "Amen" - }, - pt: { - amen: "Amém" - } -}); - -Globalize( "pt-PT" ).translate( "amen" ); // "Amém" -Globalize( "de" ).translate( "amen" ); // "Amen" -Globalize( "en" ).translate( "amen" ); // "Amen" -Globalize( "en-GB" ).translate( "amen" ); // "Amen" -Globalize( "fr" ).translate( "amen" ); // "Amen" -``` - -Note that `pt-PT`, `de`, `en`, `en-GB`, and `fr` have never been defined. -`.translate()` inherits `pt-PT` translation from `pt` (`pt-PT` ➡ `pt`), and it -inherits the other translations from root, eg. `en-GB` ➡ `en` ➡ `root`. Yes, -`root` is the last bundle of the parent lookup. - -Attention: On browsers, translation inheritance only works if the optional -dependency `cldr/unresolved` is loaded. - -```html - -``` diff --git a/doc/api/message/message-formatter.md b/doc/api/message/message-formatter.md new file mode 100644 index 000000000..420cd11fb --- /dev/null +++ b/doc/api/message/message-formatter.md @@ -0,0 +1,216 @@ +## .messageFormatter( path ) ➡ function([ variables ]) + +Return a function that formats a message (using ICU message format pattern) +given its path and a set of variables into a user-readable string. It supports +pluralization and gender inflections. + +Use [`Globalize.loadMessages( json )`](./load-messages.md) to load +messages data. + +### Parameters + +**path** + +String or Array containing the path of the message content, eg. +`"greetings/bye"`, or `[ "greetings", "bye" ]`. + +**variables** (optional) + +Variables can be Objects, where each property can be referenced by name inside a +message; or Arrays, where each entry of the Array can be used inside a message, +using numeric indices. When passing one or more arguments of other types, +they're converted to an Array and used as such. + +### Example + +You can use the static method `Globalize.messageFormatter()`, which uses the default +locale. + +```javascript +var formatter; + +Globalize.loadMessages({ + pt: { + greetings: { + bye: "Tchau" + } + } +}); + +Globalize.locale( "pt" ); +formatter = Globalize.messageFormatter( "greetings/bye" ); + +formatter(); +➡ "Tchau" +``` + +You can use the instance method `.messageFormatter()`, which uses the instance locale. + +```javascript +var pt = new Globalize( "pt" ), + formatter = pt.messageFormatter( "greetings/bye" ); + +formatter(); +➡ "Tchau" +``` + +Simple Variable Replacement. + +```javascript +var formatter; + +Globalize.loadMessages({ + en: { + hello: "Hello, {0} {1} {2}", + hey: "Hey, {first} {middle} {last}" + } +}); + +formatter = Globalize( "en" ).messageFormatter( "hello" ); + +// Numbered variables using Array. +formatter([ "Wolfgang", "Amadeus", "Mozart" ]); +➡ "Hello, Wolfgang Amadeus Mozart" + +// Numbered variables using function arguments. +formatter( "Wolfgang", "Amadeus", "Mozart" ); +➡ "Hello, Wolfgang Amadeus Mozart" + +// Named variables using Object key-value pairs. +formatter = Globalize( "en" ).messageFormatter( "hey" ); +formatter({ + first: "Wolfgang", + middle: "Amadeus", + last: "Mozart" +}); +➡ "Hey, Wolfgang Amadeus Mozart" +``` + +Gender inflections. Note `select` can be used to format any message variations +that works like a switch. + +```javascript +var formatter; + +// Note you can define multiple lines message using an Array of Strings. +Globalize.loadMessages({ + en: { + party: [ + "{hostGender, select,", + " female {{host} invites {guest} to her party}", + " male {{host} invites {guest} to his party}", + " other {{host} invites {guest} to their party}", + "}" + ] + } +}); + +formatter = Globalize( "en" ).messageFormatter( "party" ); + +formatter({ + guest: "Mozart", + host: "Beethoven", + hostGender: "male" +}); +➡ "Beethoven invites Mozart to his party" +``` + +Plural inflections. It uses the plural forms `zero`, `one`, `two`, `few`, +`many`, or `other` (required). English only uses `one` and `other`. So, +including `zero` will never get called, even when the number is 0. For more +information see [`.pluralGenerator()`](../plural/plural-generator.md). + +```javascript +var numberFormatter, taskFormatter, + en = new Globalize( "en" ); + +// Note you can define multiple lines message using an Array of Strings. +Globalize.loadMessages({ + en: { + task: [ + "You have {count, plural,", + " one {one task}", + " other {{formattedCount} tasks}", + "} remaining" + ] + } +}); + +numberFormatter = en.numberFormatter(); +taskFormatter = en.messageFormatter( "task" ); + +taskFormatter({ + count: 1000, + formattedCount: numberFormatter( 1000 ) +}); +➡ "You have 1,000 tasks remaining" +``` + +Literal numeric keys can be used in `plural` to match single, specific numbers. + +```javascript +var taskFormatter, + en = new Globalize( "en" ); + +// Note you can define multiple lines message using an Array of Strings. +Globalize.loadMessages({ + en: { + task: [ + "You have {count, plural,", + " =0 {no tasks}", + " one {one task}", + " other {{formattedCount} tasks}", + "} remaining" + ] + } +}); + +taskFormatter = Globalize( "en" ).messageFormatter( "task" ); + +taskFormatter({ + count: 0, + formattedCount: en.numberFormatter( 0 ) +}); +➡ "You have no tasks remaining" +``` + +You may find useful having the plural forms calculated with an offset applied. +Use `#` to output the resulting number. Note literal numeric keys do NOT use the +offset value. + +```javascript +var likeFormatter, + en = new Globalize( "en" ); + +Globalize.loadMessages({ + en: { + like: [ + "{0, plural, offset:1", + " =0 {Be the first to like this}", + " =1 {You liked this}", + " one {You and someone else liked this}", + " other {You and # others liked this}", + "}" + ] + } +}); + +likeFormatter = Globalize( "en" ).messageFormatter( "like" ); + +likeFormatter( 0 ); +➡ "Be the first to like this" + +likeFormatter( 1 ); +➡ "You liked this" + +likeFormatter( 2 ); +➡ "You and someone else liked this" + +likeFormatter( 3 ); +➡ "You and 2 others liked this" +``` + +Read on [SlexAxton/messageFormatter.js][] for more information on regard of ICU +MessageFormat. + +[SlexAxton/messageFormatter.js]: https://github.com/SlexAxton/messageformat.js/#no-frills diff --git a/doc/api/message/translate.md b/doc/api/message/translate.md deleted file mode 100644 index 416d65a2b..000000000 --- a/doc/api/message/translate.md +++ /dev/null @@ -1,37 +0,0 @@ -## .translate( path ) - -Translate item given its path. - -For translation inheritance, see the [Example section of -.loadTranslations()](./load-translation.md#example). - -### Parameters -**path** - -String or Array containing the translation item path, eg. `"greetings/bye"`, or -`[ "greetings", "bye" ]`. - -### Example - -You can use the static method `Globalize.translate()`, which uses the default -locale. - -```javascript -Globalize.loadTranslations({ - greetings: { - bye: "Tchau" - } -}); - -Globalize.locale( "pt-BR" ); -Globalize.translate( "greetings/bye" ); -// ➡ "Tchau" -``` - -You can use the instance method `.translate()`, which uses the instance locale. - -```javascript -var ptBr = new Globalize( "pt-BR" ); -ptBr.translate( "greetings/bye" ); -// ➡ "Tchau" -``` diff --git a/examples/amd-bower/index.html b/examples/amd-bower/index.html index 77a174e64..31f322edb 100644 --- a/examples/amd-bower/index.html +++ b/examples/amd-bower/index.html @@ -22,12 +22,14 @@

Demo output

Now:

A number:

A currency:

-

Plurals:

+

Plural form of is

+

Messages:

+
  • +
  • +
  • +
  • + + @@ -177,9 +180,23 @@

    Demo output

    } } }); + Globalize.loadMessages({ + "en": { + "like": [ + "{0, plural, offset:1", + " =0 {Be the first to like this}", + " =1 {You liked this}", + " one {You and someone else liked this}", + " other {You and # others liked this}", + "}" + ] + } + }); + + var en, like, number; // Instantiate "en". - var en = Globalize( "en" ); + en = Globalize( "en" ); // Use Globalize to format dates. document.getElementById( "date" ).innerHTML = en.formatDate( new Date(), { @@ -187,15 +204,22 @@

    Demo output

    }); // Use Globalize to format numbers. - document.getElementById( "number" ).innerHTML = en.formatNumber( 12345.6789 ); + number = en.numberFormatter(); + document.getElementById( "number" ).innerHTML = number( 12345.6789 ); // Use Globalize to format currencies. document.getElementById( "currency" ).innerHTML = en.formatCurrency( 69900, "USD" ); // Use Globalize to get the plural form of a numeric value. - document.getElementById( "plural-0" ).innerHTML = en.plural( 0 ); - document.getElementById( "plural-1" ).innerHTML = en.plural( 1 ); - document.getElementById( "plural-2" ).innerHTML = en.plural( 2 ); + document.getElementById( "plural-number" ).innerHTML = number( 12345.6789 ) + document.getElementById( "plural-form" ).innerHTML = en.plural( 12345.6789 ); + + // Use Globalize to format a message with plural inflection. + like = en.messageFormatter( "like" ); + document.getElementById( "message-0" ).innerHTML = like( 0 ); + document.getElementById( "message-1" ).innerHTML = like( 1 ); + document.getElementById( "message-2" ).innerHTML = like( 2 ); + document.getElementById( "message-3" ).innerHTML = like( 3 ); document.getElementById( "requirements" ).style.display = "none"; document.getElementById( "demo" ).style.display = "block"; diff --git a/src/build/intro-message.js b/src/build/intro-message.js index fea296133..65f72261b 100644 --- a/src/build/intro-message.js +++ b/src/build/intro-message.js @@ -35,6 +35,8 @@ }(this, function( Cldr, Globalize ) { var alwaysArray = Globalize._alwaysArray, + isPlainObject = Globalize._isPlainObject, + validate = Globalize._validate, validateDefaultLocale = Globalize._validateDefaultLocale, validateParameterPresence = Globalize._validateParameterPresence, validateParameterType = Globalize._validateParameterType, diff --git a/src/common/validate/message-presence.js b/src/common/validate/message-presence.js new file mode 100644 index 000000000..5f7fd9395 --- /dev/null +++ b/src/common/validate/message-presence.js @@ -0,0 +1,11 @@ +define([ + "../validate" +], function( validate ) { + +return function( path, value ) { + path = path.join( "/" ); + validate( "E_MISSING_MESSAGE", "Missing required message content `{path}`.", + value !== undefined, { path: path } ); +}; + +}); diff --git a/src/common/validate/message-type.js b/src/common/validate/message-type.js new file mode 100644 index 000000000..185d2b4cf --- /dev/null +++ b/src/common/validate/message-type.js @@ -0,0 +1,18 @@ +define([ + "../validate" +], function( validate ) { + +return function( path, value ) { + path = path.join( "/" ); + validate( + "E_INVALID_MESSAGE", + "Invalid message content `{path}`. {expected} expected.", + typeof value === "string", + { + expected: "a string", + path: path + } + ); +}; + +}); diff --git a/src/common/validate/parameter-type/message-variables.js b/src/common/validate/parameter-type/message-variables.js new file mode 100644 index 000000000..a0f0d5d09 --- /dev/null +++ b/src/common/validate/parameter-type/message-variables.js @@ -0,0 +1,15 @@ +define([ + "../parameter-type", + "../../../util/is-plain-object" +], function( validateParameterType, isPlainObject ) { + +return function( value, name ) { + validateParameterType( + value, + name, + value === undefined || isPlainObject( value ) || Array.isArray( value ), + "Array or Plain Object" + ); +}; + +}); diff --git a/src/message.js b/src/message.js index 8ea42a577..e9853e369 100644 --- a/src/message.js +++ b/src/message.js @@ -1,24 +1,45 @@ define([ "cldr", + "messageformat", "./core", + "./common/create-error", "./common/validate/default-locale", + "./common/validate/message-presence", + "./common/validate/message-type", "./common/validate/parameter-presence", "./common/validate/parameter-type", + "./common/validate/parameter-type/message-variables", "./common/validate/parameter-type/plain-object", + "./common/validate/plural-module-presence", "./util/always-array" -], function( Cldr, Globalize, validateDefaultLocale, validateParameterPresence, - validateParameterType, validateParameterTypePlainObject, alwaysArray ) { +], function( Cldr, MessageFormat, Globalize, createError, validateDefaultLocale, + validateMessagePresence, validateMessageType, validateParameterPresence, validateParameterType, + validateParameterTypeMessageVariables, validateParameterTypePlainObject, + validatePluralModulePresence, alwaysArray ) { + +var slice = [].slice; + +function MessageFormatInit( globalize, cldr ) { + var plural; + return new MessageFormat( cldr.locale, function( value ) { + if ( !plural ) { + validatePluralModulePresence(); + plural = globalize.pluralGenerator(); + } + return plural( value ); + }); +} /** - * .loadTranslations( json ) + * .loadMessages( json ) * * @json [JSON] * * Load translation data. */ -Globalize.loadTranslations = function( json ) { +Globalize.loadMessages = function( json ) { var customData = { - "globalize-translations": json + "globalize-messages": json }; validateParameterPresence( json, "json" ); @@ -28,15 +49,15 @@ Globalize.loadTranslations = function( json ) { }; /** - * .translate( path ) + * .messageFormatter( path ) * * @path [String or Array] * - * Translate item given its path. + * Format a message given its path. */ -Globalize.translate = -Globalize.prototype.translate = function( path ) { - var cldr; +Globalize.messageFormatter = +Globalize.prototype.messageFormatter = function( path ) { + var cldr, formatter, message; validateParameterPresence( path, "path" ); validateParameterType( path, "path", typeof path === "string" || Array.isArray( path ), @@ -47,7 +68,38 @@ Globalize.prototype.translate = function( path ) { validateDefaultLocale( cldr ); - return cldr.get( [ "globalize-translations/{languageId}" ].concat( path ) ); + message = cldr.get( [ "globalize-messages/{languageId}" ].concat( path ) ); + validateMessagePresence( path, message ); + + // If message is an Array, concatenate it. + if ( Array.isArray( message ) ) { + message = message.join( " " ); + } + validateMessageType( path, message ); + + formatter = MessageFormatInit( this, cldr ).compile( message ); + + return function( variables ) { + if ( typeof variables === "number" || typeof variables === "string" ) { + variables = slice.call( arguments, 0 ); + } + validateParameterTypeMessageVariables( variables, "variables" ); + return formatter( variables ); + }; +}; + +/** + * .formatMessage( path [, variables] ) + * + * @path [String or Array] + * + * @variables [Number, String, Array or Object] + * + * Format a message given its path. + */ +Globalize.formatMessage = +Globalize.prototype.formatMessage = function( path ) { + return this.messageFormatter( path ).apply( {}, slice.call( arguments, 1 ) ); }; return Globalize; diff --git a/test/functional.js b/test/functional.js index 84f060f13..893ed9d94 100644 --- a/test/functional.js +++ b/test/functional.js @@ -25,7 +25,8 @@ require([ "./functional/date/parse-date", // message - "./functional/message/translate", + "./functional/message/message-formatter", + "./functional/message/format-message", // number "./functional/number/number-formatter", diff --git a/test/functional/message/format-message.js b/test/functional/message/format-message.js new file mode 100644 index 000000000..065fd3b40 --- /dev/null +++ b/test/functional/message/format-message.js @@ -0,0 +1,51 @@ +define([ + "globalize", + "json!cldr-data/supplemental/likelySubtags.json", + "json!cldr-data/supplemental/plurals.json", + "../../util", + + "cldr/event", + "globalize/message", + "globalize/plural" +], function( Globalize, likelySubtags, plurals, util ) { + +QUnit.module( ".formatMessage( path [, variables] )", { + setup: function() { + Globalize.load( likelySubtags ); + Globalize.load( plurals ); + Globalize.loadMessages({ + en: { + greetings: { + hello: "Hello, {name}" + } + } + }); + }, + teardown: util.resetCldrContent +}); + +QUnit.test( "should validate parameters", function( assert ) { + util.assertParameterPresence( assert, "path", function() { + Globalize( "en" ).formatMessage(); + }); + + util.assertPathParameter( assert, "path", function( invalidValue ) { + return function() { + Globalize( "en" ).formatMessage( invalidValue ); + }; + }); + + util.assertMessageVariablesType( assert, "variables", function( invalidValue ) { + return function() { + Globalize( "en" ).formatMessage( "greetings/hello", invalidValue ); + }; + }); +}); + +QUnit.test( "should format a message", function( assert ) { + assert.equal( Globalize( "en" ).formatMessage( "greetings/hello", { + name: "Beethoven" + }), "Hello, Beethoven" ); +}); + +}); diff --git a/test/functional/message/message-formatter.js b/test/functional/message/message-formatter.js new file mode 100644 index 000000000..92e166841 --- /dev/null +++ b/test/functional/message/message-formatter.js @@ -0,0 +1,156 @@ +define([ + "globalize", + "json!cldr-data/supplemental/likelySubtags.json", + "json!cldr-data/supplemental/plurals.json", + "../../util", + + "cldr/event", + "cldr/unresolved", + "globalize/message", + "globalize/plural" +], function( Globalize, likelySubtags, plurals, util ) { + +QUnit.assert.messageFormatter = function( locale, path, variables, expected ) { + if ( arguments.length === 3 ) { + expected = variables; + variables = undefined; + } + this.equal( Globalize( locale ).messageFormatter( path )( variables ), expected ); +}; + +QUnit.module( ".messageFormatter( path )", { + setup: function() { + Globalize.load( likelySubtags ); + Globalize.load( plurals ); + Globalize.loadMessages({ + root: { + amen: "Amen" + }, + pt: { + amen: "Amém" + }, + zh: { + amen: "阿门" + }, + en: { + greetings: { + 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" + ] + } + }); + }, + teardown: util.resetCldrContent +}); + +QUnit.test( "should validate parameters", function( assert ) { + util.assertParameterPresence( assert, "path", function() { + Globalize( "en" ).messageFormatter(); + }); + + util.assertPathParameter( assert, "path", function( invalidValue ) { + return function() { + Globalize( "en" ).messageFormatter( invalidValue ); + }; + }); +}); + +QUnit.test( "should validate messages", function( assert ) { + util.assertMessagePresence( assert, "non-existent/path", function() { + Globalize( "en" ).messageFormatter( "non-existent/path" ); + }); + + util.assertMessageType( assert, "invalid-message", function( invalidValue ) { + Globalize.loadMessages({ + en: { + "invalid-message": invalidValue + } + }); + return function() { + Globalize( "en" ).messageFormatter( "invalid-message" ); + }; + }); +}); + +QUnit.test( "should return the loaded translation", function( assert ) { + assert.messageFormatter( "pt", "amen", "Amém" ); + assert.messageFormatter( "zh", "amen", "阿门" ); +}); + +QUnit.test( "should traverse the translation data", function( assert ) { + assert.messageFormatter( "en", "greetings/hello", "Hello" ); + assert.messageFormatter( "en", [ "greetings", "hello" ], "Hello" ); +}); + +QUnit.test( "should return inherited translation if cldr/unresolved is loaded", function( assert ) { + assert.messageFormatter( "en", "amen", "Amen" ); + assert.messageFormatter( "de", "amen", "Amen" ); + assert.messageFormatter( "en-GB", "amen", "Amen" ); + assert.messageFormatter( "fr", "amen", "Amen" ); + assert.messageFormatter( "pt-PT", "amen", "Amém" ); +}); + +QUnit.test( "should support ICU message format", function( assert ) { + var like; + + // Var replacement + assert.messageFormatter( "en", "greetings/helloArray", [ "Beethoven" ], "Hello, Beethoven" ); + assert.messageFormatter( "en", "greetings/helloArray", "Beethoven", "Hello, Beethoven" ); + assert.messageFormatter( "en", "greetings/helloArray2", [ "Beethoven", "Mozart" ], + "Hello, Beethoven and Mozart" ); + assert.equal( + Globalize( "en" ).messageFormatter( "greetings/helloArray2" )( "Beethoven", "Mozart" ), + "Hello, Beethoven and Mozart" + ); + assert.messageFormatter( "en", "greetings/helloName", { + name: "Beethoven" + }, "Hello, Beethoven" ); + + // Plural + assert.messageFormatter( "en", "task", 123, "You have 123 tasks remaining" ); + + // Select + assert.messageFormatter( "en", "party", { + guest: "Mozart", + host: "Beethoven", + hostGender: "male" + }, "Beethoven invites Mozart to his party" ); + + // Plural offset + like = new Globalize( "en" ).messageFormatter( "like" ); + assert.equal( like({ count: 0 }), "Be the first to like this" ); + + assert.equal( like({ count: 1 }), "You liked this" ); + + assert.equal( like({ + count: 2, + someone: "Beethoven" + }), "You and Beethoven liked this" ); + + assert.equal( like({ count: 3 }), "You and 2 others liked this" ); +}); + +}); diff --git a/test/functional/message/translate.js b/test/functional/message/translate.js deleted file mode 100644 index bd4427bfd..000000000 --- a/test/functional/message/translate.js +++ /dev/null @@ -1,63 +0,0 @@ -define([ - "globalize", - "json!cldr-data/supplemental/likelySubtags.json", - "../../util", - - "cldr/unresolved", - "globalize/message" -], function( Globalize, likelySubtags, util ) { - -QUnit.module( ".translate( path )", { - setup: function() { - Globalize.load( likelySubtags ); - Globalize.loadTranslations({ - root: { - amen: "Amen" - }, - pt: { - amen: "Amém" - }, - zh: { - amen: "阿门" - }, - en: { - greetings: { - hello: "Hello" - } - } - }); - }, - teardown: util.resetCldrContent -}); - -QUnit.test( "should validate parameters", function( assert ) { - util.assertParameterPresence( assert, "path", function() { - Globalize.translate(); - }); - - util.assertPathParameter( assert, "path", function( invalidValue ) { - return function() { - Globalize.translate( invalidValue ); - }; - }); -}); - -QUnit.test( "should return the loaded translation", function( assert ) { - assert.equal( Globalize( "pt" ).translate( "amen" ), "Amém" ); - assert.equal( Globalize( "zh" ).translate( "amen" ), "阿门" ); -}); - -QUnit.test( "should traverse the translation data", function( assert ) { - assert.equal( Globalize( "en" ).translate( "greetings/hello" ), "Hello" ); - assert.equal( Globalize( "en" ).translate([ "greetings", "hello" ]), "Hello" ); -}); - -QUnit.test( "should return inherited translation if cldr/unresolved is loaded", function( assert ) { - assert.equal( Globalize( "en" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "de" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "en-GB" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "fr" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "pt-PT" ).translate( "amen" ), "Amém" ); -}); - -}); diff --git a/test/unit.js b/test/unit.js index 86322f715..8d0ed3a9c 100644 --- a/test/unit.js +++ b/test/unit.js @@ -33,9 +33,6 @@ require([ "./unit/date/parse", - // message - "./unit/message/translate", - // number "./unit/number/pattern-properties", "./unit/number/format/integer-fraction-digits", diff --git a/test/unit/message/translate.js b/test/unit/message/translate.js deleted file mode 100644 index 6b532f240..000000000 --- a/test/unit/message/translate.js +++ /dev/null @@ -1,47 +0,0 @@ -define([ - "src/core", - "json!cldr-data/supplemental/likelySubtags.json", - - "cldr/unresolved", - "src/message" -], function( Globalize, likelySubtags ) { - -Globalize.load( likelySubtags ); -Globalize.loadTranslations({ - root: { - amen: "Amen" - }, - pt: { - amen: "Amém" - }, - zh: { - amen: "阿门" - }, - en: { - greetings: { - hello: "Hello" - } - } -}); - -QUnit.module( "Translate" ); - -QUnit.test( "should return the loaded translation", function( assert ) { - assert.equal( Globalize( "pt" ).translate( "amen" ), "Amém" ); - assert.equal( Globalize( "zh" ).translate( "amen" ), "阿门" ); -}); - -QUnit.test( "should traverse the translation data", function( assert ) { - assert.equal( Globalize( "en" ).translate( "greetings/hello" ), "Hello" ); - assert.equal( Globalize( "en" ).translate([ "greetings", "hello" ]), "Hello" ); -}); - -QUnit.test( "should return inherited translation if cldr/unresolved is loaded", function( assert ) { - assert.equal( Globalize( "en" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "de" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "en-GB" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "fr" ).translate( "amen" ), "Amen" ); - assert.equal( Globalize( "pt-PT" ).translate( "amen" ), "Amém" ); -}); - -}); diff --git a/test/util.js b/test/util.js index 54f2f684a..6dbf5a85e 100644 --- a/test/util.js +++ b/test/util.js @@ -100,6 +100,27 @@ return { assertParameterType( assert, [ "cldr", "null", "string" ], name, fn ); }, + assertMessagePresence: function( assert, path, fn ) { + assert.throws( fn, function E_MISSING_PARAMETER( error ) { + return error.code === "E_MISSING_MESSAGE" && + error.path === path; + }, "Expected \"E_MISSING_MESSAGE: Missing required message content `" + path + "`\" to be thrown" ); + }, + + assertMessageType: function( assert, path, fn ) { + Object.keys( allTypes ).filter( not([ "array", "string" ]) ).forEach(function( type ) { + assert.throws( fn( allTypes[ type ] ), function E_INVALID_MESSAGE( error ) { + return error.code === "E_INVALID_MESSAGE" && + error.path === path && + "expected" in error; + }, "Expected \"E_INVALID_MESSAGE: Invalid message content `" + path + "`\" to be thrown. (" + type + ")" ); + }); + }, + + assertMessageVariablesType: function( assert, name, fn ) { + assertParameterType( assert, [ "array", "cldr", "number", "plainObject", "string" ], name, fn ); + }, + assertNumberParameter: function( assert, name, fn ) { assertParameterType( assert, "number", name, fn ); },