diff --git a/LICENSE b/LICENSE index 896ba66..5e2d7c3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Kris Borchers +Copyright (c) 2015 Kris Borchers, Rafael Xavier de Souza @rxaviers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/examples/README.md b/examples/README.md index 4a72102..93587ba 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,5 +6,14 @@ Install These instructions assume you have node and npm installed. For help, see the [npm docs](https://docs.npmjs.com/getting-started/installing-node) 1. Run `npm install` -2. Run `npm run-script build` to generate the built JS file -3. Open browser and navigate to `react-globalize/examples/index.html` + +Development +----------- + +1. Run `npm run start` to start a server with live reload support. + +Production +---------- + +1. Run `npm run build` to generate the distribution JS file +1. Open browser and navigate to `react-globalize/examples/dist/index.html` diff --git a/examples/components/currency.jsx b/examples/components/currency.jsx new file mode 100644 index 0000000..c01b3d5 --- /dev/null +++ b/examples/components/currency.jsx @@ -0,0 +1,201 @@ +var React = require("react"); +var ReactGlobalize = require("react-globalize"); +var FormatCurrency = ReactGlobalize.FormatCurrency; +var FormatMessage = ReactGlobalize.FormatMessage; + +var LocalizedCurrencies = React.createClass({ + getInitialState: function() { + return { + valueA: 150, + valueB: 1, + valueC: -1000, + valueD: 1.491 + } + }, + handleSubmit: function(event) { + event.preventDefault(); + }, + handleValueA: function(event) { + this.setState({ + valueA: event.target.value + }); + }, + handleValueB: function(event) { + this.setState({ + valueB: event.target.value + }); + }, + handleValueC: function(event) { + this.setState({ + valueC: event.target.value + }); + }, + handleValueD: function(event) { + this.setState({ + valueD: event.target.value + }); + }, + render: function() { + console.log("render"); + return ( +
+

Currencies

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionOptionsValueCurrencyLocalized Value
+

Formatting currencies using symbols, the default

+
+

{"{}"}

+
+
+ +
+
+

USD

+
+ {+this.state.valueA} +
+

EUR

+
+ {+this.state.valueA} +
+

CNY

+
+ {+this.state.valueA} +
+

Formatting currencies in their full names

+
+

{"{style: \"name\"}"}

+
+
+ +
+
+

USD

+
+ {+this.state.valueB} +
+

EUR

+
+ {+this.state.valueB} +
+

CNY

+
+ {+this.state.valueB} +
+

Formatting currencies in the accounting form

+
+

{"{style: \"accounting\"}"}

+
+
+ +
+
+

USD

+
+ {+this.state.valueC} +
+

EUR

+
+ {+this.state.valueC} +
+

CNY

+
+ {+this.state.valueC} +
+

Formatting currencies specifying the rounding method

+
+

{"{round: \"ceil\"}"}

+
+
+ +
+
+

USD

+
+ {+this.state.valueD} +
+

EUR

+
+ {+this.state.valueD} +
+

CNY

+
+ {+this.state.valueD} +
+
+ ); + } +}); + +module.exports = LocalizedCurrencies; diff --git a/examples/components/dates.jsx b/examples/components/dates.jsx new file mode 100644 index 0000000..def5ee8 --- /dev/null +++ b/examples/components/dates.jsx @@ -0,0 +1,90 @@ +var React = require("react"); +var ReactGlobalize = require("react-globalize"); +var FormatMessage = ReactGlobalize.FormatMessage; +var FormatDate = ReactGlobalize.FormatDate; + +var LocalizedDates = React.createClass({ + getInitialState: function() { + setInterval(function() { + this.updateDate(); + }.bind(this), 1000); + return { + date: new Date() + } + }, + updateDate: function() { + this.setState({ + date: new Date() + }); + }, + render: function() { + var date = this.state.date; + return ( +
+

Dates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionOptionsLocalized Now
+

Formatting dates using the default options

+
+

{"{}"}

+
+ {date} +
+

Formatting dates using presets

+
+

{"{date: \"medium\"}"}

+
+ {date} +
+

{"{time: \"medium\"}"}

+
+ {date} +
+

{"{datetime: \"medium\"}"}

+
+ {date} +
+

Formatting dates selecting the fields individually

+
+

{"{skeleton: \"GyMMMdhms\"}"}

+
+ {date} +
+
+ ); + } +}); + +module.exports = LocalizedDates; diff --git a/examples/components/messages.jsx b/examples/components/messages.jsx new file mode 100644 index 0000000..3638a2e --- /dev/null +++ b/examples/components/messages.jsx @@ -0,0 +1,186 @@ +var Globalize = require("globalize"); +var React = require("react"); +var ReactGlobalize = require("react-globalize"); +var FormatMessage = ReactGlobalize.FormatMessage; + +var names = ["Beethoven", "Mozart", "Mendelssohn"]; +var gender = { + "Beethoven": "male", + "Mendelssohn": "female", + "Mozart": "male" +}; + +function sanitizePath(pathString) { + return pathString.trim().replace(/\{/g, "(").replace(/\}/g, ")").replace(/\//g, "|").replace(/\n/g, " ").replace(/ +/g, " ").replace(/"/g, "'"); +} + +function getRawLocalizedMessage(message) { + return Globalize.cldr.get(["globalize-messages/{bundle}", sanitizePath(message)]) || message; +} + +var LocalizedMessages = React.createClass({ + getInitialState: function() { + return { + nameA: "Beethoven", + host: "Beethoven", + hostGender: gender["Beethoven"], + guest: "Mozart", + guestGender: gender["Mozart"], + count: 0 + }; + }, + handleSubmit: function(event) { + event.preventDefault(); + }, + handleNameA: function(event) { + this.setState({ + nameA: event.target.value + }); + }, + handleHost: function(event) { + var value = event.target.value; + this.setState({ + host: value, + hostGender: gender[value] + }); + }, + handleGuest: function(event) { + var value = event.target.value; + this.setState({ + guest: value, + guestGender: gender[value] + }); + }, + handleCount: function(event) { + var value = event.target.value; + if (+value < 0) { + return; + } + this.setState({ + count: value + }); + }, + render: function() { + return ( +
+

Messages

+

Simple variable replacement

+
+
Message
+
{getRawLocalizedMessage("Hello, {name}")}
+
Variables
+
+
{"{name: \"" + this.state.nameA + "\"}"}
+
+ +
+
+
Localized message
+
{"Hello, {name}"}
+
+ +

Gender inflection

+
+
Message
+
{getRawLocalizedMessage(
+                        "{hostGender, select,\n" +
+                        "  female {{host} invites {guest} to her party}\n" +
+                        "    male {{host} invites {guest} to his party}\n" +
+                        "   other {{host} invites {guest} to their party}\n" +
+                        "}"
+                    )}
+
Variables
+
+
{
+                            "{\n" +
+                            "  host: \"" + this.state.host + "\",\n" +
+                            "  hostGender: \"" +  this.state.hostGender + "\",\n" +
+                            "  guest: \"" +  this.state.guest + "\",\n" +
+                            "  guestGender: \"" +  this.state.guestGender + "\"\n" +
+                            "}"
+                        }
+
+ + +
+
+
Localized message
+
+ { + "{hostGender, select,\n" + + " female {{host} invites {guest} to her party}\n" + + " male {{host} invites {guest} to his party}\n" + + " other {{host} invites {guest} to their party}\n" + + "}" + } +
+
+ +

Pluralization

+
+
Message
+
{getRawLocalizedMessage(
+                        "You have {count, plural,\n" +
+                        "     =0 {no tasks}\n" +
+                        "    one {one task}\n" +
+                        "  other {{formattedCount} tasks}\n" +
+                        "} remaining"
+                    )}
+
Variables
+
+
{
+                            "{\n" +
+                            "  count: " + (+this.state.count) + ",\n" +
+                            "  formattedCount: \"" +  Globalize.formatNumber(+this.state.count) + "\"\n" +
+                            "}"
+                        }
+
+ +
+
+
Localized message
+
+ { + "You have {count, plural,\n" + + " =0 {no tasks}\n" + + " one {one task}\n" + + " other {{formattedCount} tasks}\n" + + "} remaining" + } +
+
+ +
+ ); + } +}); + +module.exports = LocalizedMessages; diff --git a/examples/components/numbers.jsx b/examples/components/numbers.jsx new file mode 100644 index 0000000..07a11c8 --- /dev/null +++ b/examples/components/numbers.jsx @@ -0,0 +1,151 @@ +var React = require("react"); +var ReactGlobalize = require("react-globalize"); +var FormatNumber = ReactGlobalize.FormatNumber; +var FormatMessage = ReactGlobalize.FormatMessage; + +var LocalizedNumbers = React.createClass({ + getInitialState: function() { + return { + valueA: Math.PI, + valueB: 1, + valueC: 0.5 + } + }, + handleSubmit: function(event) { + event.preventDefault(); + }, + handleValueA: function(event) { + this.setState({ + valueA: event.target.value + }); + }, + handleValueB: function(event) { + this.setState({ + valueB: event.target.value + }); + }, + handleValueC: function(event) { + this.setState({ + valueC: event.target.value + }); + }, + render: function() { + return ( +
+

Numbers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionOptionsValueLocalized Value
+

Formatting numbers using the default options

+
+

{"{}"}

+
+
+ +
+
+ {+this.state.valueA} +
+

Formatting numbers specifying the rounding method

+
+

{"{round: \"floor\"}"}

+
+ {+this.state.valueA} +
+

Formatting numbers specifying the maximum fraction digits

+
+

{"{maximumFractionDigits: 2}"}

+
+ {+this.state.valueA} +
+

Formatting numbers specifying significant digits

+
+

+ {"{"}
+

minimumSignificantDigits: 2,
maximumSignificantDigits: 4
+ {"}"} +

+
+ {+this.state.valueA} +
+

Formatting numbers using the default options (again)

+
+

{"{}"}

+
+
+ +
+
+ {+this.state.valueB} +
+

Formatting numbers specifying the maximum fraction digits

+
+

{"{minimumFractionDigits: 2}"}

+
+ {+this.state.valueB} +
+

Formatting percentages

+
+

{"{style: \"percent\"}"}

+
+
+ +
+
+ {+this.state.valueC} +
+
+ ); + } +}); + +module.exports = LocalizedNumbers; diff --git a/examples/components/relative-time.jsx b/examples/components/relative-time.jsx new file mode 100644 index 0000000..daf4132 --- /dev/null +++ b/examples/components/relative-time.jsx @@ -0,0 +1,107 @@ +var React = require("react"); +var ReactGlobalize = require("react-globalize"); +var FormatMessage = ReactGlobalize.FormatMessage; +var FormatRelativeTime = ReactGlobalize.FormatRelativeTime; + +var LocalizedDates = React.createClass({ + getInitialState: function() { + setInterval(function() { + this.updateSeconds(); + }.bind(this), 1000); + setInterval(function() { + this.updateMinutes(); + }.bind(this), 1000 * 60); + return { + seconds: 0, + minutes: 0 + } + }, + updateSeconds: function() { + this.setState({ + seconds: --this.state.seconds + }); + }, + updateMinutes: function() { + this.setState({ + minutes: --this.state.minutes + }); + }, + render: function() { + var date = this.state.date; + return ( +
+

Relative Times

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionUnitOptionsLocalized relative time from this page load
+

Formatting relative times in seconds

+
+

second

+
+

{"{}"}

+
+ {this.state.seconds} +
+

Formatting relative times in seconds using the short form

+
+

second

+
+

{"{form: \"short\"}"}

+
+ {this.state.seconds} +
+

Formatting relative times in minutes

+
+

minute

+
+

{"{}"}

+
+ {this.state.minutes} +
+

Formatting relative times in minutes using the narrow form

+
+

minute

+
+

{"{form: \"narrow\"}"}

+
+ {this.state.minutes} +
+
+ ); + } +}); + +module.exports = LocalizedDates; diff --git a/examples/index-template.html b/examples/index-template.html new file mode 100644 index 0000000..b644d88 --- /dev/null +++ b/examples/index-template.html @@ -0,0 +1,18 @@ + + + + + React Globalize Demo + + +
+
+
+
+
+ + {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} + + {% } %} + + diff --git a/examples/index.jsx b/examples/index.jsx new file mode 100644 index 0000000..7347d9a --- /dev/null +++ b/examples/index.jsx @@ -0,0 +1,22 @@ +var React = require("react"); +var LocalizedCurrencies = require("./components/currency"); +var LocalizedDates = require("./components/dates"); +var LocalizedMessages = require("./components/messages"); +var LocalizedNumbers = require("./components/numbers"); +var LocalizedRelativeTime = require("./components/relative-time"); + +React.render( + , document.getElementById("currency") +); +React.render( + , document.getElementById("dates") +); +React.render( + , document.getElementById("messages") +); +React.render( + , document.getElementById("numbers") +); +React.render( + , document.getElementById("relative-time") +); diff --git a/examples/package.json b/examples/package.json index 6e263f1..b00e1ae 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,18 +1,26 @@ - { "name": "react-globalize-example", - "description": "Examples demonstrating use of the components", + "description": "Examples demonstrating use of the react-globalize components", "main": "index.js", - "devDependencies": { - "browserify": "^6.3.3", - "reactify": "^0.17.1" - }, "dependencies": { - "cldr-data": "~26.0.9", - "react": "~0.13.1", - "globalize": "~1.0.0-alpha.18" + "cldr-data": ">=25", + "react": "~0.13.0", + "globalize": "git://github.com/jquery/globalize#runtime-temp-build", + "react-globalize": "git://github.com/rxaviers/react-globalize#b0.3.0" + }, + "devDependencies": { + "babel": "^5.1.10", + "babel-core": "^4.7.8", + "babel-loader": "^5.0.0", + "html-webpack-plugin": "^1.1.0", + "nopt": "^3.0.3", + "react-globalize-webpack-plugin": "git://github.com/rxaviers/react-globalize-webpack-plugin#b0.0.1", + "react-hot-loader": "^1.2.3", + "webpack": "^1.9.0", + "webpack-dev-server": "^1.9.0" }, "scripts": { - "build": "cp -f ../index.js react-globalize.js && browserify --debug --transform reactify index.js > app.js" + "start": "./node_modules/.bin/webpack-dev-server --config webpack-config.js --hot --progress --colors --inline --content-base ./tmp", + "build": "./node_modules/.bin/webpack --production --config webpack-config.js" } } diff --git a/examples/webpack-config.js b/examples/webpack-config.js new file mode 100644 index 0000000..ec022a8 --- /dev/null +++ b/examples/webpack-config.js @@ -0,0 +1,65 @@ +var webpack = require("webpack"); +var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var ReactGlobalizePlugin = require("react-globalize-webpack-plugin"); +var nopt = require("nopt"); + +var jsLoaders = ["babel"]; +var options = nopt({ + production: Boolean +}); + +module.exports = { + entry: options.production ? { + main: "./index.jsx", + vendor: ["react", "globalize"] + } : "./index.jsx", + debug: !options.production, + output: { + path: options.production ? "./dist" : "./tmp", + publicPath: options.production ? "" : "http://localhost:8080/", + filename: options.production ? "app.[hash].js" : "app.js", + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loaders: jsLoaders + }, + { + test: /\.jsx$/, + exclude: /node_modules/, + loaders: options.production ? jsLoaders : ["react-hot"].concat(jsLoaders) + } + ] + }, + resolve: { + extensions: ["", ".js", ".jsx"], + }, + plugins: options.production ? [ + // Important to keep React file size down + new webpack.DefinePlugin({ + "process.env": { + "NODE_ENV": JSON.stringify("production"), + }, + }), + new webpack.optimize.DedupePlugin(), + new HtmlWebpackPlugin({ + template: "./index-template.html", + production: true, + }), + new ReactGlobalizePlugin({ + production: options.production, + defaultLocale: "en" + }), + new CommonsChunkPlugin("vendor", "vendor.[hash].js") + ] : [ + new HtmlWebpackPlugin({ + template: "./index-template.html", + }), + new ReactGlobalizePlugin({ + defaultLocale: "en" + }) + ] +}; diff --git a/package.json b/package.json index 3b950f3..3ed79b5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "cldr" ], "author": "Kris Borchers", + "contributors": [ + "Rafael Xavier de Souza @rxaviers" + ], "homepage": "https://github.com/kborchers/react-globalize", "license": "MIT", "repository": "kborchers/react-globalize", diff --git a/src/generator.js b/src/generator.js index a0933f4..5a29783 100644 --- a/src/generator.js +++ b/src/generator.js @@ -9,6 +9,9 @@ function generator(fn, argArray, options) { var Fn = capitalizeFirstLetter(fn); options = options || {}; var beforeFormat = options.beforeFormat || function() {}; + var afterFormat = options.afterFormat || function(formattedValue) { + return formattedValue; + } return { displayName: Fn, format: function() { @@ -30,7 +33,7 @@ function generator(fn, argArray, options) { } beforeFormat.call(this); - return React.DOM.span(null, this.format()); + return React.DOM.span(null, afterFormat.call(this, this.format())); } } }; diff --git a/src/message.js b/src/message.js index 7b239e7..6a4f295 100644 --- a/src/message.js +++ b/src/message.js @@ -71,6 +71,83 @@ function messageSetup(componentProps, instance, args) { } +function replaceElements(componentProps, formatted) { + var elements = componentProps.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, "'"); } @@ -93,5 +170,8 @@ Globalize.prototype.messageFormatter = function(pathOrMessage) { export default React.createClass(generator("formatMessage", ["path", "variables"], { beforeFormat: function() { messageSetup(this.props, this.instance, this.args); + }, + afterFormat: function(formattedValue) { + return replaceElements(this.props, formattedValue); } }));