├── .gitignore ├── examples ├── .gitignore ├── README.md ├── package.json ├── index.html ├── components │ ├── dates.js │ ├── numbers.js │ ├── currency.js │ └── messages.js └── index.js ├── test ├── mocha.opts ├── formatCurrency.spec.js ├── formatNumber.spec.js ├── test_setup.js ├── formatRelativeTime.spec.js ├── formatDate.spec.js ├── general.spec.js └── formatMessage.spec.js ├── src ├── date.js ├── number.js ├── currency.js ├── relative-time.js ├── util │ └── always-array.js ├── index.js ├── generator.js └── message.js ├── .babelrc ├── .eslintrc ├── CONTRIBUTING.md ├── rollup.config.js ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | app.js 2 | react-globalize 3 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --colors --require babel-register --require test/test_setup 2 | -------------------------------------------------------------------------------- /src/date.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import generator from "./generator"; 3 | 4 | export default generator("formatDate", ["value", "options"]); 5 | -------------------------------------------------------------------------------- /src/number.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import generator from "./generator"; 3 | 4 | export default generator("formatNumber", ["value", "options"]); 5 | -------------------------------------------------------------------------------- /src/currency.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import generator from "./generator"; 3 | 4 | export default generator("formatCurrency", ["value", "currency", "options"]); 5 | -------------------------------------------------------------------------------- /src/relative-time.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import generator from "./generator"; 3 | 4 | export default generator("formatRelativeTime", ["value", "unit", "options"]); 5 | -------------------------------------------------------------------------------- /src/util/always-array.js: -------------------------------------------------------------------------------- 1 | export default function alwaysArray(stringOrArray) { 2 | return Array.isArray(stringOrArray) ? stringOrArray : stringOrArray ? [stringOrArray] : []; 3 | } 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "env" 5 | ], 6 | "plugins": [ 7 | "transform-class-properties", 8 | "transform-object-rest-spread" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | Install 5 | ------- 6 | These instructions assume you have node and npm installed. For help, see the [npm docs](https://docs.npmjs.com/getting-started/installing-node) 7 | 8 | 1. Build react-globalize (`cd .. && grunt`) 9 | 2. Run `npm install` in this directory 10 | 3. Run `npm run-script build` to generate the built JS file 11 | 4. Open browser and navigate to `react-globalize/examples/index.html` 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import FormatCurrency from "./currency"; 2 | import FormatDate from "./date"; 3 | import FormatMessage from "./message"; 4 | import FormatNumber from "./number"; 5 | import FormatRelativeTime from "./relative-time"; 6 | 7 | export default { 8 | FormatCurrency: FormatCurrency, 9 | FormatDate: FormatDate, 10 | FormatMessage: FormatMessage, 11 | FormatNumber: FormatNumber, 12 | FormatRelativeTime: FormatRelativeTime 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "defaults" 10 | ], 11 | "parser": "babel-eslint", 12 | "plugins": [ 13 | "react" 14 | ], 15 | "ecmaFeatures": { 16 | "modules": true, 17 | }, 18 | "rules": { 19 | "indent": 2, 20 | "no-console": 1, 21 | "no-trailing-spaces": 2, 22 | "no-unused-vars": [0, {"ignore": ["React"]}], 23 | "quotes": [2, "double"], 24 | "semi": 1 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "react-globalize-example", 4 | "description": "Examples demonstrating use of the components", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "browserify": "^6.3.3", 8 | "reactify": "^0.17.1" 9 | }, 10 | "dependencies": { 11 | "cldr-data": "~26.0.9", 12 | "react": "~0.13.1", 13 | "globalize": "~1.0.0-alpha.18" 14 | }, 15 | "scripts": { 16 | "build": "mkdir -p react-globalize && cp ../dist/*.js react-globalize/ && browserify --debug --transform reactify index.js > app.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/formatCurrency.spec.js: -------------------------------------------------------------------------------- 1 | /*global expect React shallow Globalize*/ 2 | import FormatCurrency from "../src/currency"; 3 | 4 | describe("formatCurrency Component", () => { 5 | it("renders as a ", () => { 6 | const wrapper = shallow({150}); 7 | expect(wrapper.type()).to.equal("span"); 8 | }); 9 | 10 | it("formats 150 as $150.00", () => { 11 | const wrapper = shallow({150}); 12 | expect(wrapper.text()).to.equal("$150.00"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/formatNumber.spec.js: -------------------------------------------------------------------------------- 1 | /*global expect React shallow Globalize*/ 2 | import FormatNumber from "../src/number"; 3 | 4 | describe("formatNumber Component", () => { 5 | it("renders as a ", () => { 6 | const wrapper = shallow({Math.PI}); 7 | expect(wrapper.type()).to.equal("span"); 8 | }); 9 | 10 | it("formats pi as 3.141", () => { 11 | const wrapper = shallow({Math.PI}); 12 | expect(wrapper.text()).to.equal("3.141"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/test_setup.js: -------------------------------------------------------------------------------- 1 | import Adapter from "enzyme-adapter-react-16"; 2 | import { expect } from "chai"; 3 | import React from "react"; 4 | import Globalize from "globalize"; 5 | import Enzyme, { shallow } from "enzyme"; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | global.expect = expect; 10 | global.React = React; 11 | global.shallow = shallow; 12 | global.Globalize = Globalize; 13 | 14 | Globalize.load( 15 | require( "cldr-data" ).entireSupplemental(), 16 | require( "cldr-data" ).entireMainFor("en"), 17 | require( "cldr-data" ).entireMainFor("de") 18 | ); 19 | 20 | Globalize.locale("en"); 21 | -------------------------------------------------------------------------------- /test/formatRelativeTime.spec.js: -------------------------------------------------------------------------------- 1 | /*global expect React shallow Globalize*/ 2 | import FormatRelativeTime from "../src/relative-time"; 3 | 4 | describe("formatRelativeTime Component", () => { 5 | it("renders as a ", () => { 6 | const wrapper = shallow({1}); 7 | expect(wrapper.type()).to.equal("span"); 8 | }); 9 | 10 | it("formats value of 1 week from now as 'next week'", () => { 11 | const wrapper = shallow({1}); 12 | expect(wrapper.text()).to.equal("next week"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Welcome! Thanks for your interest in contributing to react-globalize. Please be sure to create an issue to discuss the problem or feature request you would like to fix or contribute. 2 | 3 | Before we can review or merge any pull request, we ask that you sign our [contributor license agreement](http://contribute.jquery.org/cla). 4 | 5 | You can find us on [IRC](http://irc.jquery.org), specifically in #globalize on Freenode should you have any questions. If you've never contributed to open source before, we've put together [a short guide with tips, tricks, and ideas on getting started](http://contribute.jquery.org/open-source/). 6 | 7 | Thank You! 8 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Examples 6 | 17 | 18 | 19 |

Currency

20 |
21 |

Dates

22 |
23 |

Messages

24 |
25 |

Numbers

26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/formatDate.spec.js: -------------------------------------------------------------------------------- 1 | /*global expect React shallow Globalize*/ 2 | import FormatDate from "../src/date"; 3 | 4 | describe("formatDate Component", () => { 5 | it("renders as a ", () => { 6 | const wrapper = shallow({new Date()}); 7 | expect(wrapper.type()).to.equal("span"); 8 | }); 9 | 10 | it("formats date using default pattern as 1/1/2016", () => { 11 | const wrapper = shallow({new Date("Jan 01 2016")}); 12 | expect(wrapper.text()).to.equal("1/1/2016"); 13 | }); 14 | 15 | it("formats date using options 'Jan 1, 2016'", () => { 16 | const wrapper = shallow({new Date(2016, 0, 1)}); 17 | expect(wrapper.text()).to.equal("Jan 1, 2016"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import pkg from "./package.json"; 4 | 5 | export default [ 6 | { 7 | input: "src/index.js", 8 | external: [ 9 | "react", 10 | "globalize", 11 | "globalize/*" 12 | ], 13 | globals: { 14 | react: "React", 15 | globalize: "Globalize" 16 | }, 17 | output: [ 18 | { file: pkg.browser, format: "umd", name: "react-globalize" }, 19 | { file: pkg.main, format: "cjs" }, 20 | { file: pkg.module, format: "es" } 21 | ], 22 | plugins: [ 23 | resolve(), 24 | babel({ 25 | babelrc: false, 26 | presets: [ "react", [ "env", { modules: false } ] ], 27 | plugins: [ "transform-class-properties", "transform-object-rest-spread" ], 28 | exclude: "node_modules/**" 29 | }) 30 | ] 31 | } 32 | ]; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 jQuery Foundation and other contributors, https://jquery.org/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/general.spec.js: -------------------------------------------------------------------------------- 1 | /*global expect React shallow Globalize*/ 2 | import FormatCurrency from "../src/currency"; 3 | import FormatMessage from "../src/message"; 4 | 5 | describe("Any Component", () => { 6 | it("doesn't forward ReactGlobalize specific props to underlying DOM component", () => { 7 | let wrapper = shallow({150}); 8 | expect(wrapper.props()).to.contain.all.keys(["children"]); 9 | expect(wrapper.props()).to.not.contain.any.keys(["locale", "currency"]); 10 | 11 | wrapper = shallow(Hello); 12 | expect(wrapper.props()).to.contain.all.keys(["children", "className", "style"]); 13 | expect(wrapper.props()).to.not.contain.any.keys(["elements", "variables"]); 14 | }); 15 | 16 | it("overrides default locale to format 150 as 150,00 €", () => { 17 | const wrapper = shallow({150}); 18 | expect(wrapper.text()).to.equal("150,00 €"); 19 | }); 20 | 21 | it("updates when props change", () => { 22 | const wrapper = shallow({150}); 23 | expect(wrapper.text()).to.equal("150,00 €"); 24 | wrapper.setProps({ children: 200, currency: "USD" }); 25 | expect(wrapper.text()).to.equal("200,00 $"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/components/dates.js: -------------------------------------------------------------------------------- 1 | var FormatDate = require('../react-globalize').FormatDate; 2 | var React = require('react'); 3 | 4 | module.exports = React.createClass({ 5 | getInitialState: function() { 6 | return { 7 | locale: "en" 8 | }; 9 | }, 10 | handleChange: function( event ) { 11 | this.setState({ 12 | locale: event.target.value 13 | }); 14 | }, 15 | render: function() { 16 | return ( 17 |
18 |
19 | Select a locale: 20 | 24 |
25 |
26 | "GyMMMd" - {new Date()} 27 |
28 | date: "medium" - {new Date()} 29 |
30 | time: "medium" - {new Date()} 31 |
32 | datetime: "medium" - {new Date()} 33 |
34 | datetime: "medium" with CSS class - 35 | {new Date()} 36 |
37 | ); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /examples/components/numbers.js: -------------------------------------------------------------------------------- 1 | var FormatNumber = require('../react-globalize').FormatNumber; 2 | var React = require('react'); 3 | 4 | module.exports = React.createClass({ 5 | getInitialState: function() { 6 | return { 7 | locale: "en" 8 | }; 9 | }, 10 | handleChange: function( event ) { 11 | this.setState({ 12 | locale: event.target.value 13 | }); 14 | }, 15 | render: function() { 16 | return ( 17 |
18 |
19 | Select a locale: 20 | 24 |
25 |
26 | pi, no options - {Math.PI} 27 |
28 | pi, maximumFractionDigits: 5 - {Math.PI} 29 |
30 | pi, round: 'floor' - {Math.PI} 31 |
32 | 10000, minimumFractionDigits: 2 - {10000} 33 |
34 | 0.5, style: 'percent' - {0.5} 35 |
36 | 0.5, style: 'percent' with inline styles - 37 | {0.5} 38 |
39 | ); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /src/generator.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Globalize from "globalize"; 3 | import alwaysArray from "./util/always-array"; 4 | 5 | var commonPropNames = ["elements", "locale"]; 6 | 7 | function capitalizeFirstLetter(string) { 8 | return string.charAt(0).toUpperCase() + string.slice(1); 9 | } 10 | 11 | function omit(set) { 12 | return function(element) { 13 | return set.indexOf(element) === -1; 14 | }; 15 | } 16 | 17 | function generator(fn, localPropNames, options) { 18 | options = options || {}; 19 | var Fn = capitalizeFirstLetter(fn); 20 | var beforeFormat = options.beforeFormat || function() {}; 21 | var afterFormat = options.afterFormat || function(props, formattedValue) { 22 | return formattedValue; 23 | }; 24 | var globalizePropNames = commonPropNames.concat(localPropNames); 25 | 26 | return class extends React.Component { 27 | static displayName = Fn; // eslint-disable-line no-undef 28 | 29 | componentWillMount() { 30 | this.setup(this.props); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | this.setup(nextProps); 35 | } 36 | 37 | setup(props) { 38 | this.globalize = props.locale ? Globalize(props.locale) : Globalize; 39 | this.domProps = Object.keys(props).filter(omit(globalizePropNames)).reduce(function(memo, propKey) { 40 | memo[propKey] = props[propKey]; 41 | return memo; 42 | }, {}); 43 | 44 | this.globalizePropValues = localPropNames.map(function(element) { 45 | return props[element]; 46 | }); 47 | this.globalizePropValues[0] = props.children; 48 | 49 | beforeFormat.call(this, props); 50 | var formattedValue = this.globalize[fn](...this.globalizePropValues); 51 | this.value = alwaysArray(afterFormat.call(this, props, formattedValue)); 52 | } 53 | 54 | render() { 55 | return React.createElement("span", this.domProps, ...this.value); 56 | } 57 | }; 58 | } 59 | 60 | export default generator; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-globalize", 3 | "version": "1.0.0", 4 | "description": "Bringing the i18n functionality of Globalize, backed by CLDR, to React", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "browser": "dist/index.umd.js", 8 | "files": [ 9 | "dist/", 10 | "examples/", 11 | "LICENSE", 12 | "README.md", 13 | "src/" 14 | ], 15 | "scripts": { 16 | "build": "rollup -c", 17 | "lint": "eslint rollup.config.js src test", 18 | "test": "mocha test", 19 | "prepublish": "npm run build" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "globalize", 24 | "i18n", 25 | "internationalization", 26 | "translation", 27 | "date", 28 | "currency", 29 | "number", 30 | "format", 31 | "cldr" 32 | ], 33 | "author": "jQuery Foundation and other contributors", 34 | "homepage": "https://github.com/jquery-support/react-globalize", 35 | "license": "MIT", 36 | "repository": "jquery-support/react-globalize", 37 | "bugs": "https://github.com/jquery-support/react-globalize/issues", 38 | "dependencies": { 39 | "globalize": "^1.0.0", 40 | "react": "^16.0.0" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "^6.26.0", 44 | "babel-eslint": "^8.0.1", 45 | "babel-plugin-transform-class-properties": "^6.24.1", 46 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 47 | "babel-preset-env": "^1.6.0", 48 | "babel-preset-react": "^6.24.1", 49 | "babel-register": "^6.7.2", 50 | "chai": "^4.1.2", 51 | "cldr-data": ">=25", 52 | "cldrjs": "^0.4.3", 53 | "create-react-class": "^15.5.2", 54 | "enzyme": "^3.0.0", 55 | "enzyme-adapter-react-16": "^1.0.1", 56 | "eslint": "^1.10.3", 57 | "eslint-config-defaults": "^7.1.1", 58 | "eslint-plugin-react": "^3.11.2", 59 | "globalize": "1.1.x", 60 | "mocha": "^4.0.0", 61 | "react": "^16.0.0", 62 | "react-dom": "^16.0.0", 63 | "react-test-renderer": "^16.0.0", 64 | "rollup": "^0.50.0", 65 | "rollup-plugin-babel": "^3.0.2", 66 | "rollup-plugin-node-resolve": "^3.0.0", 67 | "rollup-watch": "^4.3.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/components/currency.js: -------------------------------------------------------------------------------- 1 | var FormatCurrency = require('../react-globalize').FormatCurrency; 2 | var React = require('react'); 3 | 4 | module.exports = React.createClass({ 5 | getInitialState: function() { 6 | return { 7 | locale: "en" 8 | }; 9 | }, 10 | handleChange: function( event ) { 11 | this.setState({ 12 | locale: event.target.value 13 | }); 14 | }, 15 | render: function() { 16 | return ( 17 |
18 |
19 | Select a locale: 20 | 24 |
25 |
26 | USD, 150, locale default - {150} 27 |
28 | USD, -150, style: "accounting" - {-150} 29 |
30 | USD, 150, style: "name" - {150} 31 |
32 | USD, 150, style: "code" - {150} 33 |
34 | USD, 1.491, round: "ceil" - {1.491} 35 |
36 | EUR, 150, locale default - {150} 37 |
38 | EUR, -150, style: "accounting" - {-150} 39 |
40 | EUR, 150, style: "name" - {150} 41 |
42 | EUR, 150, style: "code" - {150} 43 |
44 | EUR, 1.491, round: "ceil" - {1.491} 45 |
46 | EUR, 150, style: "code", with CSS class - 47 | {150} 48 |
49 | ); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Globalize = require('globalize'); 3 | var LocalizedCurrencies = require('./components/currency'); 4 | var LocalizedDates = require('./components/dates'); 5 | var LocalizedMessages = require('./components/messages'); 6 | var LocalizedNumbers = require('./components/numbers'); 7 | 8 | var messages = { 9 | en: { 10 | salutations: { 11 | hi: "Hi", 12 | bye: "Bye" 13 | }, 14 | variables: { 15 | hello: "Hello, {0} {1} {2}", 16 | hey: "Hey, {first} {middle} {last}" 17 | }, 18 | party: [ 19 | "{hostGender, select,", 20 | " female {{host} invites {guest} to her party}", 21 | " male {{host} invites {guest} to his party}", 22 | " other {{host} invites {guest} to their party}", 23 | "}" 24 | ], 25 | task: [ 26 | "You have {count, plural,", 27 | " =0 {no tasks}", 28 | " one {one task}", 29 | " other {{formattedCount} tasks}", 30 | "} remaining" 31 | ], 32 | likeIncludingMe: [ 33 | "{count, plural, offset:1", 34 | " =0 {Be the first to like this}", 35 | " =1 {You liked this}", 36 | " one {You and someone else liked this}", 37 | " other {You and # others liked this}", 38 | "}" 39 | ] 40 | }, 41 | "pt-BR": { 42 | // default message examples 43 | "Hi": "Oi", 44 | "Bye": "Tchau", 45 | "Hi|Bye": "Oi/Tchau", 46 | salutations: { 47 | hi: "Oi", 48 | bye: "Tchau" 49 | }, 50 | variables: { 51 | hello: "Olá, {0} {1} {2}", 52 | hey: "Ei, {first} {middle} {last}" 53 | }, 54 | party: [ 55 | "{hostGender, select,", 56 | " female {{guestGender, select,", 57 | " female {A {host} convida a {guest} para sua festa}", 58 | " male {A {host} convida o {guest} para sua festa}", 59 | " other {A {host} convida {guest} para sua festa}", 60 | " }}", 61 | " male {{guestGender, select,", 62 | " female {O {host} convida a {guest} para sua festa}", 63 | " male {O {host} convida o {guest} para sua festa}", 64 | " other {O {host} convida {guest} para sua festa}", 65 | " }}", 66 | " other {{guestGender, select,", 67 | " female {{host} convidam a {guest} para sua festa}", 68 | " male {{host} convidam o {guest} para sua festa}", 69 | " other {{host} convidam {guest} para sua festa}", 70 | " }}", 71 | "}" 72 | ], 73 | task: [ 74 | "{count, plural,", 75 | " =0 {Você não tem nenhuma tarefa restante}", 76 | " one {Você tem uma tarefa restante}", 77 | " other {Você tem {formattedCount} tarefas restantes}", 78 | "}" 79 | ], 80 | likeIncludingMe: [ 81 | "{count, plural, offset:1", 82 | " =0 {Seja o primeiro a curtir isto}", 83 | " =1 {Você curtiu isto}", 84 | " one {Você e alguém mais curtiu isto}", 85 | " other {Você e # outros curtiram isto}", 86 | "}" 87 | ] 88 | } 89 | }; 90 | 91 | Globalize.load( 92 | require( 'cldr-data/main/en/ca-gregorian' ), 93 | require( 'cldr-data/main/en/timeZoneNames' ), 94 | require( 'cldr-data/main/en/numbers' ), 95 | require( 'cldr-data/main/en/currencies' ), 96 | require( 'cldr-data/main/pt/ca-gregorian' ), 97 | require( 'cldr-data/main/pt/timeZoneNames' ), 98 | require( 'cldr-data/main/pt/numbers' ), 99 | require( 'cldr-data/main/pt/currencies' ), 100 | require( 'cldr-data/supplemental/currencyData' ), 101 | require( 'cldr-data/supplemental/plurals' ), 102 | require( 'cldr-data/supplemental/likelySubtags' ), 103 | require( 'cldr-data/supplemental/timeData' ), 104 | require( 'cldr-data/supplemental/weekData' ) 105 | ); 106 | Globalize.loadMessages(messages); 107 | 108 | React.render( 109 | , document.getElementById('currency') 110 | ); 111 | React.render( 112 | , document.getElementById('dates') 113 | ); 114 | React.render( 115 | , document.getElementById('messages') 116 | ); 117 | React.render( 118 | , document.getElementById('numbers') 119 | ); 120 | -------------------------------------------------------------------------------- /test/formatMessage.spec.js: -------------------------------------------------------------------------------- 1 | /*global expect React shallow Globalize*/ 2 | import FormatMessage from "../src/message"; 3 | 4 | Globalize.loadMessages({ 5 | en: { 6 | salutations: { 7 | hi: "Hi" 8 | }, 9 | variables: { 10 | hello: "Hello, {0} {1} {2}" 11 | }, 12 | elements: { 13 | rglink: "For more information, see [reactGlobalizeLink]React Globalize[/reactGlobalizeLink]" 14 | }, 15 | htmlElements: { 16 | htmlTags: "You can [strong]emphasize[/strong] words, [br/] and isolate the [strong]important[/strong] ones." 17 | }, 18 | party: [ 19 | "{hostGender, select,", 20 | " female {{host} invites {guest} to her party}", 21 | " male {{host} invites {guest} to his party}", 22 | " other {{host} invites {guest} to their party}", 23 | "}" 24 | ], 25 | task: [ 26 | "You have {count, plural,", 27 | " =0 {no tasks}", 28 | " one {one task}", 29 | " other {{formattedCount} tasks}", 30 | "} remaining" 31 | ] 32 | } 33 | }); 34 | 35 | ["development", "production"].forEach((env) => { 36 | describe(`formatMessage Component (${env})`, () => { 37 | const originalEnv = process.env.NODE_ENV; 38 | 39 | before(() => { 40 | process.env.NODE_ENV = env; 41 | }); 42 | 43 | after(() => { 44 | process.env.NODE_ENV = originalEnv; 45 | }); 46 | 47 | it("renders as a ", () => { 48 | const wrapper = shallow(); 49 | expect(wrapper.type()).to.equal("span"); 50 | }); 51 | 52 | it("uses default message and prints 'Hi'", () => { 53 | const wrapper = shallow(Hi); 54 | expect(wrapper.text()).to.equal("Hi"); 55 | }); 56 | 57 | it("outputs strings not as an array of characters", () => { 58 | const wrapper = shallow(Hi); 59 | expect(wrapper.children().getElements().length).to.equal(1); 60 | }); 61 | 62 | it("resolves path and prints 'Hi'", () => { 63 | const wrapper = shallow(); 64 | expect(wrapper.text()).to.equal("Hi"); 65 | }); 66 | 67 | it("properly replaces variables", () => { 68 | const wrapper = shallow(); 69 | expect(wrapper.text()).to.equal("Hello, Wolfgang Amadeus Mozart"); 70 | }); 71 | 72 | it("properly replaces elements", () => { 73 | const wrapper = shallow(}} />); 74 | expect(wrapper.html()).to.equal("For more information, see React Globalize"); 75 | expect(wrapper.children().getElements().length).to.equal(2); 76 | expect(wrapper.children().get(1).type).to.equal("a"); 77 | }); 78 | 79 | it("properly replaces multiple elements", () => { 80 | const wrapper = shallow(, strong: }} />); 81 | expect(wrapper.html()).to.equal("You can emphasize words,
and isolate the important ones.
"); 82 | }); 83 | 84 | it("uses proper gender inflection", () => { 85 | const wrapper = shallow(); 86 | expect(wrapper.text()).to.equal("Beethoven invites Mozart to their party"); 87 | }); 88 | 89 | it("uses proper plural inflection", () => { 90 | const wrapper = shallow(); 91 | expect(wrapper.text()).to.equal("You have one task remaining"); 92 | }); 93 | 94 | it("updates when children change", () => { 95 | const wrapper = shallow(}}>[testEl]Hello[/testEl]); 96 | wrapper.setProps({ 97 | children: "[testEl]Goodbye[/testEl]", 98 | elements: {testEl: } 99 | }); 100 | expect(wrapper.text()).to.equal("Goodbye"); 101 | expect(wrapper.find("a").prop("href")).to.equal("https://github.com/globalizejs"); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /examples/components/messages.js: -------------------------------------------------------------------------------- 1 | var FormatMessage = require('../react-globalize').FormatMessage; 2 | var React = require('react'); 3 | var Globalize = require('globalize'); 4 | 5 | module.exports = React.createClass({ 6 | getInitialState: function() { 7 | return { 8 | locale: "en" 9 | }; 10 | }, 11 | handleChange: function( event ) { 12 | this.setState({ 13 | locale: event.target.value 14 | }); 15 | }, 16 | render: function() { 17 | return ( 18 |
19 |
20 | Select a locale: 21 | 25 |
26 |

Simple Salutation

27 | hi - 28 |
29 | bye - 30 |

Simple default message

31 | Hi 32 |
33 | Bye 34 |
35 | Hi/Bye 36 |

Default messages with style

37 | 38 | Hi 39 | 40 |
41 | 42 | Bye 43 | 44 |

Variable Replacement

45 | ["Wolfgang", "Amadeus", "Mozart"] - 46 |
47 | {JSON.stringify({first:"Wolfgang", middle:"Amadeus", last:"Mozart"})} - 48 |

Gender Inflection

49 | {JSON.stringify({guest:"Mozart", guestGender:"male", host:"Beethoven", hostGender:"male"})} - 50 |
51 | {JSON.stringify({guest:"Mozart", guestGender:"male", host:"Mendelssohn", hostGender:"female"})} - 52 |
53 | {JSON.stringify({guest:"Mozart", guestGender:"male", host:"Beethoven", hostGender:"other"})} - 54 |
55 | {JSON.stringify({guest:"Mozart", guestGender:"other", host:"Beethoven", hostGender:"male"})} - 56 |
57 | {JSON.stringify({guest:"Mozart", guestGender:"other", host:"Beethoven", hostGender:"other"})} - 58 |

Plural Inflection

59 | task count 1 - 60 |
61 | task count 0 - 62 |
63 | task count 1000 formatted - 64 |
65 | like count 0 with offset:1 - 66 |
67 | like count 1 with offset:1 - 68 |
69 | like count 2 with offset:1 - 70 |
71 | like count 3 with offset:1 - 72 |
73 | ); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | import Globalize from "globalize"; 2 | import React from "react"; 3 | import generator from "./generator"; 4 | 5 | function messageSetup(globalize, props, globalizePropValues) { 6 | var defaultMessage; 7 | var children = props.children; 8 | var scope = props.scope; 9 | 10 | function getDefaultMessage(children) { 11 | if (typeof children === "string") { 12 | return children; 13 | } else { 14 | throw new Error("Invalid default message type `" + typeof children + "`"); 15 | } 16 | } 17 | 18 | // Set path - path as props supercedes default value. 19 | if (props.path) { 20 | // Override generator assumption. The generator assumes the globalizePropValues[0] 21 | // (path) and props.children to be mutually exclusive, but this isn't 22 | // true here for messages. Because, it's possible to use props.path (for 23 | // path) and props.children for defaultMessage, which are two different 24 | // variables. 25 | globalizePropValues[0] = props.path; 26 | } else { 27 | // Although the generator had already set globalizePropValues[0] (path) as 28 | // props.children, here its type is checked and its value is sanitized. 29 | defaultMessage = getDefaultMessage(children); 30 | globalizePropValues[0] = sanitizePath(defaultMessage); 31 | } 32 | 33 | // Scope path. 34 | if (scope) { 35 | globalizePropValues[0] = scope + "/" + globalizePropValues[0]; 36 | } 37 | 38 | // Development mode only. 39 | if (process.env.NODE_ENV !== "production") { 40 | var path = props.path ? props.path.split("/") : [globalizePropValues[0]]; 41 | var getMessage = function(globalize, path) { 42 | return globalize.cldr.get(["globalize-messages/{bundle}"].concat(path)); 43 | }; 44 | 45 | var setMessage = function(globalize, path, message) { 46 | var data = {}; 47 | function set(data, path, value) { 48 | var i; 49 | var node = data; 50 | var length = path.length; 51 | 52 | for (i = 0; i < length - 1; i++) { 53 | if (!node[path[i]]) { 54 | node[path[i]] = {}; 55 | } 56 | node = node[path[i]]; 57 | } 58 | node[path[i]] = value; 59 | } 60 | set(data, [globalize.cldr.attributes.bundle].concat(path), message); 61 | Globalize.loadMessages(data); 62 | }; 63 | 64 | if (globalize.cldr) { 65 | if (!getMessage(globalize, path)) { 66 | defaultMessage = defaultMessage || getDefaultMessage(children); 67 | setMessage(globalize, path, defaultMessage); 68 | } 69 | } 70 | } 71 | } 72 | 73 | function replaceElements(props, formatted) { 74 | var elements = props.elements; 75 | 76 | function _replaceElements(format, elements) { 77 | if (typeof format !== "string") { 78 | throw new Error(`Missing or invalid string \`${format}\` (${typeof format})`); 79 | } 80 | if (typeof elements !== "object") { 81 | throw new Error("Missing or invalid elements `" + elements + "` (" + typeof elements + ")"); 82 | } 83 | 84 | // Given [x, y, z], it returns [x, element, y, element, z]. 85 | function spreadElementsInBetweenItems(array, element) { 86 | var getElement = typeof element === "function" ? element : function() { 87 | return element; 88 | }; 89 | return array.slice(1).reduce(function(ret, item, i) { 90 | ret.push(getElement(i), item); 91 | return ret; 92 | }, [array[0]]); 93 | } 94 | 95 | function splice(sourceArray, start, deleteCount, itemsArray) { 96 | [].splice.apply(sourceArray, [start, deleteCount].concat(itemsArray)); 97 | } 98 | 99 | return Object.keys(elements).reduce((nodes, key) => { 100 | const element = elements[key]; 101 | 102 | // Insert array into the correct ret position. 103 | function replaceNode(array, i) { 104 | splice(nodes, i, 1, array); 105 | } 106 | 107 | for (let i = 0; i < nodes.length; i += 1) { 108 | const node = nodes[i]; 109 | if (typeof node !== "string") { 110 | continue; // eslint-disable-line no-continue 111 | } 112 | 113 | // Empty tags, e.g., `[foo/]`. 114 | let aux = node.split(`[${key}/]`); 115 | if (aux.length > 1) { 116 | aux = spreadElementsInBetweenItems(aux, element); 117 | replaceNode(aux, i); 118 | continue; // eslint-disable-line no-continue 119 | } 120 | 121 | // Start-end tags, e.g., `[foo]content[/foo]`. 122 | const regexp = new RegExp(`\\[${key}\\][\\s\\S]*?\\[\\/${key}\\]`, "g"); 123 | const regexp2 = new RegExp(`\\[${key}\\]([\\s\\S]*?)\\[\\/${key}\\]`); 124 | aux = node.split(regexp); 125 | if (aux.length > 1) { 126 | const contents = node.match(regexp).map(content => content.replace(regexp2, "$1")); 127 | aux = spreadElementsInBetweenItems( 128 | aux, 129 | idx => React.cloneElement(element, {}, contents[idx]), 130 | ); 131 | replaceNode(aux, i); 132 | } 133 | } 134 | 135 | return nodes; 136 | }, [format]); 137 | } 138 | 139 | 140 | // Elements replacement. 141 | if (elements) { 142 | formatted = _replaceElements(formatted, elements); 143 | } 144 | 145 | return formatted; 146 | } 147 | 148 | function sanitizePath(pathString) { 149 | return pathString.trim().replace(/\{/g, "(").replace(/\}/g, ")").replace(/\//g, "|").replace(/\n/g, " ").replace(/ +/g, " ").replace(/"/g, "'"); 150 | } 151 | 152 | // Overload Globalize's `.formatMessage` to allow default message. 153 | var globalizeMessageFormatter = Globalize.messageFormatter; 154 | Globalize.messageFormatter = Globalize.prototype.messageFormatter = function(pathOrMessage) { 155 | var aux = {}; 156 | var sanitizedPath = sanitizePath(pathOrMessage); 157 | 158 | // Globalize runtime 159 | if (!this.cldr) { 160 | // On runtime, the only way for deciding between using sanitizedPath or 161 | // pathOrMessage as path is by checking which formatter exists. 162 | arguments[0] = sanitizedPath; 163 | aux = globalizeMessageFormatter.apply(this, arguments); 164 | arguments[0] = pathOrMessage; 165 | return aux || globalizeMessageFormatter.apply(this, arguments); 166 | } 167 | 168 | var sanitizedPathExists = this.cldr.get(["globalize-messages/{bundle}", sanitizedPath]) !== undefined; 169 | var pathExists = this.cldr.get(["globalize-messages/{bundle}", pathOrMessage]) !== undefined; 170 | 171 | // Want to distinguish between default message and path value - just checking 172 | // for sanitizedPath won't be enough, because sanitizedPath !== pathOrMessage 173 | // for paths like "salutations/hi". 174 | if (!sanitizedPathExists && !pathExists) { 175 | aux[this.cldr.attributes.bundle] = {}; 176 | aux[this.cldr.attributes.bundle][sanitizedPath] = pathOrMessage; 177 | Globalize.loadMessages(aux); 178 | sanitizedPathExists = true; 179 | } 180 | 181 | arguments[0] = sanitizedPathExists ? sanitizedPath : pathOrMessage; 182 | return globalizeMessageFormatter.apply(this, arguments); 183 | }; 184 | 185 | export default generator("formatMessage", ["path", "variables"], { 186 | beforeFormat: function(props) { 187 | messageSetup(this.globalize, props, this.globalizePropValues); 188 | }, 189 | afterFormat: function(props, formattedValue) { 190 | return replaceElements(props, formattedValue); 191 | } 192 | }); 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-globalize 2 | 3 | [React](http://facebook.github.io/react/) components that provide internationalization features via [Globalize](https://github.com/jquery/globalize). With a little initialization, you get instantly internationalized values in your application. 4 | 5 | ## React versions 6 | 7 | | react-globalize | react | 8 | | --- | --- | 9 | | 0.x | ^0.14.0, ^0.14.0, ^15.0.0 | 10 | | 1.x | ^16.0.0 | 11 | 12 | ## Install 13 | 14 | 1. `npm install react-globalize --save` 15 | 2. In your application just: 16 | ```JS 17 | var ReactGlobalize = require("react-globalize"); 18 | var Globalize = require("globalize"); 19 | var FormatCurrency = ReactGlobalize.FormatCurrency; 20 | 21 | // Initialize Globalize and load your CLDR data 22 | // See https://github.com/jquery/globalize for details on Globalize usage 23 | 24 | Globalize.locale("en"); 25 | 26 | // Then, to use any ReactGlobalize component (with JSX) 27 | React.render( 28 | {150} 29 | ); 30 | // Which would render for example: 31 | // $150.00 when using the `en` (English) locale, or 32 | // 150,00 $ when using the `de` (German) locale, or 33 | // US$150,00 when using the `pt` (Portuguese) locale, or 34 | // US$ 150.00 when using the `zh` (Chinese) locale, or 35 | // US$ ١٥٠٫٠٠ when using the `ar` (Arabic) locale. 36 | ``` 37 | 38 | 3. Further info about each component is available below. 39 | 40 | ## Components 41 | 42 | These components provide a simple way to display things like currency, dates, numbers and messages, formatted or translated to the current locale set by your application. Each component has a set of props, both required and optional. The component then uses the values of those props to properly format the passed values. Below is a listing of each component, its props and a usage example. 43 | 44 | ### FormatCurrency 45 | 46 | It allows to format a currency. Your code can be completely independent of the locale conventions for which currency symbol to use, whether or not there's a space between the currency symbol and the value, the side where the currency symbol must be placed, or even decimal digits used by particular currencies. Currencies can be displayed using symbols (the default), accounting form, 3-letter code, or plural messages. See [currencyFormatter][] docs in Globalize for more information. 47 | 48 | [currencyFormatter]: https://github.com/jquery/globalize/blob/master/doc/api/currency/currency-formatter.md 49 | 50 | #### Children 51 | 52 | The numeric value to be formatted. Required. 53 | 54 | #### Props 55 | 56 | - **currency** - required 57 | - A 3-letter string currency code as defined by ISO 4217 (e.g., USD, EUR, CNY, etc). 58 | - **options** 59 | - An optional set of options to further format the value. See the [currencyFormatter][] docs in Globalize for more info on specific options 60 | - **locale** - optional 61 | - A string value representing the locale (as defined by BCP47) used to override the default locale (preferred) set by your application using `Globalize.locale(locale)` when formatting the amount. 62 | 63 | #### Usage 64 | 65 | Default format with USD. 66 | 67 | ```js 68 | {150} 69 | // Which would render: 70 | // $150.00 when using the `en` (English) locale, or 71 | // US$150,00 when using the `pt` (Portuguese) locale. 72 | ``` 73 | 74 | Accounting format with EUR. 75 | 76 | ```js 77 | 78 | {-150} 79 | 80 | // Which would render: 81 | // (€150.00) when using the `en` (English) locale, or 82 | // (€150,00) when using the `pt` (Portuguese) locale. 83 | ``` 84 | 85 | ### FormatDate 86 | 87 | It allows to convert dates and times from their internal representations to textual form in a language-independent manner. Your code can conveniently control the length of the formatted date, time, datetime. See [dateFormatter][] docs in Globalize for more information. 88 | 89 | [dateFormatter]: https://github.com/jquery/globalize/blob/master/doc/api/date/date-formatter.md 90 | 91 | #### Children 92 | 93 | The date object to be formatted. Required. 94 | 95 | #### Props 96 | 97 | - **options** 98 | - An optional set of options which defines how to format the date. See the [dateFormatter][] docs in Globalize for more info on supported patterns 99 | - **locale** - optional 100 | - A string value representing the locale (as defined by BCP47) used to override the default locale (preferred) set by your application using `Globalize.locale(locale)` when formatting the amount. 101 | 102 | #### Usage 103 | 104 | Simple string skeleton. 105 | 106 | ```js 107 | 108 | {new Date()} 109 | 110 | // Which would render: 111 | // Feb 27, 2015 AD when using the `en` (English) locale, or 112 | // 27 de fev de 2015 d.C. when using the `pt` (Portuguese) locale. 113 | ``` 114 | 115 | Medium length date and time. 116 | 117 | ```js 118 | 119 | {new Date()} 120 | 121 | // Which would render: 122 | // Feb 27, 2015, 11:17:10 AM when using the `en` (English) locale, or 123 | // 27 de fev de 2015 11:17:10 when using the `pt` (Portuguese) locale. 124 | ``` 125 | 126 | ### FormatMessage 127 | 128 | It allows for the creation of internationalized messages (as defined by the [ICU Message syntax][]), with optional arguments (variables/placeholders) allowing for simple replacement, gender and plural inflections. The arguments can occur in any order, which is necessary for translation into languages with different grammars. See [messageFormatter][] docs in Globalize for more information. 129 | 130 | [messageFormatter]: https://github.com/jquery/globalize/blob/master/doc/api/message/message-formatter.md 131 | [ICU Message syntax]: http://userguide.icu-project.org/formatparse/messages 132 | 133 | #### Children 134 | 135 | Required unless the `path` property is set. It's a string with the default message. Either this or the `path` property is required. 136 | 137 | #### Props 138 | 139 | - **path** - required unless children is set 140 | - String or array path to traverse a set of messages store in JSON format. Defaults to the message itself defined by the children. 141 | - **variables** - optional 142 | - An array (where variables are represented as indeces) or object (for named variables) which contains values for variable replacement within a message. 143 | - **elements** - optional 144 | - An object (where element names are represented as keys, and its corresponding element as values) which contains elements replacement within a message. 145 | - **locale** - optional 146 | - A string value representing the locale (as defined by BCP47) used to override the default locale (preferred) set by your application using `Globalize.locale(locale)` when formatting the amount. 147 | 148 | #### Usage 149 | 150 | Below translation message JSON used in these examples: 151 | ```js 152 | { 153 | "pt": { 154 | "Hi": "Oi", 155 | "Hi, {0} {1} {2}": "Olá, {0} {1} {2}", 156 | "Hello, {first} {middle} {last}": "Ei, {first} {middle} {last}" 157 | } 158 | } 159 | ``` 160 | 161 | Simple salutation. 162 | 163 | ```js 164 | Hi 165 | // Which would render: 166 | // Hi when using the default message, in this case `en` (English). 167 | // Oi when using the `pt` (Portuguese) locale and its translation messages. 168 | ``` 169 | 170 | Variable Replacement. 171 | 172 | ```js 173 | // Using Array. 174 | 175 | {"Hi, {0} {1} {2}"} 176 | 177 | // Which would render: 178 | // Hello, Wolfgang Amadeus Mozart when using the default message, in this case `en` (English). 179 | // Hello, Wolfgang Amadeus Mozart when using the `en` (English) locale and its translation messages. 180 | 181 | // Using Object. 182 | 183 | {"Hey, {first} {middle} {last}"} 184 | 185 | // Which would render: 186 | // Hey, Wolfgang Amadeus Mozart when using the default message, in this case `en` (English). 187 | // Ei, Wolfgang Amadeus Mozart when using the `pt` (Portuguese) locale and its translation messages. 188 | ``` 189 | 190 | Element Replacement. 191 | 192 | ```js 193 | 196 | }} 197 | > 198 | For more information, see [reactGlobalizeLink]React Globalize[/reactGlobalizeLink] 199 | 200 | // Which would render: 201 | // 202 | // For more information, see 203 | // React Globalize 204 | // 205 | // when using the default message, in this case `en` (English). 206 | ``` 207 | 208 | See [messageFormatter][] docs in Globalize for more message examples (e.g., pluralization or gender selection). 209 | 210 | ### FormatNumber 211 | 212 | It allows to convert numbers into textual representations. Your code can be completely independent of the locale conventions for decimal points, thousands-separators, or even the particular decimal digits used, or whether the number format is even decimal. Though, it can still conveniently control various aspects of the formatted number like the minimum and maximum fraction digits, integer padding, rounding method, display as percentage, and others. See [numberFormatter][] docs in Globalize for more information. 213 | 214 | [numberFormatter]: https://github.com/jquery/globalize/blob/master/doc/api/number/number-formatter.md 215 | 216 | #### Children 217 | 218 | The number to be formatted. Required. 219 | 220 | #### Props 221 | 222 | - **options** 223 | - An optional set of options to further format the value. See the [numberFormatter][] docs in Globalize for more info on specific options 224 | - **locale** - optional 225 | - A string value representing the locale (as defined by BCP47) used to override the default locale (preferred) set by your application using `Globalize.locale(locale)` when formatting the amount. 226 | 227 | #### Usage 228 | 229 | Default format pi. 230 | 231 | ```js 232 | {Math.PI} 233 | // Which would render: 234 | // 3.142 when using the `en` (English) locale, or 235 | // 3,142 when using the `pt` (Portuguese) locale. 236 | ``` 237 | 238 | Show at least 2 decimal places. 239 | 240 | ```js 241 | 242 | {10000} 243 | 244 | // Which would render: 245 | // 10,000.00 when using the `en` (English) locale, or 246 | // 10.000,00 when using the `pt` (Portuguese) locale. 247 | ``` 248 | 249 | ## Development 250 | 251 | ### Testing 252 | 253 | ``` 254 | npm test 255 | ``` 256 | 257 | ### Release process 258 | 259 | Update package.json version, commit, and merge it into `master`. 260 | 261 | On master, run: 262 | 263 | ``` 264 | VER= # e.g., "1.0.1" 265 | git checkout --detach && 266 | npm run build && 267 | git add -f dist/* && 268 | git commit -a -m Build && 269 | git tag -a -m v$VER v$VER 270 | ``` 271 | 272 | Verify the tag and: 273 | 274 | ``` 275 | git push --tags origin && 276 | npm publish 277 | ``` 278 | 279 | ## License 280 | 281 | This project is distributed under the [MIT license](https://www.tldrlegal.com/l/mit). 282 | --------------------------------------------------------------------------------