├── .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 | [![Build Status](https://travis-ci.org/infinum/react-mobx-translatable.svg?branch=master)](https://travis-ci.org/infinum/react-mobx-translatable) 8 | [![Dependency Status](https://david-dm.org/infinum/react-mobx-translatable.svg)](https://david-dm.org/infinum/react-mobx-translatable) 9 | [![devDependency Status](https://david-dm.org/infinum/react-mobx-translatable/dev-status.svg)](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 | --------------------------------------------------------------------------------