├── .babelrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── index.js
├── package.json
└── test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6.3"
4 | install:
5 | - npm install
6 | script:
7 | - npm test
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Infinum
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-mobx-translatable
2 |
3 | Make React components translatable using MobX. Can be used both on the server (SSR) and in the browser.
4 |
5 | Note: This plugin depends on ``mobx-react`` features that are currently marked as experimental: ``Provider`` and ``inject``.
6 |
7 | [](https://travis-ci.org/infinum/react-mobx-translatable)
8 | [](https://david-dm.org/infinum/react-mobx-translatable)
9 | [](https://david-dm.org/infinum/react-mobx-translatable#info=devDependencies)
10 |
11 | ## Installation
12 |
13 | ```Bash
14 | npm install react-mobx-translatable
15 | ```
16 |
17 | ## Usage
18 |
19 | ### Checklist (see the example for more details)
20 |
21 | * Setup the i18n object in store
22 | * Initialize the [i18n-harmony lib](https://github.com/DarkoKukovec/i18n-harmony) and translatable
23 | * Wrap your components with ``Provider`` component
24 | * Set the ``@translatable`` decorator on your components
25 | * Call ``this.t(translationKey, options)`` from the component
26 | * To change the language, change the locale variable in the store
27 |
28 | ### Methods
29 |
30 | #### init(injectFn)
31 |
32 | Method receives ``injectFn`` - function that maps the i18n object from the store
33 | * The function receives the whole store object (from ``Provider``)
34 | * The function should return an object that contains an ``i18n`` key with the i18n object from store (see the example)
35 | * Default: ``(store) => {i18n: i18n.store}``
36 | * If you have the i18n object in the root of the store (the default function can map the value), you don't need to call ``init``
37 |
38 | #### translatable(Component|String[])
39 |
40 | Method can receive either an array of strings or a React component.
41 |
42 | * Array of strings - Used for connecting the store from ``Provider`` to the component. Translatable is returning a new function that accepts a React component.
43 | * React component - The function will wrap the passed component
44 |
45 | In both cases, the wrapped component will also be an observer. If using with other decorators, ``translatable`` should be the innermost one.
46 |
47 | ## Example
48 |
49 | The example assumes you're using the following:
50 | * [ES2015](https://babeljs.io/docs/plugins/preset-es2015/)
51 | * [object rest spread](http://babeljs.io/docs/plugins/transform-object-rest-spread/)
52 | * [decorators](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy)
53 |
54 | This is however not a requirement.
55 |
56 | ### Initialize store, i18n, and translatable
57 |
58 | ```JavaScript
59 | import {observable} from 'mobx';
60 | import i18n from 'i18n-harmony';
61 | import {init} from 'translatable';
62 |
63 | const defaultLocale = 'en'; // Can be based on browser language or user selection (localStorage, cookies)
64 |
65 | const store = {
66 | i18n: observable({locale: defaultLocale})
67 | };
68 |
69 | // For details, see i18n-harmony: https://github.com/DarkoKukovec/i18n-harmony
70 | i18n.init({
71 | translations: {
72 | en: {hello: 'Hello world!'}
73 | }
74 | });
75 |
76 | init((store) => ({i18n: store.i18n}));
77 | ```
78 |
79 | ### Wrap your React components inside of the ``Provider`` component and pass it the store
80 |
81 | ```JavaScript
82 | import {Provider} from 'mobx-react';
83 | import store from './store';
84 |
85 | ReactDOM.render(
86 |
87 | , document.getElementById('app'));
88 | ```
89 |
90 | ### Translatable component
91 |
92 | ``` JavaScript
93 | import {Component} from 'react';
94 | import {translatable} from 'translatable';
95 |
96 | @translatable
97 | export default class MyComponent extends Component {
98 | render() {
99 | return
{this.t('hello')}
100 | }
101 | }
102 | ```
103 |
104 | ### `has` method
105 |
106 | ``` JavaScript
107 | import {Component} from 'react';
108 | import {translatable} from 'translatable';
109 |
110 | @translatable
111 | export default class MyComponent extends Component {
112 | render() {
113 | return {this.has('hello') && this.t('hello')}
114 | }
115 | }
116 | ```
117 |
118 | ## Changelog
119 |
120 | ### v1.2.0
121 |
122 | * Expose `has` from [i18n-harmony lib](https://github.com/DarkoKukovec/i18n-harmony)
123 |
124 | ### v1.1.0
125 |
126 | * Add ability to connect the store with the component
127 |
128 | ### v1.0.0
129 |
130 | * Initial release
131 |
132 | ## License
133 | [MIT License](LICENSE)
134 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var mobxReact = require('mobx-react');
3 | var I18n = require('i18n-harmony');
4 |
5 | function defaultInjectFn(stores) {
6 | return {
7 | i18n: stores.i18n
8 | };
9 | }
10 |
11 | var config = {
12 | injectFn: defaultInjectFn
13 | };
14 |
15 | function init(injectFn) {
16 | config.injectFn = injectFn || defaultInjectFn;
17 | }
18 |
19 | /**
20 | * Wrap a React Component in order to make it translatable (decorator)
21 | *
22 | * @param {Component} Component - Component to be wrapped
23 | * @param {String[]} [connect=undefined] - Connect store to the component
24 | * @return {Component} Wrapped component
25 | */
26 | function translatable(Component, connect) {
27 |
28 | /* istanbul ignore if */
29 | if (Component.propTypes) {
30 | Component.propTypes.i18n = React.PropTypes.object;
31 | } else {
32 | Component.propTypes = {
33 | i18n: React.PropTypes.object
34 | };
35 | }
36 |
37 | Component.prototype.t = function(key, opts) {
38 | var i18n = this.props.i18n;
39 |
40 | // Third argument is the locale that should be used
41 | // We can't use the active one because of the SSR and shared I18n state
42 | // Also, we need to observe i18n.locale to detect changes, so this is useful
43 | return I18n.t(key, opts, i18n.locale);
44 | };
45 |
46 | Component.prototype.has = function(key, opts, includeDefault = false) {
47 | var i18n = this.props.i18n;
48 |
49 | // Forth argument is the locale that should be used
50 | // We can't use the active one because of the SSR and shared I18n state
51 | // Also, we need to observe i18n.locale to detect changes, so this is useful
52 | return I18n.has(key, opts, includeDefault, i18n.locale);
53 | };
54 |
55 | var observed = connect
56 | ? mobxReact.observer(connect)(Component)
57 | : mobxReact.observer(Component);
58 | return mobxReact.inject(config.injectFn)(observed);
59 | }
60 |
61 | module.exports = {
62 | init: init,
63 | translatable: function(arg) {
64 | if (arg instanceof Array) {
65 | return function(Component) {
66 | return translatable(Component, arg);
67 | }
68 | }
69 | return translatable(arg);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mobx-translatable",
3 | "version": "1.2.0",
4 | "description": "Make React components translatable using MobX",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha test --compilers js:babel-register",
8 | "precommit": "npm test"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/infinum/react-mobx-translatable.git"
13 | },
14 | "keywords": [
15 | "react",
16 | "i18n",
17 | "mobx"
18 | ],
19 | "author": "Infinum ",
20 | "contributors": [
21 | {
22 | "name": "Darko Kukovec",
23 | "email": "darko@infinum.co"
24 | }
25 | ],
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/infinum/react-mobx-translatable/issues"
29 | },
30 | "homepage": "https://github.com/infinum/react-mobx-translatable#readme",
31 | "devDependencies": {
32 | "babel-preset-react": "^6.11.1",
33 | "babel-register": "^6.11.6",
34 | "chai": "^3.5.0",
35 | "enzyme": "^2.4.1",
36 | "husky": "^0.13.0",
37 | "jsdom": "^9.4.1",
38 | "mobx": "^3.0.1",
39 | "mocha": "^3.0.1",
40 | "react-addons-test-utils": "^15.3.0",
41 | "react-dom": "^15.3.0"
42 | },
43 | "dependencies": {
44 | "i18n-harmony": "^1.4.0",
45 | "mobx-react": "^4.0.0",
46 | "react": "^15.3.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai');
2 | const {describe, it} = require('mocha');
3 | const {mount} = require('enzyme');
4 |
5 | const React = require('react');
6 | const {Provider} = require('mobx-react');
7 | const {observable} = require('mobx');
8 | const i18n = require('i18n-harmony');
9 | const {jsdom} = require('jsdom');
10 |
11 | const {init, translatable} = require('./index');
12 |
13 | const exposedProperties = ['window', 'navigator', 'document'];
14 |
15 | function initMockDOM() {
16 | global.document = jsdom('');
17 | global.window = document.defaultView;
18 | Object.keys(document.defaultView).forEach((property) => {
19 | if (typeof global[property] === 'undefined') {
20 | exposedProperties.push(property);
21 | global[property] = document.defaultView[property];
22 | }
23 | });
24 |
25 | global.navigator = {
26 | userAgent: 'node.js'
27 | };
28 | }
29 |
30 | describe('react-mobx-translatable', function() {
31 | beforeEach(function() {
32 | i18n.init({
33 | translations: {
34 | en: {hello: 'Hello'},
35 | de: {hello: 'Hallo'}
36 | }
37 | });
38 | initMockDOM();
39 | });
40 |
41 | it('should work with default configuration', function() {
42 | const store = {
43 | i18n: observable({locale: 'en'})
44 | };
45 | init();
46 |
47 | class MyComponent extends React.Component {
48 | render() {
49 | return {this.t('hello')}
;
50 | }
51 | }
52 | const MyWrappedComponent = translatable(MyComponent);
53 |
54 | const wrapper = mount();
55 |
56 | expect(wrapper.find('div').first().text()).to.equal('Hello');
57 |
58 | store.i18n.locale = 'de';
59 | expect(wrapper.find('div').first().text()).to.equal('Hallo');
60 | });
61 |
62 | it('should work with custom store configuration', function() {
63 | const store = {
64 | ui: {
65 | i18n: observable({locale: 'en'})
66 | }
67 | };
68 | init((store) => ({i18n: store.ui.i18n}));
69 |
70 | class MyComponent extends React.Component {
71 | render() {
72 | return {this.t('hello')}
;
73 | }
74 | }
75 | const MyWrappedComponent = translatable(MyComponent);
76 |
77 | const wrapper = mount();
78 |
79 | expect(wrapper.find('div').first().text()).to.equal('Hello');
80 |
81 | store.ui.i18n.locale = 'de';
82 | expect(wrapper.find('div').first().text()).to.equal('Hallo');
83 | });
84 |
85 | it('should work with custom connectors', function() {
86 | const store = {
87 | data: observable({
88 | foo: 1
89 | }),
90 | ui: {
91 | i18n: observable({locale: 'en'})
92 | }
93 | };
94 | init((store) => ({i18n: store.ui.i18n}));
95 |
96 | class MyComponent extends React.Component {
97 | render() {
98 | expect(this.props.data).to.be.an('object');
99 |
100 | return {this.t('hello')}
;
101 | }
102 | }
103 | MyComponent.propTypes = {
104 | data: React.PropTypes.object
105 | };
106 | const MyWrappedComponent = translatable(['data'])(MyComponent);
107 |
108 | const wrapper = mount();
109 |
110 | expect(wrapper.find('div').first().text()).to.equal('Hello');
111 |
112 | store.ui.i18n.locale = 'de';
113 | expect(wrapper.find('div').first().text()).to.equal('Hallo');
114 | });
115 |
116 | it('has should return if translation exists', function() {
117 | let helloExists = false;
118 | const store = {
119 | i18n: observable({locale: 'en'})
120 | };
121 | init();
122 |
123 | class MyComponent extends React.Component {
124 | componentWillMount() {
125 | const existsKey = this.has('hello');
126 | const doesntExistsKey = this.has('unknown-key');
127 |
128 | expect(existsKey).to.be.eq(true);
129 | expect(doesntExistsKey).to.be.eq(false);
130 | }
131 |
132 | render() {
133 | return {this.t('hello')}
;
134 | }
135 | }
136 | const MyWrappedComponent = translatable(MyComponent);
137 |
138 | const wrapper = mount();
139 | });
140 | });
141 |
--------------------------------------------------------------------------------