├── translate.js ├── src ├── constants.js ├── constants.spec.js ├── actions.js ├── reducer.js ├── private │ ├── utils.js │ └── test │ │ └── boot.js ├── index.js ├── __snapshots__ │ └── middleware.spec.js.snap ├── index.spec.js ├── actions.spec.js ├── reducer.spec.js ├── translate.js ├── middleware.js ├── selectors.spec.js ├── selectors.js ├── middleware.spec.js └── translate.spec.js ├── .babelrc ├── .npmignore ├── scripts └── prepublish.js ├── CONTRIBUTING.md ├── LICENSE ├── .gitignore ├── .eslintrc ├── package.json └── README.md /translate.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/translate'); 2 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const SET_LANGUAGE = '@@polyglot/SET_LANGUAGE'; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc 2 | .babelrc 3 | CONTRIBUTING.md 4 | webpack.config.js 5 | scripts 6 | *.spec.js 7 | coverage 8 | src 9 | -------------------------------------------------------------------------------- /src/constants.spec.js: -------------------------------------------------------------------------------- 1 | import { SET_LANGUAGE } from './constants'; 2 | 3 | describe('constants', () => { 4 | it('defines SET_LANGUAGE', () => { expect(SET_LANGUAGE).toBeDefined(); }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { SET_LANGUAGE } from './constants'; 2 | 3 | export const setLanguage = (locale, phrases) => ({ 4 | type: SET_LANGUAGE, 5 | payload: { 6 | locale, 7 | phrases, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { SET_LANGUAGE } from './constants'; 2 | 3 | const initialState = { 4 | locale: null, 5 | phrases: null, 6 | }; 7 | 8 | export const polyglotReducer = (state = initialState, action) => ( 9 | action.type === SET_LANGUAGE ? action.payload : state 10 | ); 11 | -------------------------------------------------------------------------------- /src/private/utils.js: -------------------------------------------------------------------------------- 1 | 2 | export const identity = x => x; 3 | // eslint-disable-next-line valid-typeof 4 | export const is = type => x => typeof x === type; 5 | export const isString = is('string'); 6 | export const isFunction = is('function'); 7 | export const isObject = is('object'); 8 | export const { isArray } = Array; 9 | -------------------------------------------------------------------------------- /src/private/test/boot.js: -------------------------------------------------------------------------------- 1 | const toBe = (type) => (received) => { 2 | // eslint-disable-next-line valid-typeof 3 | const isFunction = typeof received === type; 4 | return { 5 | pass: isFunction, 6 | message: `expected ${received}${isFunction ? 'not' : ''} to be a function`, 7 | }; 8 | }; 9 | 10 | expect.extend({ 11 | toBeFunction: toBe('function'), 12 | toBeString: toBe('string'), 13 | }); 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const PropType = PropTypes.shape({ 4 | t: PropTypes.func.isRequired, 5 | tc: PropTypes.func.isRequired, 6 | tt: PropTypes.func.isRequired, 7 | tu: PropTypes.func.isRequired, 8 | tm: PropTypes.func.isRequired, 9 | }); 10 | 11 | export * from './middleware'; 12 | export * from './actions'; 13 | export * from './constants'; 14 | export * from './reducer'; 15 | export * from './selectors'; 16 | export { PropType }; 17 | -------------------------------------------------------------------------------- /scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-undef */ 3 | require('shelljs/global'); 4 | 5 | console.info('--- PREPUBLISH ...'); 6 | const noError = result => result.code === 0; 7 | 8 | const execOrDie = (cmd, text) => (noError(exec(cmd)) ? console.info(text) : exit(-1)); 9 | 10 | execOrDie('npm run -s clean', '--- Clean OK ---'); 11 | execOrDie('npm run -s build', '--- Build OK ---'); 12 | execOrDie('npm run -s test', '--- Tests OK ---'); 13 | execOrDie('npm run -s lint', '--- Lint OK ---'); 14 | echo('... Prepublish OK ---'); 15 | -------------------------------------------------------------------------------- /src/__snapshots__/middleware.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`middleware doesn't impact the store when action is unknown. 1`] = ` 4 | Array [ 5 | Object { 6 | "type": "UNCATCHED_ACTION", 7 | }, 8 | ] 9 | `; 10 | 11 | exports[`middleware impacts the store when action is CATCHED_ACTION. 1`] = ` 12 | Array [ 13 | Object { 14 | "payload": Object { 15 | "locale": "en", 16 | }, 17 | "type": "CATCHED_ACTION", 18 | }, 19 | Object { 20 | "payload": Object { 21 | "locale": "en", 22 | "phrases": Object { 23 | "hello": "hello", 24 | }, 25 | }, 26 | "type": "@@polyglot/SET_LANGUAGE", 27 | }, 28 | ] 29 | `; 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork the project and clone your fork. 4 | 5 | 2. Create a local feature branch: 6 | 7 | $ git checkout -b 8 | 9 | 3. Make one or more atomic commits. Do not commit changes to 10 | __dist/*__. 11 | 12 | 4. Run `npm test` and address any errors. It will install 13 | needed dependencies locally. Preferably, fix commits in place using `git 14 | rebase` or `git commit --amend` to make the changes easier to review and to 15 | keep the history tidy. 16 | 17 | 5. Push to your fork: 18 | 19 | $ git push origin 20 | 21 | 6. Open a pull request. 22 | 23 | 24 | 25 | ---------------- 26 | 27 | Please read semver.org 28 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as all from './'; 2 | import translate from './translate'; 3 | 4 | describe('index', () => { 5 | it('exports middleware', () => { 6 | expect(all.createPolyglotMiddleware).toBeFunction(); 7 | }); 8 | it('exports SET_LANGUAGE action', () => { 9 | expect(all.SET_LANGUAGE).toBeString(); 10 | }); 11 | it('exports setLanguage action creator', () => { 12 | expect(all.setLanguage).toBeFunction(); 13 | }); 14 | it('exports reducer', () => { 15 | expect(all.polyglotReducer).toBeFunction(); 16 | }); 17 | it('exports selectors', () => { 18 | expect(all.getP).toBeFunction(); 19 | expect(all.getLocale).toBeFunction(); 20 | }); 21 | it('exports translate enhancer', () => { 22 | expect(translate).toBeFunction(); 23 | }); 24 | it('exports p PropTypes', () => { 25 | expect(all.PropType).toBeFunction(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/actions.spec.js: -------------------------------------------------------------------------------- 1 | import { setLanguage } from './actions'; 2 | import { SET_LANGUAGE } from './constants'; 3 | 4 | describe('actions', () => { 5 | describe('setLanguage', () => { 6 | it('returns a SET_LANGUAGE without locale/phrases if not given', () => { 7 | const actionWithoutLocaleAndPhrases = setLanguage(); 8 | 9 | expect(actionWithoutLocaleAndPhrases).toBeDefined(); 10 | expect(actionWithoutLocaleAndPhrases).toEqual({ 11 | type: SET_LANGUAGE, 12 | payload: {}, 13 | }); 14 | }); 15 | it('returns a SET_LANGUAGE with locale/phrases', () => { 16 | const actionWithoutLocaleAndPhrases = setLanguage('yolo', 42); 17 | 18 | expect(actionWithoutLocaleAndPhrases).toBeDefined(); 19 | expect(actionWithoutLocaleAndPhrases).toEqual({ 20 | type: SET_LANGUAGE, 21 | payload: { 22 | locale: 'yolo', 23 | phrases: 42, 24 | }, 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/reducer.spec.js: -------------------------------------------------------------------------------- 1 | import { polyglotReducer } from './reducer'; 2 | import { SET_LANGUAGE } from './constants'; 3 | 4 | const unknownAction = { 5 | type: 'UNKNOWN_ACTION', 6 | payload: true, 7 | }; 8 | 9 | const setLanguageAction = { 10 | type: SET_LANGUAGE, 11 | payload: { 12 | locale: 'yolo', 13 | phrases: true, 14 | }, 15 | }; 16 | 17 | // check if an unknown action impact the state 18 | const checkUnknownAction = state => expect(state).toEqual(polyglotReducer(state, unknownAction)); 19 | 20 | describe('reducer', () => { 21 | let state = { 22 | locale: null, 23 | phrases: null, 24 | }; 25 | 26 | beforeEach(() => checkUnknownAction(state)); 27 | 28 | it('doesn\'t update the state when action is unknown', () => { 29 | state = polyglotReducer(state, unknownAction); 30 | }); 31 | 32 | it('updates the state when action is SET_LANGUAGE', () => { 33 | state = polyglotReducer(state, setLanguageAction); 34 | expect(state).toEqual(setLanguageAction.payload); 35 | }); 36 | 37 | afterEach(() => checkUnknownAction(state)); 38 | }); 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Guillaume ARM 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 | -------------------------------------------------------------------------------- /src/translate.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash.curry'; 2 | import { connect } from 'react-redux'; 3 | import { createGetP } from './selectors'; 4 | import { isFunction, isString, isObject } from './private/utils'; 5 | 6 | const getDisplayName = Component => ( 7 | Component.displayName || Component.name || 'Component' 8 | ); 9 | 10 | const mapPolyglotToProps = (options) => () => { 11 | const getP = createGetP(); 12 | 13 | return state => ({ 14 | p: getP(state, options), 15 | }); 16 | }; 17 | 18 | const translateEnhancer = curry((polyglotScope, Component) => { 19 | const Connected = connect(mapPolyglotToProps(polyglotScope), () => ({}))(Component); 20 | Connected.displayName = `Translated(${getDisplayName(Connected.WrappedComponent)})`; 21 | return Connected; 22 | }); 23 | 24 | const translate = (fstArg, sndArg) => { 25 | if (fstArg === undefined && sndArg === undefined) 26 | return translateEnhancer({}); 27 | 28 | else if (isFunction(fstArg)) 29 | return translateEnhancer({}, fstArg); 30 | 31 | else if (isString(fstArg) && sndArg === undefined) 32 | return translateEnhancer({ polyglotScope: fstArg }); 33 | 34 | else if (isObject(fstArg) && sndArg === undefined) 35 | return translateEnhancer(fstArg); 36 | 37 | return translateEnhancer(fstArg, sndArg); 38 | }; 39 | 40 | export default translate; 41 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | import { setLanguage } from './actions'; 2 | import { isString, isFunction, isObject, isArray } from './private/utils'; 3 | 4 | const checkParams = (catchedAction, getLocale, getPhrases) => { 5 | if (!catchedAction || !getLocale || !getPhrases) 6 | throw (new Error('polyglotMiddleware : missing parameters.')); 7 | if (!isString(catchedAction) && !isArray(catchedAction)) { 8 | throw (new Error( 9 | 'polyglotMiddleware : first parameter must be a string or an array of string.' 10 | )); 11 | } 12 | if (!isFunction(getLocale)) 13 | throw (new Error('polyglotMiddleware : second parameter must be a function.')); 14 | if (!isFunction(getPhrases)) 15 | throw (new Error('polyglotMiddleware : third parameter must be a function.')); 16 | }; 17 | 18 | const getIsValidPolyglotReducer = state => !!(state && isObject(state.polyglot)); 19 | const checkState = state => { 20 | if (!getIsValidPolyglotReducer(state)) 21 | throw (new Error('polyglotReducer : need to be store in "state.polyglot"')); 22 | }; 23 | 24 | export const createPolyglotMiddleware = (catchedAction, getLocale, getPhrases) => { 25 | checkParams(catchedAction, getLocale, getPhrases); 26 | const actions = isArray(catchedAction) ? catchedAction : [catchedAction]; 27 | return ({ dispatch, getState }) => { 28 | checkState(getState()); 29 | return next => action => { 30 | if (actions.includes(action.type)) { 31 | const locale = getLocale(action); 32 | const nexted = next(action); 33 | return getPhrases(locale).then(phrases => { 34 | dispatch(setLanguage(locale, phrases)); 35 | return nexted; 36 | }); 37 | } 38 | return next(action); 39 | }; 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### 2 | dist/ 3 | 4 | ### Node template 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | ### JetBrains template 43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 44 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 45 | 46 | # User-specific stuff: 47 | .idea/workspace.xml 48 | .idea/tasks.xml 49 | .idea/dictionaries 50 | .idea/vcs.xml 51 | .idea/jsLibraryMappings.xml 52 | 53 | # Sensitive or high-churn files: 54 | .idea/dataSources.ids 55 | .idea/dataSources.xml 56 | .idea/dataSources.local.xml 57 | .idea/sqlDataSources.xml 58 | .idea/dynamic.xml 59 | .idea/uiDesigner.xml 60 | 61 | # Gradle: 62 | .idea/gradle.xml 63 | .idea/libraries 64 | 65 | # Mongo Explorer plugin: 66 | .idea/mongoSettings.xml 67 | 68 | ## File-based project format: 69 | *.iws 70 | 71 | ## Plugin-specific files: 72 | 73 | # IntelliJ 74 | /out/ 75 | 76 | # mpeltonen/sbt-idea plugin 77 | .idea_modules/ 78 | 79 | # JIRA plugin 80 | atlassian-ide-plugin.xml 81 | 82 | # Crashlytics plugin (for Android Studio and IntelliJ) 83 | com_crashlytics_export_strings.xml 84 | crashlytics.properties 85 | crashlytics-build.properties 86 | fabric.properties 87 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "mocha": true 5 | }, 6 | "parser": "babel-eslint", 7 | "plugins": ["react"], 8 | "extends": "airbnb", 9 | "globals": { 10 | "afterEach": false, 11 | "afterAll": false, 12 | "beforeEach": false, 13 | "beforeAll": false, 14 | "jest": false, 15 | "document": false, 16 | "window": false, 17 | "global": false, 18 | "require": false, 19 | "expect": false, 20 | "should": false, 21 | "chai": false, 22 | "sinon": false, 23 | "__DEVELOPMENT__": false 24 | }, 25 | "rules": { 26 | "max-len": ["error", 100, { "ignoreComments": true }], 27 | "comma-dangle": ["warn", "always-multiline"], // disallow or enforce trailing commas 28 | "curly": ["warn", "multi-or-nest"], // specify curly brace conventions for all control statements 29 | "indent": ["warn", 4, {"SwitchCase": 1}], // this option sets a specific tab width for your code (off by default) 30 | "brace-style": ["warn", "1tbs", { "allowSingleLine": true }], // enforce one true brace style (off by default) 31 | "key-spacing": ["warn", {"beforeColon": false, "afterColon": true}], // enforces spacing between keys and values in object literal properties 32 | "quotes": ["warn", "single", "avoid-escape"], // specify whether double or single quotes should be used 33 | "keyword-spacing": ["warn", {"before": true, "after": true}], // require a space after certain keywords (off by default) 34 | "linebreak-style": [0], 35 | "arrow-parens": [0], 36 | "new-cap": [0], 37 | "function-paren-newline": [0], 38 | "object-curly-newline": [0], 39 | // Import 40 | "import/no-extraneous-dependencies": [0], 41 | "import/prefer-default-export": [0], 42 | // React 43 | "react/require-extension": [0], 44 | "react/prop-types": [0], 45 | "react/jsx-filename-extension": [0], 46 | "react/jsx-indent": ["warn", 4], 47 | "react/jsx-indent-props": ["warn", 4] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-polyglot", 3 | "version": "0.7.0", 4 | "description": "Tool for using Polyglot.js with Redux", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir dist --ignore '*.spec.js'", 8 | "clean": "rimraf ./dist", 9 | "test": "jest --coverage", 10 | "test:all": "npm run -s prepublishOnly", 11 | "test:watch": "jest --watch --coverage", 12 | "tw": "npm run test:watch", 13 | "prepublishOnly": "node scripts/prepublish.js", 14 | "postpublish": "echo --- PUBLISHED ---", 15 | "lint": "eslint --max-warnings 0 src" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/Tiqa/redux-polyglot.git" 20 | }, 21 | "keywords": [ 22 | "polyglot", 23 | "redux", 24 | "react", 25 | "i18n", 26 | "reselect", 27 | "component enhancer", 28 | "translation" 29 | ], 30 | "maintainers": [ 31 | { 32 | "name": "Guillaume Arm", 33 | "email": "garm@student.42.fr" 34 | }, 35 | { 36 | "name": "Jalil Arfaoui", 37 | "email": "jalil@arfaoui.net" 38 | }, 39 | { 40 | "name": "Jérémy Vincent", 41 | "email": "jvincent@student.42.fr" 42 | } 43 | ], 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/Tiqa/redux-polyglot/issues" 47 | }, 48 | "homepage": "https://github.com/Tiqa/redux-polyglot", 49 | "jest": { 50 | "setupTestFrameworkScriptFile": "./src/private/test/boot.js", 51 | "moduleFileExtensions": [ 52 | "js", 53 | "jsx" 54 | ], 55 | "roots": [ 56 | "src" 57 | ], 58 | "coveragePathIgnorePatterns": [ 59 | "src/private/test/" 60 | ] 61 | }, 62 | "dependencies": { 63 | "lodash.assign": "^4.2.0", 64 | "lodash.curry": "^4.1.1", 65 | "node-polyglot": "^2.2.2", 66 | "redux": "^4.0.0", 67 | "reselect": "^3.0.1" 68 | }, 69 | "devDependencies": { 70 | "babel-cli": "^6.26.0", 71 | "babel-core": "^6.26.3", 72 | "babel-eslint": "^8.2.3", 73 | "babel-jest": "^22.4.3", 74 | "babel-plugin-transform-es2015-modules-umd": "^6.24.1", 75 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 76 | "babel-preset-env": "^1.6.1", 77 | "babel-preset-react": "^6.24.1", 78 | "babel-register": "^6.26.0", 79 | "eslint": "^4.19.1", 80 | "eslint-config-airbnb": "^16.1.0", 81 | "eslint-plugin-import": "^2.11.0", 82 | "eslint-plugin-jsx-a11y": "^6.0.3", 83 | "eslint-plugin-react": "^7.7.0", 84 | "jest": "^22.4.3", 85 | "prop-types": "^15.6.1", 86 | "ramda": "^0.25.0", 87 | "react": "16.3.2", 88 | "react-redux": "^5.0.7", 89 | "react-test-renderer": "^16.3.2", 90 | "redux-mock-store": "^1.5.1", 91 | "rimraf": "^2.6.2", 92 | "shelljs": "^0.8.1", 93 | "webpack": "^4.6.0" 94 | }, 95 | "peerDependencies": { 96 | "react": "^16.3.2", 97 | "react-redux": "^5.0.7", 98 | "prop-types": "^15.6.1" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/selectors.spec.js: -------------------------------------------------------------------------------- 1 | import { toUpper, evolve, is, pipe, values, all, equals, either, isNil } from 'ramda'; 2 | import { getP, getLocale } from './selectors'; 3 | 4 | const isValidPolyglot = pipe( 5 | evolve({ 6 | phrases: is(Object), 7 | currentLocale: is(String), 8 | onMissingKey: either(isNil, is(Boolean)), 9 | warn: is(Function), 10 | t: is(Function), 11 | tc: is(Function), 12 | tt: is(Function), 13 | tu: is(Function), 14 | tm: is(Function), 15 | }), 16 | values, 17 | all(equals(true)), 18 | ); 19 | 20 | const fakeState = { 21 | polyglot: { 22 | locale: 'fr', 23 | phrases: { test: { hello: 'bonjour', hello_world: 'bonjour monde' } }, 24 | }, 25 | }; 26 | 27 | describe('selectors', () => { 28 | describe('getLocale', () => { 29 | it('doesn\'t crash when state is an empty object', () => { 30 | expect(getLocale({})).toBe(undefined); 31 | }); 32 | 33 | it('doesn\'t crash when state is an empty object', () => { 34 | expect(getLocale(fakeState)).toBe('fr'); 35 | }); 36 | }); 37 | 38 | describe('getP', () => { 39 | const p = getP(fakeState, { polyglotScope: 'test' }); 40 | 41 | it('gives a valid redux-polyglot object', () => { 42 | expect(isValidPolyglot(p)).toBe(true); 43 | }); 44 | 45 | it('returns phrase key if no locale defined', () => { 46 | expect(getP({})).toBeDefined(); 47 | 48 | const emptyP = getP({}); 49 | 50 | expect(emptyP.t('a.path.to.translate')).toEqual('a.path.to.translate'); 51 | }); 52 | 53 | it('translates "hello" to "bonjour"', () => { 54 | expect(p.t('hello')).toBe('bonjour'); 55 | }); 56 | 57 | it('translates "hello" to "Bonjour" (capitalize)', () => { 58 | expect(p.tc('hello')).toBe('Bonjour'); 59 | }); 60 | 61 | it('translates "hello world" to "Bonjour Monde" (titleize)', () => { 62 | expect(p.tt('hello_world')).toBe('Bonjour Monde'); 63 | }); 64 | 65 | it('translates "hello" to "BONJOUR" (upper-case)', () => { 66 | expect(p.tu('hello')).toBe('BONJOUR'); 67 | }); 68 | 69 | it('translates "hello" to "BONJOUR" (morphed with upper-case function)', () => { 70 | expect(p.tm(toUpper)('hello')).toBe('BONJOUR'); 71 | }); 72 | 73 | it('translates when scope is not given', () => { 74 | expect(getP(fakeState).t('test.hello')).toBe('bonjour'); 75 | expect(getP(fakeState).tu('test.hello')).toBe('BONJOUR'); 76 | }); 77 | 78 | it('overwrite default scope translation "hello" to "Hi !"', () => { 79 | const p1 = getP(fakeState, { 80 | polyglotScope: 'test', 81 | ownPhrases: { 'test.hello': 'Hi !' }, 82 | }); 83 | expect(p1.tc('hello')).toBe('Hi !'); 84 | }); 85 | 86 | it('overwrite multiple default scope translations', () => { 87 | const p2 = getP(fakeState, { 88 | polyglotScope: 'test', 89 | ownPhrases: { 90 | 'test.hello': 'Hi !', 91 | 'test.hello_world': 'Hi WORLD !', 92 | }, 93 | }); 94 | 95 | expect(p2.tc('hello_world')).toBe('Hi WORLD !'); 96 | expect(p2.tc('hello')).toBe('Hi !'); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/selectors.js: -------------------------------------------------------------------------------- 1 | import assign from 'lodash.assign'; 2 | import { compose } from 'redux'; 3 | import { createSelector } from 'reselect'; 4 | import Polyglot from 'node-polyglot'; 5 | import { identity } from './private/utils'; 6 | 7 | const path = arrPath => obj => arrPath.reduce((cursor, key) => cursor && cursor[key], obj); 8 | const toUpper = str => str.toUpperCase(); 9 | const titleize = str => str.toLowerCase().replace(/(?:^|\s|-)\S/g, c => c.toUpperCase()); 10 | const adjustString = (f, index) => str => ( 11 | str.substr(0, index) + f(str[index]) + str.substr(index + 1) 12 | ); 13 | const capitalize = adjustString(toUpper, 0); 14 | 15 | const getLocale = path(['polyglot', 'locale']); 16 | const getPhrases = path(['polyglot', 'phrases']); 17 | 18 | const getPolyglotScope = (state, { polyglotScope = '' }) => ( 19 | (polyglotScope !== '') 20 | ? `${polyglotScope}.` 21 | : '' 22 | ); 23 | 24 | const getPolyglotOwnPhrases = (state, { ownPhrases = '' }) => ( 25 | (ownPhrases !== '') 26 | ? ownPhrases 27 | : '' 28 | ); 29 | 30 | const getPolyglotOptions = (state, { polyglotOptions }) => polyglotOptions; 31 | 32 | const createGetPolyglot = () => createSelector( 33 | getLocale, 34 | getPhrases, 35 | getPolyglotOptions, 36 | (locale, phrases, polyglotOptions) => new Polyglot({ 37 | locale, 38 | phrases, 39 | ...polyglotOptions, 40 | }) 41 | ); 42 | 43 | const createGetTranslation = () => createSelector( 44 | createGetPolyglot(), 45 | getPolyglotScope, 46 | getPolyglotOwnPhrases, 47 | (p, polyglotScope, ownPhrases) => (polyglotKey, ...args) => { 48 | const fullPath = polyglotScope + polyglotKey; 49 | const ownPhrase = (ownPhrases !== '') 50 | ? ownPhrases[fullPath] 51 | : null; 52 | 53 | return ownPhrase || p.t(fullPath, ...args); 54 | } 55 | ); 56 | 57 | const createGetTranslationMorphed = () => createSelector( 58 | createGetTranslation(), 59 | t => f => compose(f, t) 60 | ); 61 | 62 | const createGetTranslationUpperCased = () => createSelector( 63 | createGetTranslationMorphed(), 64 | m => m(toUpper) 65 | ); 66 | 67 | const createGetTranslationCapitalized = () => createSelector( 68 | createGetTranslationMorphed(), 69 | m => m(capitalize) 70 | ); 71 | 72 | const createGetTranslationTitleized = () => createSelector( 73 | createGetTranslationMorphed(), 74 | m => m(titleize) 75 | ); 76 | 77 | const createGetP = (polyglotOptions) => { 78 | const options = { polyglotOptions }; 79 | const getP = createSelector( 80 | getLocale, 81 | getPhrases, 82 | createGetPolyglot(), 83 | createGetTranslation(), 84 | createGetTranslationCapitalized(), 85 | createGetTranslationTitleized(), 86 | createGetTranslationUpperCased(), 87 | createGetTranslationMorphed(), 88 | (locale, phrases, p, t, tc, tt, tu, tm) => { 89 | if (!locale || !phrases) { 90 | return { 91 | t: identity, 92 | tc: identity, 93 | tt: identity, 94 | tu: identity, 95 | tm: identity, 96 | }; 97 | } 98 | return assign({}, p, { t, tc, tt, tu, tm }); 99 | }, 100 | ); 101 | return (state, props) => getP(state, { ...props, ...options }); 102 | }; 103 | 104 | const getP = createGetP(); 105 | 106 | export { getP, getLocale, createGetP }; 107 | -------------------------------------------------------------------------------- /src/middleware.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 3 | import { path } from 'ramda'; 4 | import configureStore from 'redux-mock-store'; 5 | 6 | import { createPolyglotMiddleware } from './middleware'; 7 | 8 | const CATCHED_ACTION = 'CATCHED_ACTION'; 9 | const OTHER_CATCHED_ACTION = 'OTHER_CATCHED_ACTION'; 10 | const UNCATCHED_ACTION = 'UNCATCHED_ACTION'; 11 | 12 | const fakePhrases = { hello: 'hello' }; 13 | 14 | const mockStore = configureStore([ 15 | createPolyglotMiddleware( 16 | [CATCHED_ACTION, OTHER_CATCHED_ACTION], 17 | path(['payload', 'locale']), 18 | () => new Promise(resolve => setTimeout(resolve, 1000, fakePhrases)) 19 | ), 20 | ]); 21 | 22 | describe('middleware', () => { 23 | jest.useFakeTimers(); 24 | 25 | it('doesn\'t impact the store when action is unknown.', (done) => { 26 | const store = mockStore({ polyglot: {} }); 27 | store.dispatch({ type: UNCATCHED_ACTION }); 28 | setImmediate(() => { 29 | expect(store.getActions()).toMatchSnapshot(); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('impacts the store when action is CATCHED_ACTION.', (done) => { 35 | const store = mockStore({ polyglot: {} }); 36 | store.dispatch({ type: CATCHED_ACTION, payload: { locale: 'en' } }); 37 | jest.runAllTimers(); 38 | setImmediate(() => { 39 | expect(store.getActions()).toMatchSnapshot(); 40 | done(); 41 | }); 42 | }); 43 | 44 | describe('Errors catching', () => { 45 | const errorMissing = 'polyglotMiddleware : missing parameters.'; 46 | const errorFirst = 'polyglotMiddleware : first parameter must be a string or an array of string.'; 47 | const errorSecond = 'polyglotMiddleware : second parameter must be a function.'; 48 | const errorThird = 'polyglotMiddleware : third parameter must be a function.'; 49 | 50 | const first = 'text'; 51 | 52 | const second = () => true; 53 | const third = () => true; 54 | const _badParams_ = true; // eslint-disable-line no-underscore-dangle 55 | 56 | const createMiddleware = createPolyglotMiddleware; 57 | 58 | it('doesn\'t crash when all parameters are provided.', () => { 59 | createPolyglotMiddleware(first, second, third); 60 | }); 61 | 62 | it('throws an error when createPolyglotMiddleware parameters are missing.', () => { 63 | expect(() => createPolyglotMiddleware()).toThrowError(errorMissing); 64 | expect(() => createPolyglotMiddleware(first)).toThrowError(errorMissing); 65 | expect(() => createPolyglotMiddleware(first, second)).toThrowError(errorMissing); 66 | }); 67 | 68 | it('throws an error when first parameter is not a string or an array', () => { 69 | expect(() => createMiddleware(_badParams_, second, third)).toThrowError(errorFirst); 70 | }); 71 | 72 | it('doesn\'t throw an error when first parameter is a string', () => { 73 | expect(() => createMiddleware(first, second, third)).not.toThrow(); 74 | }); 75 | 76 | it('doesn\'t throw an error when first parameter is an array', () => { 77 | expect(() => createMiddleware([], second, third)).not.toThrow(); 78 | }); 79 | 80 | it('throws an error when second parameter is not a string', () => { 81 | expect(() => createMiddleware(first, _badParams_, third)).toThrowError(errorSecond); 82 | }); 83 | 84 | it('throws an error when thrid parameter is not a string', () => { 85 | expect(() => createMiddleware(first, second, _badParams_)).toThrowError(errorThird); 86 | }); 87 | 88 | describe('reducer', () => { 89 | const polyglotMiddleware = createPolyglotMiddleware([], () => 'en', () => Promise.resolve({})); 90 | const rootReducer = combineReducers({ test: (state = 42) => state }); 91 | it('throws an error when polyglot is not in "state.polyglot"', () => { 92 | expect(() => { 93 | createStore(rootReducer, {}, applyMiddleware(polyglotMiddleware)); 94 | }).toThrowError('polyglotReducer : need to be store in "state.polyglot"'); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('Async dispatch', () => { 100 | it('returns the promise getPhrases called', () => { 101 | const store = mockStore({ polyglot: {} }); 102 | const action = store.dispatch({ type: CATCHED_ACTION }); 103 | expect(typeof action.then).toBe('function'); 104 | }); 105 | it('returns a promise resolving CATCHED_ACTION', async () => { 106 | const store = mockStore({ polyglot: {} }); 107 | const promise = store.dispatch({ type: CATCHED_ACTION }); 108 | jest.runAllTimers(); 109 | const result = await promise; 110 | expect(result).toEqual({ type: CATCHED_ACTION }); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-polyglot 2 | 3 | [ ![Codeship Status for Tiqa/redux-polyglot](https://app.codeship.com/projects/21623df0-8959-0134-75f2-0e35097499a9/status?branch=master)](https://app.codeship.com/projects/184177) 4 | 5 | Toolset (actions, reducer, middleware, enhancer, selectors) to help use Polyglot with Redux. 6 | 7 | ## Installation 8 | ``` 9 | npm install --save redux-polyglot 10 | ``` 11 | ## Setup 12 | 13 | First of all, you need the polyglot reducer in your rootReducer : 14 | ```javascript 15 | import { createStore, combineReducers } from 'redux'; 16 | import { polyglotReducer } from 'redux-polyglot'; 17 | 18 | const rootReducer = combineReducers({ 19 | ...yourReducers, 20 | polyglot: polyglotReducer, 21 | }); 22 | const store = createStore(rootReducer, {}); 23 | 24 | ``` 25 | ## Usage 26 | 27 | ### Set the language 28 | #### without middleware 29 | You can use redux-polyglot without his middleware, for this you need the `setLanguage()` action creator : 30 | 31 | - ```setLanguage :: (String, Object) -> Action``` 32 | 33 | Example: 34 | ```javascript 35 | import { setLanguage } from 'redux-polyglot'; 36 | 37 | store.dispatch(setLanguage('en', { yolo: 'yolo' })); 38 | ``` 39 | second parameter should be `polyglot phrases` (see [polyglot documentation](http://airbnb.io/polyglot.js/)) 40 | 41 | note: if language phrases already exists, this will overwrite the corresponding object state. 42 | 43 | #### with middleware 44 | The `createPolyglotMiddleware()` function allow you to automatically update language and phrases by listening to specific action(s). 45 | 46 | The middleware catches specific action(s), and find the locale in the payload, and then [asynchronously] load the `polyglot phrases` (with Promise). 47 | 48 | It takes 3 parameters and return a middleware : 49 | - 1 - `actionToCatch :: String | Array` 50 | - the type(s) of the action to catch 51 | - 2 - `getLocale :: Object -> String` 52 | - a function that take the catched action as parameter and return new language. 53 | - 3 - `getPhrases :: String -> Promise Object` 54 | - a function that take the language (as provided by `getLocale`) and return a Promise of Object ( Object should be `polyglot phrases` ) 55 | 56 | the middleware will catch `actionToCatch`; note: when a matching action is dispatched, it will return this promise called so you can await on it (pretty useful on SSR) 57 | 58 | ```javascript 59 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 60 | 61 | const polyglotMiddleware = createPolyglotMiddleware( 62 | 'ACTION_TO_CATCH', 63 | action => action.payload.locale, 64 | locale => new Promise(resolve => { 65 | // perform async here 66 | resolve({ 67 | hello: 'bonjour', 68 | }); 69 | }), 70 | ) 71 | 72 | const store = createStore(rootReducer, {}, applyMiddleware(polyglotMiddleware)); 73 | ``` 74 | 75 | you can catch more than one action passing an array of action types: 76 | ```javascript 77 | const polyglotMiddleware = createPolyglotMiddleware( 78 | ['FIRST_ACTION_TO_CATCH', 'SECOND_ACTION_TO_CATCH'], 79 | getLocale, 80 | getPhrases, 81 | ) 82 | ``` 83 | 84 | note: if language has not changed, nothing happens. 85 | 86 | ### Translation 87 | #### with getP() selector 88 | You can use the `getP(state)` selector. 89 | 90 | It returns an object with 4 functions inside : 91 | - t: String -> String : translation (the original polyglot `t` function) 92 | - tc: String -> String : translation capitalized 93 | - tt: String -> String : translation titleized 94 | - tu: String -> String : translation upper-cased 95 | - tm: (String -> String) -> String -> String : translation using a custom morphism 96 | 97 | (see [polyglot documentation](http://airbnb.io/polyglot.js/)) 98 | 99 | there is an optional parameter to getP(). 100 | this is allow you to automatically 'aim' a scope in your phrases object using `polyglotScope` property. 101 | 102 | for example : 103 | 104 | ```js 105 | store.dispatch(setLanguage('en', { 106 | some: { nested: { data: { hello: 'hello' } } } 107 | })); 108 | const p = getP(store.getState(), { polyglotScope: 'some.nested.data' }); 109 | console.log(p.tc('hello')) // => will return 'Hello' 110 | ``` 111 | 112 | #### Getting current locale 113 | `getLocale(state)` selector returns current language. 114 | 115 | #### If you use React 116 | 117 | You can use `connect()` from `react-redux`, and the getP() selector, to get the `p` prop in your component. 118 | 119 | Proptype: 120 | ````javascript 121 | p: PropTypes.shape({ 122 | t: PropTypes.func.isRequired, 123 | tc: PropTypes.func.isRequired, 124 | tu: PropTypes.func.isRequired, 125 | tm: PropTypes.func.isRequired, 126 | }), 127 | ```` 128 | 129 | ##### translate() enhancer 130 | `props.p` can be also be provided easily to a component with the translate enhancer : 131 | ```javascript 132 | import translate from 'redux-polyglot/translate'; 133 | const DummyComponentWithPProps = translate(DummyComponent); 134 | ``` 135 | 136 | you can select a `polyglotScope` with `translate('scope', Component)` 137 | ```js 138 | // all this lines return an enhanced Dummy component 139 | translate(Dummy); 140 | translate('catalog', Dummy); // with polyglotScope 141 | translate()(Dummy); // curried 142 | translate('catalog')(Dummy); // curried with polyglotScope. 143 | translate({ polyglotScope : 'some.nested.data', ownPhrases: 'some.nested.data.hello': 'Hi !', ... })(Dummy); // curried with object configuration. 144 | ``` 145 | 146 | ##### get locale in a component 147 | You can use the `getLocale()` selector inside a [mapStateToProps](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) from react-redux. 148 | 149 | Proptype: ````locale: PropTypes.string,```` 150 | 151 | ### Overwrite phrases 152 | In some case, you should be able to replace some default phrases by others phrases. 153 | 154 | For doing this, you have to define an object which contains your overwrited phrases. 155 | This object is composed of : ``` { 'some.nested.data': 'phrase', ... }``` where `key` is the target path you want to replace and `value` ... the new value. 156 | 157 | ##### with _getP()_ selector 158 | Simply add `ownPhrases` property and set the new configuration like above to overwrite : 159 | ```js 160 | store.dispatch(setLanguage('en', { 161 | some: { nested: { data: { hello: 'hello' } } } 162 | })); 163 | const p = getP(store.getState(), { 164 | polyglotScope: 'some.nested.data', 165 | ownPhrases: { 'some.nested.data.hello': 'Hi !' } 166 | }); 167 | console.log(p.tc('hello')) // => will return 'Hi !' 168 | ``` 169 | ##### with _translate()_ enhancer 170 | Instead passing only _string_ as parameter : `translate('catalog', Dummy)`, pass a plain _object_ which contains `polyglotScope` and `ownPhrases` properties : 171 | ```js 172 | translate({ 173 | polyglotScope : 'some.nested.data', 174 | ownPhrases: { 'some.nested.data.catalog': 'Cars' } 175 | }, Dummy); 176 | console.log(p.tc('catalog')) // => will return 'Cars' 177 | ``` 178 | 179 | ### Use polyglot options 180 | if you want to use `onMissingKey`, `allowMissing` or `warn` [polyglot](http://airbnb.io/polyglot.js/) options, you can use the `createGetP` factory selector to create a custom `getP`. 181 | 182 | usage : 183 | ```js 184 | import { createGetP } from 'redux-polyglot'; 185 | 186 | const options = { 187 | allowMissing: true, 188 | } 189 | 190 | export const getP = createGetP(options); 191 | ``` 192 | 193 | Please note you cannot use translate hoc with a custom `getP` selector. 194 | 195 | ## Team 196 | 197 | These folks keep the project moving and are resources for help: 198 | 199 | * Jérémy Vincent ([@jvincent42](https://github.com/jvincent42)) - developer 200 | * Jalil Arfaoui ([@JalilArfaoui](https://github.com/JalilArfaoui)) - developer 201 | * Guillaume ARM ([@guillaumearm](https://github.com/guillaumearm/)) - developer 202 | -------------------------------------------------------------------------------- /src/translate.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp, max-len */ 2 | 3 | import React, { PureComponent } from 'react'; 4 | import { Provider, connect } from 'react-redux'; 5 | import { createStore, combineReducers } from 'redux'; 6 | import renderer from 'react-test-renderer'; 7 | 8 | import { polyglotReducer } from './reducer'; 9 | import translate from './translate'; 10 | 11 | const dummyReducer = (state = '', action) => (action.type === 'DUMMY' ? action.payload : state); 12 | const createRootReducer = () => combineReducers({ polyglot: polyglotReducer, dummy: dummyReducer }); 13 | const fakeStore = createStore(createRootReducer(), { 14 | polyglot: { locale: 'en', phrases: { hello: 'hello', scope1: { hello: 'hello2' }, scope2: { hello: 'hello3' } } }, 15 | }); 16 | 17 | const createAnonymousComponent = () => () => (
); 18 | 19 | const DummyComponentWithDisplayName = createAnonymousComponent(); 20 | const name = 'DummyComponent'; 21 | DummyComponentWithDisplayName.displayName = name; 22 | 23 | describe('translate enhancer', () => { 24 | const DummyComponent = ({ p, dispatch }) => ( 25 |
31 | ); 32 | const EnhancedComponent = translate(DummyComponent); 33 | 34 | const tree = renderer.create( 35 | 36 | 37 | 38 | ).toJSON(); 39 | 40 | it('provides a valid p object.', () => { 41 | expect(tree.props['data-t']).toBe('hello'); 42 | expect(tree.props['data-tc']).toBe('Hello'); 43 | expect(tree.props['data-tu']).toBe('HELLO'); 44 | }); 45 | 46 | it('not inject dispatch prop.', () => { 47 | expect(tree.props['data-dispatch']).toBe(false); 48 | }); 49 | 50 | it('should return a valid translated component', () => { 51 | const Dummy = DummyComponent; 52 | expect(translate()(Dummy).displayName).toEqual(EnhancedComponent.displayName); 53 | expect(translate(Dummy).displayName).toEqual(EnhancedComponent.displayName); 54 | expect(translate('', Dummy).displayName).toEqual(EnhancedComponent.displayName); 55 | expect(translate('')(Dummy).displayName).toEqual(EnhancedComponent.displayName); 56 | expect(translate({ polyglotScope: '' })(Dummy).displayName).toEqual(EnhancedComponent.displayName); 57 | expect(translate({ polyglotScope: '', ownPhrases: { hello: 'Hi !' } })(Dummy).displayName).toEqual(EnhancedComponent.displayName); 58 | expect(translate({ ownPhrases: { hello: 'Hi !' } })(Dummy).displayName).toEqual(EnhancedComponent.displayName); 59 | }); 60 | 61 | describe('displayName', () => { 62 | it('has a valid displayName', () => { 63 | const translatedName = `Translated(${name})`; 64 | expect(EnhancedComponent.displayName).toBe(translatedName); 65 | expect(translate(DummyComponentWithDisplayName).displayName) 66 | .toBe(translatedName); 67 | }); 68 | 69 | it('has a default name when it is an anonymous component', () => { 70 | const translatedDefaultName = 'Translated(Component)'; 71 | const TranslatedComponent = translate(createAnonymousComponent()); 72 | expect(TranslatedComponent.displayName).toBe(translatedDefaultName); 73 | }); 74 | }); 75 | 76 | it('should not re-render component on every non-related dispatch call', async () => { 77 | let pChanged = false; 78 | let nbDispatch = 0; 79 | 80 | class TestComponent extends PureComponent { 81 | componentWillReceiveProps(nextProps) { 82 | if (nextProps.p !== this.props.p) pChanged = true; 83 | } 84 | 85 | render() { 86 | return
; 87 | } 88 | } 89 | 90 | 91 | class UnrelatedComponent extends PureComponent { 92 | componentDidMount() { 93 | nbDispatch += 1; 94 | this.props.dispatch({ type: 'DUMMY', payload: 're-render on every dispatch' }); 95 | } 96 | 97 | render() { 98 | return
{ this.props.dummy }
; 99 | } 100 | } 101 | 102 | const EnhancedTestComponent = translate(TestComponent); 103 | const ConnectedUnrelatedComponent = connect((state) => ({ dummy: state.dummy }))(UnrelatedComponent); 104 | 105 | renderer.create( 106 | 107 |
108 | 109 | 110 |
111 |
112 | ).toJSON(); 113 | 114 | expect(nbDispatch).toBe(1); 115 | expect(pChanged).toBe(false); 116 | }); 117 | 118 | it('should not re-render component on every non-related dispatch call when there are multiple translate with different options', async () => { 119 | let pChanged1 = false; 120 | let pChanged2 = false; 121 | let pChanged3 = false; 122 | let pChanged4 = false; 123 | let nbDispatch = 0; 124 | 125 | class TestComponent1 extends PureComponent { 126 | componentWillReceiveProps(nextProps) { 127 | if (nextProps.p !== this.props.p) pChanged1 = true; 128 | } 129 | 130 | render() { 131 | return
; 132 | } 133 | } 134 | 135 | class TestComponent2 extends PureComponent { 136 | componentWillReceiveProps(nextProps) { 137 | if (nextProps.p !== this.props.p) pChanged2 = true; 138 | } 139 | 140 | render() { 141 | return
; 142 | } 143 | } 144 | 145 | class TestComponent3 extends PureComponent { 146 | componentWillReceiveProps(nextProps) { 147 | if (nextProps.p !== this.props.p) pChanged3 = true; 148 | } 149 | 150 | render() { 151 | return
; 152 | } 153 | } 154 | 155 | class TestComponent4 extends PureComponent { 156 | componentWillReceiveProps(nextProps) { 157 | if (nextProps.p !== this.props.p) pChanged4 = true; 158 | } 159 | 160 | render() { 161 | return
; 162 | } 163 | } 164 | 165 | class UnrelatedComponent extends PureComponent { 166 | componentDidMount() { 167 | nbDispatch += 1; 168 | this.props.dispatch({ type: 'DUMMY', payload: 're-render on every dispatch' }); 169 | } 170 | 171 | render() { 172 | return
{ this.props.dummy }
; 173 | } 174 | } 175 | 176 | const EnhancedTestComponent1 = translate()(TestComponent1); 177 | const EnhancedTestComponent2 = translate('scope1')(TestComponent2); 178 | const EnhancedTestComponent3 = translate('scope2')(TestComponent3); 179 | const EnhancedTestComponent4 = translate(TestComponent4); 180 | const ConnectedUnrelatedComponent = connect((state) => ({ dummy: state.dummy }))(UnrelatedComponent); 181 | 182 | renderer.create( 183 | 184 |
185 | 186 | 187 | 188 | 189 | 190 |
191 |
192 | ).toJSON(); 193 | 194 | expect(nbDispatch).toBe(1); 195 | expect(pChanged1).toBe(false); 196 | expect(pChanged2).toBe(false); 197 | expect(pChanged3).toBe(false); 198 | expect(pChanged4).toBe(false); 199 | }); 200 | }); 201 | --------------------------------------------------------------------------------