diff --git a/dist/amd/currency.js b/dist/amd/currency.js new file mode 100644 index 0000000..18dcc81 --- /dev/null +++ b/dist/amd/currency.js @@ -0,0 +1,7 @@ +define(['react', './generator', 'globalize/currency'], function (React, generator) { + + 'use strict'; + + return generator("formatCurrency", ["value", "currency", "options"]); + +}); \ No newline at end of file diff --git a/dist/amd/date.js b/dist/amd/date.js new file mode 100644 index 0000000..90c539c --- /dev/null +++ b/dist/amd/date.js @@ -0,0 +1,7 @@ +define(['react', './generator', 'globalize/date'], function (React, generator) { + + 'use strict'; + + return generator("formatDate", ["value", "options"]); + +}); \ No newline at end of file diff --git a/dist/amd/generator.js b/dist/amd/generator.js new file mode 100644 index 0000000..858b5ac --- /dev/null +++ b/dist/amd/generator.js @@ -0,0 +1,59 @@ +define(['react', 'globalize'], function (React, Globalize) { + + 'use strict'; + + var commonPropNames = ["elements", "locale"]; + + function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + function omit(set) { + return function(element) { + return set.indexOf(element) === -1; + }; + } + + function generator(fn, localPropNames, options) { + options = options || {}; + var Fn = capitalizeFirstLetter(fn); + var beforeFormat = options.beforeFormat || function() {}; + var afterFormat = options.afterFormat || function(formattedValue) { + return formattedValue; + }; + var globalizePropNames = commonPropNames.concat(localPropNames); + + return React.createClass({ + displayName: Fn, + componentWillMount: function() { + this.setup(this.props); + }, + componentWillReceiveProps: function(nextProps) { + this.setup(nextProps); + }, + setup: function(props) { + this.globalize = props.locale ? Globalize(props.locale) : Globalize; + this.domProps = Object.keys(props).filter(omit(globalizePropNames)).reduce(function(memo, propKey) { + memo[propKey] = props[propKey]; + return memo; + }, {}); + + this.globalizePropValues = localPropNames.map(function(element) { + return props[element]; + }); + this.globalizePropValues[0] = props.children; + + beforeFormat.call(this); + var formattedValue = this.globalize[fn].apply(this.globalize, this.globalizePropValues); + this.value = afterFormat.call(this, formattedValue); + }, + render: function() { + return React.DOM.span(this.domProps, this.value); + } + }); + } + + + return generator; + +}); \ No newline at end of file diff --git a/dist/amd/message.js b/dist/amd/message.js new file mode 100644 index 0000000..ac98253 --- /dev/null +++ b/dist/amd/message.js @@ -0,0 +1,199 @@ +define(['globalize', 'react', './generator', 'globalize/message', 'globalize/plural'], function (Globalize, React, generator) { + + 'use strict'; + + function messageSetup(globalize, props, globalizePropValues) { + var defaultMessage; + var children = props.children; + var scope = props.scope; + + function getDefaultMessage(children) { + if (typeof children === "string") { + return children; + } else { + throw new Error("Invalid default message type `" + typeof children + "`"); + } + } + + // Set path - path as props supercedes default value. + if (props.path) { + // Override generator assumption. The generator assumes the globalizePropValues[0] + // (path) and props.children to be mutually exclusive, but this isn't + // true here for messages. Because, it's possible to use props.path (for + // path) and props.children for defaultMessage, which are two different + // variables. + globalizePropValues[0] = props.path; + } else { + // Although the generator had already set globalizePropValues[0] (path) as + // props.children, here its type is checked and its value is sanitized. + defaultMessage = getDefaultMessage(children); + globalizePropValues[0] = sanitizePath(defaultMessage); + } + + // Scope path. + if (scope) { + globalizePropValues[0] = scope + "/" + globalizePropValues[0]; + } + + // Development mode only. + if (process.env.NODE_ENV !== "production") { + var path = props.path ? props.path.split("/") : [globalizePropValues[0]]; + /* eslint-disable no-inner-declarations */ + function getMessage(globalize, path) { + return globalize.cldr.get(["globalize-messages/{bundle}"].concat(path)); + } + + function setMessage(globalize, path, message) { + var data = {}; + function set(data, path, value) { + var i; + var node = data; + var length = path.length; + + for (i = 0; i < length - 1; i++) { + if (!node[path[i]]) { + node[path[i]] = {}; + } + node = node[path[i]]; + } + node[path[i]] = value; + } + set(data, [globalize.cldr.attributes.bundle].concat(path), message); + Globalize.loadMessages(data); + } + /* eslint-enable no-inner-declarations */ + + if (globalize.cldr) { + if (!getMessage(globalize, path)) { + defaultMessage = defaultMessage || getDefaultMessage(children); + setMessage(globalize, path, defaultMessage); + } + } + } + } + + function replaceElements(props, formatted) { + var elements = props.elements; + + function _replaceElements(string, elements) { + if (typeof string !== "string") { + throw new Error("Missing or invalid string `" + string + "` (" + typeof string + ")"); + } + if (typeof elements !== "object") { + throw new Error("Missing or invalid elements `" + elements + "` (" + typeof elements + ")"); + } + + // Given [x, y, z], it returns [x, element, y, element, z]. + function spreadElementsInBetweenItems(array, element) { + var getElement = typeof element === "function" ? element : function() { + return element; + }; + return array.slice(1).reduce(function(ret, item, i) { + ret.push(getElement(i), item); + return ret; + }, [array[0]]); + } + + function splice(sourceArray, start, deleteCount, itemsArray) { + [].splice.apply(sourceArray, [start, deleteCount].concat(itemsArray)); + } + + return Object.keys(elements).reduce(function(ret, key) { + var element = elements[key]; + + ret.forEach(function(string, i) { + var aux, contents, regexp, regexp2; + + // Insert array into the correct ret position. + function replaceRetItem(array) { + splice(ret, i, 1, array); + } + + if (typeof string !== "string") { + return; // continue; + } + + // Empty tags, e.g., `[foo/]`. + aux = string.split("[" + key + "/]"); + if (aux.length > 1) { + aux = spreadElementsInBetweenItems(aux, element); + replaceRetItem(aux); + return; // continue; + } + + // Start-end tags, e.g., `[foo]content[/foo]`. + regexp = new RegExp("\\[" + key + "\\][\\s\\S]*?\\[\\/" + key + "\\]", "g"); + regexp2 = new RegExp("\\[" + key + "\\]([\\s\\S]*?)\\[\\/" + key + "\\]"); + aux = string.split(regexp); + if (aux.length > 1) { + contents = string.match(regexp).map(function(content) { + return content.replace(regexp2, "$1"); + }); + aux = spreadElementsInBetweenItems(aux, function(i) { + return React.cloneElement(element, {}, contents[i]); + }); + replaceRetItem(aux); + } + }); + + return ret; + }, [string]); + } + + + // Elements replacement. + if (elements) { + formatted = React.DOM.span.apply(React.DOM.span, [{}].concat(_replaceElements(formatted, elements))); + } + + return formatted; + } + + function sanitizePath(pathString) { + return pathString.trim().replace(/\{/g, "(").replace(/\}/g, ")").replace(/\//g, "|").replace(/\n/g, " ").replace(/ +/g, " ").replace(/"/g, "'"); + } + + // Overload Globalize's `.formatMessage` to allow default message. + var globalizeMessageFormatter = Globalize.messageFormatter; + Globalize.messageFormatter = Globalize.prototype.messageFormatter = function(pathOrMessage) { + var aux = {}; + var sanitizedPath = sanitizePath(pathOrMessage); + + // Globalize runtime + if (!this.cldr) { + // On runtime, the only way for deciding between using sanitizedPath or + // pathOrMessage as path is by checking which formatter exists. + arguments[0] = sanitizedPath; + aux = globalizeMessageFormatter.apply(this, arguments); + arguments[0] = pathOrMessage; + return aux || globalizeMessageFormatter.apply(this, arguments); + } + + var sanitizedPathExists = this.cldr.get(["globalize-messages/{bundle}", sanitizedPath]) !== undefined; + var pathExists = this.cldr.get(["globalize-messages/{bundle}", pathOrMessage]) !== undefined; + + // Want to distinguish between default message and path value - just checking + // for sanitizedPath won't be enough, because sanitizedPath !== pathOrMessage + // for paths like "salutations/hi". + if (!sanitizedPathExists && !pathExists) { + aux[this.cldr.attributes.bundle] = {}; + aux[this.cldr.attributes.bundle][sanitizedPath] = pathOrMessage; + Globalize.loadMessages(aux); + sanitizedPathExists = true; + } + + arguments[0] = sanitizedPathExists ? sanitizedPath : pathOrMessage; + return globalizeMessageFormatter.apply(this, arguments); + }; + + + return generator("formatMessage", ["path", "variables"], { + beforeFormat: function() { + messageSetup(this.globalize, this.props, this.globalizePropValues); + }, + afterFormat: function(formattedValue) { + return replaceElements(this.props, formattedValue); + } + }); + +}); \ No newline at end of file diff --git a/dist/amd/number.js b/dist/amd/number.js new file mode 100644 index 0000000..57b917d --- /dev/null +++ b/dist/amd/number.js @@ -0,0 +1,7 @@ +define(['react', './generator', 'globalize/number'], function (React, generator) { + + 'use strict'; + + return generator("formatNumber", ["value", "options"]); + +}); \ No newline at end of file diff --git a/dist/amd/relative-time.js b/dist/amd/relative-time.js new file mode 100644 index 0000000..78c699c --- /dev/null +++ b/dist/amd/relative-time.js @@ -0,0 +1,7 @@ +define(['react', './generator', 'globalize/relative-time'], function (React, generator) { + + 'use strict'; + + return generator("formatRelativeTime", ["value", "unit", "options"]); + +}); \ No newline at end of file diff --git a/dist/currency.js b/dist/currency.js new file mode 100644 index 0000000..9c4fbe7 --- /dev/null +++ b/dist/currency.js @@ -0,0 +1,7 @@ +'use strict'; + +var React = require('react'); +var generator = require('./generator'); + + +module.exports = generator("formatCurrency", ["value", "currency", "options"]); \ No newline at end of file diff --git a/dist/date.js b/dist/date.js new file mode 100644 index 0000000..6ecd0ea --- /dev/null +++ b/dist/date.js @@ -0,0 +1,7 @@ +'use strict'; + +var React = require('react'); +var generator = require('./generator'); + + +module.exports = generator("formatDate", ["value", "options"]); \ No newline at end of file diff --git a/dist/generator.js b/dist/generator.js new file mode 100644 index 0000000..5896f1b --- /dev/null +++ b/dist/generator.js @@ -0,0 +1,57 @@ +'use strict'; + +var React = require('react'); +var Globalize = require('globalize'); + +var commonPropNames = ["elements", "locale"]; + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function omit(set) { + return function(element) { + return set.indexOf(element) === -1; + }; +} + +function generator(fn, localPropNames, options) { + options = options || {}; + var Fn = capitalizeFirstLetter(fn); + var beforeFormat = options.beforeFormat || function() {}; + var afterFormat = options.afterFormat || function(formattedValue) { + return formattedValue; + }; + var globalizePropNames = commonPropNames.concat(localPropNames); + + return React.createClass({ + displayName: Fn, + componentWillMount: function() { + this.setup(this.props); + }, + componentWillReceiveProps: function(nextProps) { + this.setup(nextProps); + }, + setup: function(props) { + this.globalize = props.locale ? Globalize(props.locale) : Globalize; + this.domProps = Object.keys(props).filter(omit(globalizePropNames)).reduce(function(memo, propKey) { + memo[propKey] = props[propKey]; + return memo; + }, {}); + + this.globalizePropValues = localPropNames.map(function(element) { + return props[element]; + }); + this.globalizePropValues[0] = props.children; + + beforeFormat.call(this); + var formattedValue = this.globalize[fn].apply(this.globalize, this.globalizePropValues); + this.value = afterFormat.call(this, formattedValue); + }, + render: function() { + return React.DOM.span(this.domProps, this.value); + } + }); +} + +module.exports = generator; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..6da4851 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var FormatCurrency = require('./currency'); +var FormatDate = require('./date'); +var FormatMessage = require('./message'); +var FormatNumber = require('./number'); +var FormatRelativeTime = require('./relative-time'); + +module.exports = { + FormatCurrency: FormatCurrency, + FormatDate: FormatDate, + FormatMessage: FormatMessage, + FormatNumber: FormatNumber, + FormatRelativeTime: FormatRelativeTime +}; \ No newline at end of file diff --git a/dist/message.js b/dist/message.js new file mode 100644 index 0000000..a138a8f --- /dev/null +++ b/dist/message.js @@ -0,0 +1,200 @@ +'use strict'; + +var Globalize = require('globalize'); +var React = require('react'); +var generator = require('./generator'); + + + +function messageSetup(globalize, props, globalizePropValues) { + var defaultMessage; + var children = props.children; + var scope = props.scope; + + function getDefaultMessage(children) { + if (typeof children === "string") { + return children; + } else { + throw new Error("Invalid default message type `" + typeof children + "`"); + } + } + + // Set path - path as props supercedes default value. + if (props.path) { + // Override generator assumption. The generator assumes the globalizePropValues[0] + // (path) and props.children to be mutually exclusive, but this isn't + // true here for messages. Because, it's possible to use props.path (for + // path) and props.children for defaultMessage, which are two different + // variables. + globalizePropValues[0] = props.path; + } else { + // Although the generator had already set globalizePropValues[0] (path) as + // props.children, here its type is checked and its value is sanitized. + defaultMessage = getDefaultMessage(children); + globalizePropValues[0] = sanitizePath(defaultMessage); + } + + // Scope path. + if (scope) { + globalizePropValues[0] = scope + "/" + globalizePropValues[0]; + } + + // Development mode only. + if (process.env.NODE_ENV !== "production") { + var path = props.path ? props.path.split("/") : [globalizePropValues[0]]; + /* eslint-disable no-inner-declarations */ + function getMessage(globalize, path) { + return globalize.cldr.get(["globalize-messages/{bundle}"].concat(path)); + } + + function setMessage(globalize, path, message) { + var data = {}; + function set(data, path, value) { + var i; + var node = data; + var length = path.length; + + for (i = 0; i < length - 1; i++) { + if (!node[path[i]]) { + node[path[i]] = {}; + } + node = node[path[i]]; + } + node[path[i]] = value; + } + set(data, [globalize.cldr.attributes.bundle].concat(path), message); + Globalize.loadMessages(data); + } + /* eslint-enable no-inner-declarations */ + + if (globalize.cldr) { + if (!getMessage(globalize, path)) { + defaultMessage = defaultMessage || getDefaultMessage(children); + setMessage(globalize, path, defaultMessage); + } + } + } +} + +function replaceElements(props, formatted) { + var elements = props.elements; + + function _replaceElements(string, elements) { + if (typeof string !== "string") { + throw new Error("Missing or invalid string `" + string + "` (" + typeof string + ")"); + } + if (typeof elements !== "object") { + throw new Error("Missing or invalid elements `" + elements + "` (" + typeof elements + ")"); + } + + // Given [x, y, z], it returns [x, element, y, element, z]. + function spreadElementsInBetweenItems(array, element) { + var getElement = typeof element === "function" ? element : function() { + return element; + }; + return array.slice(1).reduce(function(ret, item, i) { + ret.push(getElement(i), item); + return ret; + }, [array[0]]); + } + + function splice(sourceArray, start, deleteCount, itemsArray) { + [].splice.apply(sourceArray, [start, deleteCount].concat(itemsArray)); + } + + return Object.keys(elements).reduce(function(ret, key) { + var element = elements[key]; + + ret.forEach(function(string, i) { + var aux, contents, regexp, regexp2; + + // Insert array into the correct ret position. + function replaceRetItem(array) { + splice(ret, i, 1, array); + } + + if (typeof string !== "string") { + return; // continue; + } + + // Empty tags, e.g., `[foo/]`. + aux = string.split("[" + key + "/]"); + if (aux.length > 1) { + aux = spreadElementsInBetweenItems(aux, element); + replaceRetItem(aux); + return; // continue; + } + + // Start-end tags, e.g., `[foo]content[/foo]`. + regexp = new RegExp("\\[" + key + "\\][\\s\\S]*?\\[\\/" + key + "\\]", "g"); + regexp2 = new RegExp("\\[" + key + "\\]([\\s\\S]*?)\\[\\/" + key + "\\]"); + aux = string.split(regexp); + if (aux.length > 1) { + contents = string.match(regexp).map(function(content) { + return content.replace(regexp2, "$1"); + }); + aux = spreadElementsInBetweenItems(aux, function(i) { + return React.cloneElement(element, {}, contents[i]); + }); + replaceRetItem(aux); + } + }); + + return ret; + }, [string]); + } + + + // Elements replacement. + if (elements) { + formatted = React.DOM.span.apply(React.DOM.span, [{}].concat(_replaceElements(formatted, elements))); + } + + return formatted; +} + +function sanitizePath(pathString) { + return pathString.trim().replace(/\{/g, "(").replace(/\}/g, ")").replace(/\//g, "|").replace(/\n/g, " ").replace(/ +/g, " ").replace(/"/g, "'"); +} + +// Overload Globalize's `.formatMessage` to allow default message. +var globalizeMessageFormatter = Globalize.messageFormatter; +Globalize.messageFormatter = Globalize.prototype.messageFormatter = function(pathOrMessage) { + var aux = {}; + var sanitizedPath = sanitizePath(pathOrMessage); + + // Globalize runtime + if (!this.cldr) { + // On runtime, the only way for deciding between using sanitizedPath or + // pathOrMessage as path is by checking which formatter exists. + arguments[0] = sanitizedPath; + aux = globalizeMessageFormatter.apply(this, arguments); + arguments[0] = pathOrMessage; + return aux || globalizeMessageFormatter.apply(this, arguments); + } + + var sanitizedPathExists = this.cldr.get(["globalize-messages/{bundle}", sanitizedPath]) !== undefined; + var pathExists = this.cldr.get(["globalize-messages/{bundle}", pathOrMessage]) !== undefined; + + // Want to distinguish between default message and path value - just checking + // for sanitizedPath won't be enough, because sanitizedPath !== pathOrMessage + // for paths like "salutations/hi". + if (!sanitizedPathExists && !pathExists) { + aux[this.cldr.attributes.bundle] = {}; + aux[this.cldr.attributes.bundle][sanitizedPath] = pathOrMessage; + Globalize.loadMessages(aux); + sanitizedPathExists = true; + } + + arguments[0] = sanitizedPathExists ? sanitizedPath : pathOrMessage; + return globalizeMessageFormatter.apply(this, arguments); +}; + +module.exports = generator("formatMessage", ["path", "variables"], { + beforeFormat: function() { + messageSetup(this.globalize, this.props, this.globalizePropValues); + }, + afterFormat: function(formattedValue) { + return replaceElements(this.props, formattedValue); + } +}); \ No newline at end of file diff --git a/dist/number.js b/dist/number.js new file mode 100644 index 0000000..c9ee109 --- /dev/null +++ b/dist/number.js @@ -0,0 +1,7 @@ +'use strict'; + +var React = require('react'); +var generator = require('./generator'); + + +module.exports = generator("formatNumber", ["value", "options"]); \ No newline at end of file diff --git a/dist/relative-time.js b/dist/relative-time.js new file mode 100644 index 0000000..e802345 --- /dev/null +++ b/dist/relative-time.js @@ -0,0 +1,7 @@ +'use strict'; + +var React = require('react'); +var generator = require('./generator'); + + +module.exports = generator("formatRelativeTime", ["value", "unit", "options"]); \ No newline at end of file