├── demo ├── tslint.json ├── src │ ├── translations │ │ ├── en.movies.json │ │ ├── fr.movies.json │ │ ├── es.movies.json │ │ ├── global.json │ │ └── books.json │ ├── FormatCurrency.js │ ├── LanguageToggle.js │ ├── Main.css │ ├── index.js │ ├── sections │ │ ├── Books.js │ │ └── Movies.js │ └── Main.js ├── tsconfig.json ├── seed-translations.js ├── package.json └── public │ └── index.html ├── .prettierrc ├── .travis.yml ├── .flowconfig ├── .npmignore ├── .gitignore ├── .babelrc ├── src ├── index.js ├── withLocalize.js ├── LocalizeContext.js ├── Translate.js ├── LocalizeProvider.js ├── index.js.flow ├── index.d.ts ├── utils.js └── localize.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── ISSUE_TEMPLATE │ └── issue.md ├── LICENSE ├── flow-typed └── npm │ ├── flat_vx.x.x.js │ ├── prop-types_v15.x.x.js │ ├── redux_v3.x.x.js │ └── reselect_v3.x.x.js ├── webpack.config.babel.js ├── tests ├── withLocalize.test.js ├── LocalizeProvider.test.js ├── utils.test.js ├── Translate.test.js └── localize.test.js ├── README.md ├── package.json ├── CHANGELOG.md └── MIGRATING.md /demo/tslint.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - "6" 5 | script: 6 | - yarn test 7 | after_success: 8 | - yarn run coverage -------------------------------------------------------------------------------- /demo/src/translations/en.movies.json: -------------------------------------------------------------------------------- 1 | { 2 | "movie1": { 3 | "title": "Jurassic Park", 4 | "description": "here is description" 5 | }, 6 | "movie2": { 7 | "title": "Back to the Future", 8 | "description": "fasdfsafsadf" 9 | } 10 | } -------------------------------------------------------------------------------- /demo/src/translations/fr.movies.json: -------------------------------------------------------------------------------- 1 | { 2 | "movie1": { 3 | "title": "French - Jurassic Park", 4 | "description": "French - here is description" 5 | }, 6 | "movie2": { 7 | "title": "French - Back to the Future", 8 | "description": "French - fasdfsafsadf" 9 | } 10 | } -------------------------------------------------------------------------------- /demo/src/translations/es.movies.json: -------------------------------------------------------------------------------- 1 | { 2 | "movie1": { 3 | "title": "Spanish - Jurassic Park", 4 | "description": "Spanish - here is description" 5 | }, 6 | "movie2": { 7 | "title": "Spanish - Back to the Future", 8 | "description": "Spanish - fasdfsafsadf" 9 | } 10 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ./dist/.* 3 | ./es/.* 4 | ./lib/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | 10 | [lints] 11 | 12 | [options] 13 | module.ignore_non_literal_requires=true 14 | module.name_mapper='react-localize-redux' -> '/src/index.js' 15 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | node_modules 4 | .npm 5 | .vscode 6 | 7 | coverage 8 | demo 9 | docs 10 | examples 11 | flow-typed 12 | site 13 | src 14 | tests 15 | 16 | .babelrc 17 | .flowconfig 18 | .gitignore 19 | .travis.yml 20 | mkdocs.yml 21 | package-lock.json 22 | webpack.config.babel.js 23 | -------------------------------------------------------------------------------- /demo/src/FormatCurrency.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withLocalize } from 'react-localize-redux'; 3 | 4 | const FormatNumber = props => { 5 | return props.activeLanguage 6 | ? Number(props.children).toLocaleString(props.activeLanguage.code) 7 | : null; 8 | } 9 | 10 | export default withLocalize(FormatNumber); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Coverage directory used by tools like istanbul 7 | coverage 8 | 9 | # Dependency directories 10 | node_modules 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | lib 16 | es 17 | dist 18 | site 19 | .vscode 20 | .DS_Store 21 | demo/build 22 | testing 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-no-commonjs", "react", "stage-0"], 3 | "plugins": ["transform-flow-strip-types", "transform-react-remove-prop-types"], 4 | "env": { 5 | "development": { 6 | "plugins": ["transform-es2015-modules-commonjs"] 7 | }, 8 | "commonjs": { 9 | "plugins": ["transform-es2015-modules-commonjs"] 10 | }, 11 | "test": { 12 | "plugins": ["transform-es2015-modules-commonjs"] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { Translate } from './Translate'; 3 | export { withLocalize } from './withLocalize'; 4 | export { LocalizeProvider } from './LocalizeProvider'; 5 | export { LocalizeContext } from './LocalizeContext'; 6 | 7 | export { 8 | localizeReducer, 9 | initialize, 10 | addTranslation, 11 | addTranslationForLanguage, 12 | setActiveLanguage, 13 | getTranslate, 14 | getActiveLanguage, 15 | getLanguages, 16 | getTranslations, 17 | getOptions 18 | } from './localize'; 19 | -------------------------------------------------------------------------------- /demo/src/translations/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": [ 3 | "My Article title", 4 | "My Article title FR", 5 | "My Article title ES" 6 | ], 7 | "greeting": [ 8 | null, 9 | "Bonjour ${name}!", 10 | "Hola ${name}!" 11 | ], 12 | "homeLink": [ 13 | "Click ${ link } to go home.", 14 | "Cliquez ${ link } pour rentrer à la page d'accueil", 15 | "Haga clic ${ link } para ir a la página de inicio" 16 | ], 17 | "here": [ 18 | "here", 19 | "ici", 20 | "aquí" 21 | ] 22 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request,** please make sure the following is done: 2 | 3 | 1. Fork [the repository](https://github.com/ryandrewjohnson/react-localize-redux) and create your branch from `master`. 4 | 2. Run `yarn` in the repository root. 5 | 3. If you've fixed a bug or added code that should be tested, add tests! 6 | 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test:dev` is helpful in development. 7 | 5. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8 | 6. Run the [Flow](https://flowtype.org/) typechecks (`yarn flow`). 9 | -------------------------------------------------------------------------------- /demo/src/LanguageToggle.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { withLocalize } from 'react-localize-redux'; 4 | import './Main.css'; 5 | 6 | const LanguageToggle = ({languages, activeLanguage, setActiveLanguage}) => { 7 | const getClass = (languageCode) => { 8 | return languageCode === activeLanguage.code ? 'active' : '' 9 | }; 10 | 11 | return ( 12 | 19 | ); 20 | }; 21 | 22 | export default withLocalize(LanguageToggle); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | **Do you want to request a _feature_ or report a _bug_?** 7 | 8 | **What is the current behavior?** 9 | 10 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:** 11 | 12 | **What is the expected behavior?** 13 | 14 | **Which versions of `react` and `react-localize-redux` are you using?** 15 | -------------------------------------------------------------------------------- /src/withLocalize.js: -------------------------------------------------------------------------------- 1 | import React, { Component, type ComponentType } from 'react'; 2 | import hoistNonReactStatic from 'hoist-non-react-statics'; 3 | import { LocalizeContext, type LocalizeContextProps } from './LocalizeContext'; 4 | 5 | export function withLocalize( 6 | WrappedComponent: ComponentType 7 | ): ComponentType<$Diff> { 8 | class LocalizedComponent extends Component { 9 | render() { 10 | return ( 11 | 12 | {context => } 13 | 14 | ); 15 | } 16 | } 17 | hoistNonReactStatic(LocalizedComponent, WrappedComponent); 18 | return LocalizedComponent; 19 | } 20 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": true, 19 | "checkJs": true, 20 | "allowJs": true 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "build", 25 | "scripts", 26 | "acceptance-tests", 27 | "webpack", 28 | "jest", 29 | "src/setupTests.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Please fill out the following template. 4 | 5 | --- 6 | 7 | 11 | 12 | **Do you want to request a _feature_ or report a _bug_?** 13 | 14 | **What is the current behavior?** 15 | 16 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:** 17 | 18 | **What is the expected behavior?** 19 | 20 | **Which versions of `react` and `react-localize-redux` are you using?** 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ryan Johnson 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 | -------------------------------------------------------------------------------- /flow-typed/npm/flat_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: edc765270117dc6b5b098c8801defc9d 2 | // flow-typed version: <>/flat_v^2.0.1/flow_v0.70.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'flat' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'flat' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'flat/test/test' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'flat/index' { 31 | declare module.exports: $Exports<'flat'>; 32 | } 33 | declare module 'flat/index.js' { 34 | declare module.exports: $Exports<'flat'>; 35 | } 36 | declare module 'flat/test/test.js' { 37 | declare module.exports: $Exports<'flat/test/test'>; 38 | } 39 | -------------------------------------------------------------------------------- /demo/seed-translations.js: -------------------------------------------------------------------------------- 1 | const writeJsonFile = require('write-json-file'); 2 | const loremIpsum = require('lorem-ipsum'); 3 | 4 | const translationCount = 10000; 5 | const translations = {}; 6 | const translationsForLanguage = {}; 7 | const languages = ['EN', 'FR', 'ES']; 8 | 9 | for (let i = 0; i < translationCount; i++) { 10 | translations[`key-${i}`] = languages.map(lang => `${lang} - ${loremIpsum()}`); 11 | } 12 | 13 | languages.forEach(lang => { 14 | for (let i = 0; i < translationCount; i++) { 15 | translationsForLanguage[lang] === undefined 16 | ? (translationsForLanguage[lang] = { [`key-${i}`]: loremIpsum() }) 17 | : (translationsForLanguage[lang][`key-${i}`] = loremIpsum()); 18 | } 19 | }); 20 | 21 | Object.keys(translationsForLanguage).forEach(key => { 22 | writeJsonFile( 23 | `./src/translations/${key.toLowerCase()}.seed-translations.json`, 24 | translationsForLanguage[key] 25 | ).then(() => { 26 | console.log(`translations created for ${key}!`); 27 | }); 28 | }); 29 | 30 | writeJsonFile('./src/translations/seed-translations.json', translations).then( 31 | () => { 32 | console.log('translations created!'); 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /demo/src/Main.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #20232a; 3 | font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | header { 9 | border-bottom: 1px solid #eee; 10 | padding: 1rem; 11 | } 12 | 13 | main { 14 | display: flex; 15 | } 16 | 17 | nav { 18 | background-color: #eee; 19 | min-width: 15%; 20 | padding: 1rem 0; 21 | } 22 | nav > a { 23 | border-bottom: 1px solid #ccc; 24 | color: #20232a; 25 | display: block; 26 | text-decoration: none; 27 | padding: 0.75rem 2rem; 28 | } 29 | nav > a.active { 30 | color: #61dafb; 31 | background-color: #20232a; 32 | } 33 | nav > a:first-child { 34 | border-top: 1px solid #ccc; 35 | } 36 | 37 | .content { 38 | padding: 2rem; 39 | } 40 | 41 | .selector { 42 | margin: 0; 43 | padding: 0; 44 | list-style: none; 45 | } 46 | .selector > li { 47 | display: inline-block; 48 | padding: 0 0.5rem; 49 | } 50 | .selector button { 51 | background-color: #eee; 52 | box-shadow: none; 53 | border: 1px solid #ccc; 54 | padding: 10px 10px; 55 | border-radius: 4px; 56 | min-width: 100px; 57 | transition: all 0.3s ease-out; 58 | } 59 | .selector button.active { 60 | color: #61dafb; 61 | background-color: #20232a; 62 | } 63 | 64 | .card { 65 | border: 1px solid #ccc; 66 | } 67 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "^16.3.2", 9 | "react-dom": "^16.3.2", 10 | "react-loadable": "^5.3.1", 11 | "react-router": "4.2.0", 12 | "react-router-dom": "^4.2.2", 13 | "react-scripts": "1.1.4", 14 | "react-scripts-ts": "2.13.0", 15 | "react-syntax-highlighter": "^7.0.2", 16 | "react-toggle-button": "^2.2.0", 17 | "redux": "^4.0.0", 18 | "redux-devtools-extension": "^2.13.2" 19 | }, 20 | "devDependencies": { 21 | "@types/jest": "^22.1.3", 22 | "@types/node": "^9.4.6", 23 | "@types/react": "16.0.38", 24 | "@types/react-dom": "16.0.4", 25 | "@types/react-router": "^4.0.24", 26 | "html-webpack-plugin": "^3.2.0", 27 | "lorem-ipsum": "^1.0.4", 28 | "typescript": "^2.7.2", 29 | "write-json-file": "^2.3.0" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test --env=jsdom", 35 | "eject": "react-scripts eject", 36 | "start:ts": "react-scripts-ts start", 37 | "build:ts": "react-scripts-ts build", 38 | "test:ts": "react-scripts-ts test --env=jsdom", 39 | "eject:ts": "react-scripts-ts eject", 40 | "seed:translations": "node seed-translations.js" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /flow-typed/npm/prop-types_v15.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3eaa1f24c7397b78a7481992d2cddcb2 2 | // flow-typed version: a1a20d4928/prop-types_v15.x.x/flow_>=v0.41.x 3 | 4 | type $npm$propTypes$ReactPropsCheckType = ( 5 | props: any, 6 | propName: string, 7 | componentName: string, 8 | href?: string) => ?Error; 9 | 10 | declare module 'prop-types' { 11 | declare var array: React$PropType$Primitive>; 12 | declare var bool: React$PropType$Primitive; 13 | declare var func: React$PropType$Primitive; 14 | declare var number: React$PropType$Primitive; 15 | declare var object: React$PropType$Primitive; 16 | declare var string: React$PropType$Primitive; 17 | declare var any: React$PropType$Primitive; 18 | declare var arrayOf: React$PropType$ArrayOf; 19 | declare var element: React$PropType$Primitive; /* TODO */ 20 | declare var instanceOf: React$PropType$InstanceOf; 21 | declare var node: React$PropType$Primitive; /* TODO */ 22 | declare var objectOf: React$PropType$ObjectOf; 23 | declare var oneOf: React$PropType$OneOf; 24 | declare var oneOfType: React$PropType$OneOfType; 25 | declare var shape: React$PropType$Shape; 26 | 27 | declare function checkPropTypes( 28 | propTypes: $Subtype<{[_: $Keys]: $npm$propTypes$ReactPropsCheckType}>, 29 | values: V, 30 | location: string, 31 | componentName: string, 32 | getStack: ?(() => ?string) 33 | ) : void; 34 | } 35 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import { resolve } from 'path'; 3 | import ProgressBarPlugin from 'progress-bar-webpack-plugin'; 4 | import { getIfUtils, removeEmpty } from 'webpack-config-utils'; 5 | 6 | export default env => { 7 | const { ifProd, ifNotProd } = getIfUtils(env); 8 | const reactExternal = { 9 | root: 'React', 10 | commonjs2: 'react', 11 | commonjs: 'react', 12 | amd: 'react' 13 | }; 14 | 15 | return { 16 | entry: './src/index.js', 17 | 18 | resolve: { 19 | extensions: ['.webpack.js', '.web.js', '.js', '.jsx', '.json'], 20 | modules: [ 21 | resolve(__dirname, 'src'), 22 | resolve(__dirname, 'node_modules') 23 | ] 24 | }, 25 | 26 | output: { 27 | library: 'ReactLocalizeRedux', 28 | libraryTarget: 'umd' 29 | }, 30 | 31 | externals: { 32 | 'react': reactExternal 33 | }, 34 | 35 | module: { 36 | rules: removeEmpty([ 37 | { 38 | test: /\.js(x?)$/, 39 | loaders: [ 'babel-loader' ], 40 | exclude: /node_modules/ 41 | } 42 | ]) 43 | }, 44 | 45 | plugins: removeEmpty([ 46 | new ProgressBarPlugin(), 47 | new webpack.DefinePlugin({ 48 | 'process.env': { 49 | NODE_ENV: ifProd('"production"', '"development"') 50 | } 51 | }), 52 | ifProd(new webpack.LoaderOptionsPlugin({ 53 | minimize: true, 54 | debug: true 55 | })), 56 | ifProd(new webpack.optimize.UglifyJsPlugin({ 57 | compress: { 58 | screw_ie8: true, 59 | warnings: false 60 | } 61 | })) 62 | ]) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore, combineReducers } from 'redux'; 4 | import { composeWithDevTools } from 'redux-devtools-extension'; 5 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 6 | import { LocalizeProvider, localizeReducer } from 'react-localize-redux'; 7 | import Main from './Main'; 8 | 9 | const USING_REDUX_KEY = 'redux'; 10 | 11 | class App extends React.Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | 16 | this.onToggleReduxClick = this.onToggleReduxClick.bind(this); 17 | 18 | const isUsingReduxFromLocalStorage = window.localStorage.getItem(USING_REDUX_KEY) 19 | ? window.localStorage.getItem(USING_REDUX_KEY) === 'true' 20 | : false; 21 | 22 | const store = isUsingReduxFromLocalStorage === false 23 | ? undefined 24 | : this.getReduxStore(); 25 | 26 | this.state = { 27 | isUsingRedux: isUsingReduxFromLocalStorage, 28 | store 29 | }; 30 | } 31 | 32 | getReduxStore() { 33 | return createStore(combineReducers({ 34 | localize: localizeReducer 35 | }), composeWithDevTools()); 36 | } 37 | 38 | onToggleReduxClick() { 39 | const nextIsUsingRedux = !this.state.isUsingRedux; 40 | 41 | window.localStorage.setItem(USING_REDUX_KEY, nextIsUsingRedux); 42 | window.location.reload(); 43 | } 44 | 45 | render() { 46 | console.log(this.state.store); 47 | return ( 48 | 49 | 50 | 51 |
55 | } /> 56 | 57 | 58 | ); 59 | } 60 | } 61 | 62 | render(, document.getElementById('root')); 63 | -------------------------------------------------------------------------------- /demo/src/sections/Books.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { withLocalize, Translate } from 'react-localize-redux'; 4 | import translations from '../translations/books.json'; 5 | import largeTranslations from '../translations/seed-translations.json'; 6 | import '../Main.css'; 7 | import FormatCurrency from '../FormatCurrency'; 8 | 9 | class Books extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | books: ['book1', 'book2', 'book3'] 15 | }; 16 | 17 | this.props.addTranslation(translations); 18 | this.props.addTranslation(largeTranslations); 19 | } 20 | 21 | addBook() { 22 | const index = Math.floor(Math.random() * 3) + 1; 23 | this.setState({ 24 | books: [...this.state.books, `book${index}`] 25 | }); 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 | In this section the following features are demostrated: 32 |
    33 |
  • Translate component's render props API
  • 34 |
  • Nested translation data format
  • 35 |
  • 36 | Overriding default language translation data with Translate's 37 | children 38 |
  • 39 |
  • Using translation data with placeholders
  • 40 |
  • Adding localize props using withLocalize HOC
  • 41 |
42 |

43 | 47 | {'Top ${count} books:'} 48 | 49 |

50 |

51 | Money: 1000.00 52 |

53 | 54 | {({ translate }) => 55 | this.state.books.map((book, index) => ( 56 |
57 |

{translate(`books.list.${book}.title`)}

58 |

{translate(`books.list.${book}.description`)}

59 |
60 | )) 61 | } 62 |
63 | 64 |
65 | ); 66 | } 67 | } 68 | 69 | export default withLocalize(Books); 70 | -------------------------------------------------------------------------------- /tests/withLocalize.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | 7 | const defaultContext = { 8 | translate: jest.fn(), 9 | languages: [], 10 | defaultLanguage: 'en', 11 | activeLanguage: { code: 'en', active: true }, 12 | initialize: jest.fn(), 13 | addTranslation: jest.fn(), 14 | addTranslationForLanguage: jest.fn(), 15 | setActiveLanguage: jest.fn() 16 | }; 17 | 18 | const getWithLocalizeWithContext = () => { 19 | jest.doMock('../src/LocalizeContext', () => { 20 | return { 21 | LocalizeContext: { 22 | Consumer: (props) => props.children(defaultContext) 23 | } 24 | } 25 | }); 26 | 27 | return require('withLocalize').withLocalize; 28 | }; 29 | 30 | describe('withLocalize', () => { 31 | it('should add LocalizeContext as props to WrappedComponent', () => { 32 | const withLocalize = getWithLocalizeWithContext(); 33 | const WrapperComponent = props =>

Hello You!

; 34 | const Wrapped = withLocalize(WrapperComponent); 35 | 36 | const result = shallow(); 37 | const wrapper = result.dive(); 38 | 39 | Object.keys(defaultContext).forEach(key => { 40 | expect(wrapper.props()[key]).toBeDefined(); 41 | }); 42 | }); 43 | 44 | it('should include any existing props on WrappedComponent', () => { 45 | const withLocalize = getWithLocalizeWithContext(); 46 | const WrapperComponent = props =>

Hello You!

; 47 | const Wrapped = withLocalize(WrapperComponent); 48 | 49 | const result = shallow(); 50 | const wrapper = result.dive(); 51 | expect(wrapper.props().name).toEqual('Testy McTest'); 52 | }); 53 | 54 | it('should hoist any existing static functions on WrappedComponent', () => { 55 | const withLocalize = getWithLocalizeWithContext(); 56 | class WrapperComponent extends React.Component { 57 | static sayHello() { 58 | return 'hello'; 59 | } 60 | render() { 61 | return ( 62 |

Hello You!

63 | ); 64 | } 65 | }; 66 | const Wrapped = withLocalize(WrapperComponent); 67 | expect(Wrapped.sayHello()).toEqual('hello') 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /demo/src/Main.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { renderToStaticMarkup } from 'react-dom/server'; 4 | import { Route, NavLink } from 'react-router-dom'; 5 | import ToggleButton from 'react-toggle-button' 6 | import { withLocalize, Translate } from 'react-localize-redux'; 7 | import LanguageToggle from './LanguageToggle'; 8 | import globalTranslations from './translations/global.json'; 9 | import Movies from './sections/Movies'; 10 | import Books from './sections/Books'; 11 | import * as classes from './Main.css'; 12 | 13 | class Main extends React.Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | 18 | this.props.initialize({ 19 | languages: [ 20 | { name: 'English', code: 'en' }, 21 | { name: 'French', code: 'fr' }, 22 | { name: 'Spanish', code: 'es' } 23 | ], 24 | translation: globalTranslations, 25 | options: { renderToStaticMarkup } 26 | }); 27 | } 28 | 29 | componentDidUpdate(prevProps) { 30 | const prevLangCode = prevProps.activeLanguage && prevProps.activeLanguage.code; 31 | const curLangCode = this.props.activeLanguage && this.props.activeLanguage.code; 32 | const hasLanguageChanged = prevLangCode !== curLangCode; 33 | console.log('test', hasLanguageChanged); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |
40 | 41 | 42 | Using Redux 43 | this.props.onToggleClick()} 48 | /> 49 | 50 | here}} 53 | > 54 | {'Click ${ here } to go home'} 55 | 56 |
57 | 58 |
59 | 63 | 64 | 65 | 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | export default withLocalize(Main); -------------------------------------------------------------------------------- /demo/src/sections/Movies.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { withLocalize, Translate } from 'react-localize-redux'; 4 | import '../Main.css'; 5 | 6 | class Movies extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | name: '' 12 | }; 13 | 14 | this.addTranslationsForActiveLanguage(); 15 | } 16 | 17 | componentDidUpdate(prevProps, prevState) { 18 | const hasActiveLanguageChanged = 19 | prevProps.activeLanguage !== this.props.activeLanguage; 20 | 21 | if (hasActiveLanguageChanged) { 22 | this.addTranslationsForActiveLanguage(); 23 | } 24 | } 25 | 26 | addTranslationsForActiveLanguage() { 27 | const { activeLanguage } = this.props; 28 | 29 | if (!activeLanguage) { 30 | return; 31 | } 32 | 33 | import(`../translations/${activeLanguage.code}.movies.json`).then( 34 | translations => { 35 | this.props.addTranslationForLanguage(translations, activeLanguage.code); 36 | } 37 | ); 38 | 39 | import(`../translations/${ 40 | activeLanguage.code 41 | }.seed-translations.json`).then(translations => { 42 | this.props.addTranslationForLanguage(translations, activeLanguage.code); 43 | }); 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 | In this section the following features are demostrated: 50 |
    51 |
  • Splitting translation data by language
  • 52 |
  • Dynamically load translation data based on active language
  • 53 |
  • Using Translate component with and without children
  • 54 |
  • Using translation data with placeholders
  • 55 |
  • Adding localize props using withLocalize HOC
  • 56 |
57 |
58 | 59 | this.setState({ name: e.target.value })} 63 | /> 64 |
65 | 66 |

67 | 68 | {'Welcome ${name}!'} 69 | 70 |

71 | {[1, 2].map(item => ( 72 |
73 |

74 | 75 |

76 |

77 | 78 |

79 |
80 | ))} 81 |
82 | ); 83 | } 84 | } 85 | 86 | export default withLocalize(Movies); 87 | -------------------------------------------------------------------------------- /demo/src/translations/books.json: -------------------------------------------------------------------------------- 1 | { 2 | "books": { 3 | "heading": [ 4 | "This will be overidden with default language text in Translate component:", 5 | "FR - Top ${count} Books:", 6 | "ES - Top ${count} Books:" 7 | ], 8 | "list": { 9 | "book1": { 10 | "title": [ 11 | "1. The Daylight War: Book Three of The Demon Cycle", 12 | "FR - The Daylight War: Book Three of The Demon Cycle", 13 | "ES - The Daylight War: Book Three of The Demon Cycle" 14 | ], 15 | "description": [ 16 | "Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes.", 17 | "FR - Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes.", 18 | "ES - Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes." 19 | ] 20 | }, 21 | "book2": { 22 | "title": [ 23 | "2. The Daylight War: Book Three of The Demon Cycle", 24 | "FR - The Daylight War: Book Three of The Demon Cycle", 25 | "ES - The Daylight War: Book Three of The Demon Cycle" 26 | ], 27 | "description": [ 28 | "Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes.", 29 | "FR - Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes.", 30 | "ES - Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes." 31 | ] 32 | }, 33 | "book3": { 34 | "title": [ 35 | "3. The Daylight War: Book Three of The Demon Cycle", 36 | "FR - The Daylight War: Book Three of The Demon Cycle", 37 | "ES - The Daylight War: Book Three of The Demon Cycle" 38 | ], 39 | "description": [ 40 | "Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes.", 41 | "FR - Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes.", 42 | "ES - Continuing the impressive debut fantasy series from author Peter V. Brett, The DAYLIGHT WAR is book three of the Demon Cycle, pulling the reader into a world of demons, darkness and heroes." 43 | ] 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | React Localize 4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 | --- 20 | 21 | Localization library for handling translations in [React](https://facebook.github.io/react). 22 | 23 | * Does not require [Redux](https://redux.js.org/), but does provide out of the box support for it. 24 | * Built on React's native [Context](https://reactjs.org/docs/context.html). 25 | * [Works with Server Side Rendering](https://ryandrewjohnson.github.io/react-localize-redux-docs/#working-with-server-side-rendering). 26 | * [Include inline default translations](https://ryandrewjohnson.github.io/react-localize-redux-docs/#include-inline-default-translations) 27 | * [Render React components from translations](https://ryandrewjohnson.github.io/react-localize-redux-docs/#react-translations). 28 | * [Dynamic translations](https://ryandrewjohnson.github.io/react-localize-redux-docs/#dynamic-translations) 29 | * [HTML translations](https://ryandrewjohnson.github.io/react-localize-redux-docs/#html-translations) 30 | * [Plus more...](https://ryandrewjohnson.github.io/react-localize-redux-docs/#guides) 31 | 32 | ## Installation 33 | 34 | ``` 35 | npm install react-localize-redux --save 36 | ``` 37 | 38 | ## Documentation 39 | 40 | The official documentation can be found [online](https://ryandrewjohnson.github.io/react-localize-redux-docs/), and is divided into the following sections: 41 | 42 | * [Getting Started](https://ryandrewjohnson.github.io/react-localize-redux-docs/#getting-started) 43 | * [Formatting Translations](https://ryandrewjohnson.github.io/react-localize-redux-docs//#formatting-translations) 44 | * [Guides](https://ryandrewjohnson.github.io/react-localize-redux-docs/#guides) 45 | * [FAQ](https://ryandrewjohnson.github.io/react-localize-redux-docs/#faq) 46 | * [API Reference](https://ryandrewjohnson.github.io/react-localize-redux-docs/#api-reference) 47 | * [Migrating from v2 to v3](MIGRATING.md) 48 | 49 | ## Demo 50 | 51 | [Code Sandbox Demo](https://codesandbox.io/s/14xp1xy9ql) 52 | 53 | ## Contributing 54 | 55 | Want to help? Contributions are welcome, but please be sure before submitting a pull request that you 56 | have first opened an issue to discuss the work with the maintainers first. This will ensure we're all 57 | on the same page before any work is done. 58 | 59 | **For additional info:** 60 | 61 | * See [Issue Template](.github/ISSUE_TEMPLATE.md). 62 | * See [Pull Request Templete](.github/PULL_REQUEST_TEMPLATE.md). 63 | 64 | ## Change Log 65 | 66 | This project adheres to [Semantic Versioning](https://semver.org/). 67 | Every release will be [documented](CHANGELOG.md) along with any breaking changes when applicable. 68 | -------------------------------------------------------------------------------- /src/LocalizeContext.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import createReactContext, { type Context } from 'create-react-context'; 4 | import { createSelector, type Selector } from 'reselect'; 5 | import { 6 | type TranslateFunction, 7 | type Language, 8 | type MultipleLanguageTranslation, 9 | type SingleLanguageTranslation, 10 | type InitializePayload, 11 | type LocalizeState, 12 | type renderToStaticMarkupFunction, 13 | defaultTranslateOptions 14 | } from './localize'; 15 | import { 16 | localizeReducer, 17 | getTranslate, 18 | initialize, 19 | addTranslation, 20 | addTranslationForLanguage, 21 | setActiveLanguage, 22 | getLanguages, 23 | getActiveLanguage, 24 | getOptions 25 | } from './localize'; 26 | 27 | export type LocalizeContextProps = { 28 | translate: TranslateFunction, 29 | languages: Language[], 30 | activeLanguage: Language, 31 | defaultLanguage: string, 32 | initialize: (payload: InitializePayload) => void, 33 | addTranslation: (translation: MultipleLanguageTranslation) => void, 34 | addTranslationForLanguage: ( 35 | translation: SingleLanguageTranslation, 36 | language: string 37 | ) => void, 38 | setActiveLanguage: (languageCode: string) => void, 39 | renderToStaticMarkup: renderToStaticMarkupFunction | false, 40 | ignoreTranslateChildren: boolean 41 | }; 42 | 43 | const dispatchInitialize = (dispatch: Function) => ( 44 | payload: InitializePayload 45 | ) => { 46 | return dispatch(initialize(payload)); 47 | }; 48 | 49 | const dispatchAddTranslation = (dispatch: Function) => ( 50 | translation: MultipleLanguageTranslation 51 | ) => { 52 | return dispatch(addTranslation(translation)); 53 | }; 54 | 55 | const dispatchAddTranslationForLanguage = (dispatch: Function) => ( 56 | translation: SingleLanguageTranslation, 57 | language: string 58 | ) => { 59 | return dispatch(addTranslationForLanguage(translation, language)); 60 | }; 61 | 62 | const dispatchSetActiveLanguage = (dispatch: Function) => ( 63 | languageCode: string 64 | ) => { 65 | return dispatch(setActiveLanguage(languageCode)); 66 | }; 67 | 68 | export const getContextPropsFromState = ( 69 | dispatch: Function 70 | ): Selector => 71 | createSelector( 72 | getTranslate, 73 | getLanguages, 74 | getActiveLanguage, 75 | getOptions, 76 | (translate, languages, activeLanguage, options) => { 77 | const defaultLanguage = 78 | options.defaultLanguage || (languages[0] && languages[0].code); 79 | const renderToStaticMarkup = options.renderToStaticMarkup; 80 | const ignoreTranslateChildren = 81 | options.ignoreTranslateChildren !== undefined 82 | ? options.ignoreTranslateChildren 83 | : defaultTranslateOptions.ignoreTranslateChildren; 84 | 85 | return { 86 | translate, 87 | languages, 88 | defaultLanguage, 89 | activeLanguage, 90 | initialize: dispatchInitialize(dispatch), 91 | addTranslation: dispatchAddTranslation(dispatch), 92 | addTranslationForLanguage: dispatchAddTranslationForLanguage(dispatch), 93 | setActiveLanguage: dispatchSetActiveLanguage(dispatch), 94 | renderToStaticMarkup, 95 | ignoreTranslateChildren 96 | }; 97 | } 98 | ); 99 | 100 | const defaultLocalizeState = localizeReducer(undefined, ({}: any)); 101 | const defaultContext = getContextPropsFromState(() => {})(defaultLocalizeState); 102 | 103 | export const LocalizeContext: Context< 104 | LocalizeContextProps 105 | > = createReactContext(defaultContext); 106 | -------------------------------------------------------------------------------- /src/Translate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { 5 | getTranslate, 6 | addTranslationForLanguage, 7 | getLanguages, 8 | getOptions, 9 | getActiveLanguage, 10 | getTranslationsForActiveLanguage 11 | } from './localize'; 12 | import { get, storeDidChange } from './utils'; 13 | import { LocalizeContext, type LocalizeContextProps } from './LocalizeContext'; 14 | import { withLocalize } from './withLocalize'; 15 | import type { 16 | TranslateOptions, 17 | TranslatePlaceholderData, 18 | TranslateFunction, 19 | Language 20 | } from './localize'; 21 | 22 | export type TranslateProps = { 23 | id?: string, 24 | options?: TranslateOptions, 25 | data?: TranslatePlaceholderData, 26 | children?: any | TranslateChildFunction 27 | }; 28 | type TranslateWithContextProps = TranslateProps & { 29 | context: LocalizeContextProps 30 | }; 31 | 32 | export type TranslateChildFunction = (context: LocalizeContextProps) => any; 33 | 34 | class WrappedTranslate extends React.Component { 35 | unsubscribeFromStore: any; 36 | 37 | componentDidMount() { 38 | this.addDefaultTranslation(); 39 | } 40 | 41 | componentDidUpdate(prevProps: TranslateWithContextProps) { 42 | const idChanged = this.props.id && prevProps.id !== this.props.id; 43 | 44 | const noDefaultLanguage = 45 | !get(prevProps, 'context.defaultLanguage') && 46 | !get(prevProps, 'options.language'); 47 | const incomingLanguage = 48 | get(this.props, 'context.defaultLanguage') || 49 | get(this.props, 'options.language'); 50 | 51 | const defaultLanguageSet = noDefaultLanguage && incomingLanguage; 52 | 53 | if (idChanged || defaultLanguageSet) { 54 | this.addDefaultTranslation(); 55 | } 56 | } 57 | 58 | addDefaultTranslation() { 59 | const { context, id, children, options = {} } = this.props; 60 | const defaultLanguage = options.language || context.defaultLanguage; 61 | const fallbackRenderToStaticMarkup = value => value; 62 | const renderToStaticMarkup = 63 | context.renderToStaticMarkup || fallbackRenderToStaticMarkup; 64 | const hasId = id !== undefined; 65 | const hasDefaultLanguage = defaultLanguage !== undefined; 66 | const hasChildren = children !== undefined; 67 | const hasFunctionAsChild = typeof children === 'function'; 68 | 69 | const ignoreTranslateChildren = 70 | options.ignoreTranslateChildren !== undefined 71 | ? options.ignoreTranslateChildren 72 | : context.ignoreTranslateChildren; 73 | 74 | const isValidDefaultTranslation = 75 | hasChildren && hasId && hasDefaultLanguage; 76 | 77 | const shouldAddDefaultTranslation = 78 | isValidDefaultTranslation && 79 | !hasFunctionAsChild && 80 | !ignoreTranslateChildren; 81 | 82 | if (shouldAddDefaultTranslation) { 83 | const translation = renderToStaticMarkup(children); 84 | context.addTranslationForLanguage && 85 | context.addTranslationForLanguage( 86 | { 87 | // $FlowFixMe: flow complains that type of id can't be undefined, but we already guard against this in the checks above 88 | [id]: translation 89 | }, 90 | defaultLanguage 91 | ); 92 | } 93 | } 94 | 95 | renderChildren() { 96 | const { context, id = '', options, data } = this.props; 97 | 98 | return typeof this.props.children === 'function' 99 | ? this.props.children(context) 100 | : context.translate && (context.translate(id, data, options): any); 101 | } 102 | 103 | render() { 104 | return this.renderChildren(); 105 | } 106 | } 107 | 108 | export const Translate = (props: TranslateProps) => ( 109 | 110 | {context => } 111 | 112 | ); 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-localize-redux", 3 | "version": "3.5.3", 4 | "description": "Localization library for handling translations in React", 5 | "main": "./lib/index.js", 6 | "module": "./es/index.js", 7 | "jsnext:main": "./es/index.js", 8 | "typings": "lib/index.d.ts", 9 | "scripts": { 10 | "build": "npm run build:commonjs && npm run build:es && npm run copy:flow && npm run build:umd && npm run build:umd:min && npm run copy:ts", 11 | "build:commonjs": "rimraf lib && cross-env BABEL_ENV=commonjs babel ./src -d lib", 12 | "build:es": "rimraf es && cross-env BABEL_ENV=es babel ./src -d es", 13 | "build:umd": "rimraf dist && webpack --env.dev --output-filename dist/ReactLocalizeRedux.js", 14 | "build:umd:min": "webpack --env.prod --output-filename dist/ReactLocalizeRedux.min.js", 15 | "copy:flow": "ncp ./src/index.js.flow ./lib/index.js.flow && ncp ./src/index.js.flow ./es/index.js.flow", 16 | "copy:ts": "ncp ./src/index.d.ts ./lib/index.d.ts && ncp ./src/index.d.ts ./es/index.d.ts", 17 | "coverage": "jest && codecov", 18 | "prepublish": "npm run build", 19 | "test": "cross-env BABEL_ENV=test jest", 20 | "test:dev": "jest --watch", 21 | "flow": "flow", 22 | "types": "flow-typed install -i dev", 23 | "prettier": "prettier --write src/**/*", 24 | "upload:demo": "codesandbox ./demo" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/ryandrewjohnson/react-localize-redux.git" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "redux", 33 | "intl", 34 | "i18n", 35 | "internationalization", 36 | "locale", 37 | "localization", 38 | "globalization" 39 | ], 40 | "tags": [ 41 | "react", 42 | "redux", 43 | "i18n", 44 | "localization" 45 | ], 46 | "author": "Ryan Johnson", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/ryandrewjohnson/react-localize-redux/issues" 50 | }, 51 | "homepage": "https://github.com/ryandrewjohnson/react-localize-redux#readme", 52 | "peerDependencies": { 53 | "react": "^16.0.0" 54 | }, 55 | "devDependencies": { 56 | "babel-cli": "^6.26.0", 57 | "babel-core": "^6.26.3", 58 | "babel-jest": "^18.0.0", 59 | "babel-loader": "^6.2.10", 60 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 61 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 62 | "babel-plugin-transform-react-remove-prop-types": "^0.4.13", 63 | "babel-preset-es2015-no-commonjs": "0.0.2", 64 | "babel-preset-flow": "^6.23.0", 65 | "babel-preset-react": "^6.16.0", 66 | "babel-preset-stage-0": "^6.16.0", 67 | "codecov": "^3.0.2", 68 | "codesandbox": "^1.2.10", 69 | "cross-env": "^3.1.4", 70 | "enzyme": "^3.9.0", 71 | "enzyme-adapter-react-16": "^1.12.1", 72 | "flow-bin": "^0.70.0", 73 | "flow-copy-source": "^1.3.0", 74 | "flow-typed": "^2.4.0", 75 | "immutable": "^3.8.2", 76 | "jest": "^22.4.3", 77 | "json-loader": "^0.5.4", 78 | "ncp": "^2.0.0", 79 | "prettier": "1.12.1", 80 | "progress-bar-webpack-plugin": "^1.9.1", 81 | "raf": "^3.3.2", 82 | "react": "^16.3.1", 83 | "react-addons-test-utils": "^15.6.2", 84 | "react-dom": "^16.3.1", 85 | "react-test-renderer": "^16.0.0", 86 | "redux": "^3.6.0", 87 | "rimraf": "^2.5.4", 88 | "webpack": "^2.2.0-rc.2", 89 | "webpack-config-utils": "^2.3.0" 90 | }, 91 | "jest": { 92 | "verbose": true, 93 | "roots": [ 94 | "tests/" 95 | ], 96 | "modulePaths": [ 97 | "src/", 98 | "node_modules" 99 | ], 100 | "setupFiles": [ 101 | "raf/polyfill" 102 | ], 103 | "coverageDirectory": "./coverage/", 104 | "collectCoverage": true 105 | }, 106 | "dependencies": { 107 | "create-react-context": "^0.2.2", 108 | "flat": "^2.0.1", 109 | "hoist-non-react-statics": "^3.0.1", 110 | "prop-types": "^15.6.1", 111 | "reselect": "^3.0.1" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ba3d9a91a007e55cdce50cf2f6cdcbeb 2 | // flow-typed version: cab04034e7/redux_v3.x.x/flow_>=v0.33.x <=v0.54.x 3 | 4 | declare module 'redux' { 5 | 6 | /* 7 | 8 | S = State 9 | A = Action 10 | D = Dispatch 11 | 12 | */ 13 | 14 | declare export type DispatchAPI = (action: A) => A; 15 | declare export type Dispatch }> = DispatchAPI; 16 | 17 | declare export type MiddlewareAPI> = { 18 | dispatch: D; 19 | getState(): S; 20 | }; 21 | 22 | declare export type Store> = { 23 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 24 | dispatch: D; 25 | getState(): S; 26 | subscribe(listener: () => void): () => void; 27 | replaceReducer(nextReducer: Reducer): void 28 | }; 29 | 30 | declare export type Reducer = (state: S | void, action: A) => S; 31 | 32 | declare export type CombinedReducer = (state: $Shape & {} | void, action: A) => S; 33 | 34 | declare export type Middleware> = 35 | (api: MiddlewareAPI) => 36 | (next: D) => D; 37 | 38 | declare export type StoreCreator> = { 39 | (reducer: Reducer, enhancer?: StoreEnhancer): Store; 40 | (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; 41 | }; 42 | 43 | declare export type StoreEnhancer> = (next: StoreCreator) => StoreCreator; 44 | 45 | declare export function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store; 46 | declare export function createStore(reducer: Reducer, preloadedState?: S, enhancer?: StoreEnhancer): Store; 47 | 48 | declare export function applyMiddleware(...middlewares: Array>): StoreEnhancer; 49 | 50 | declare export type ActionCreator = (...args: Array) => A; 51 | declare export type ActionCreators = { [key: K]: ActionCreator }; 52 | 53 | declare export function bindActionCreators, D: DispatchAPI>(actionCreator: C, dispatch: D): C; 54 | declare export function bindActionCreators, D: DispatchAPI>(actionCreators: C, dispatch: D): C; 55 | 56 | declare export function combineReducers(reducers: O): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; 57 | 58 | declare export function compose(ab: (a: A) => B): (a: A) => B 59 | declare export function compose( 60 | bc: (b: B) => C, 61 | ab: (a: A) => B 62 | ): (a: A) => C 63 | declare export function compose( 64 | cd: (c: C) => D, 65 | bc: (b: B) => C, 66 | ab: (a: A) => B 67 | ): (a: A) => D 68 | declare export function compose( 69 | de: (d: D) => E, 70 | cd: (c: C) => D, 71 | bc: (b: B) => C, 72 | ab: (a: A) => B 73 | ): (a: A) => E 74 | declare export function compose( 75 | ef: (e: E) => F, 76 | de: (d: D) => E, 77 | cd: (c: C) => D, 78 | bc: (b: B) => C, 79 | ab: (a: A) => B 80 | ): (a: A) => F 81 | declare export function compose( 82 | fg: (f: F) => G, 83 | ef: (e: E) => F, 84 | de: (d: D) => E, 85 | cd: (c: C) => D, 86 | bc: (b: B) => C, 87 | ab: (a: A) => B 88 | ): (a: A) => G 89 | declare export function compose( 90 | gh: (g: G) => H, 91 | fg: (f: F) => G, 92 | ef: (e: E) => F, 93 | de: (d: D) => E, 94 | cd: (c: C) => D, 95 | bc: (b: B) => C, 96 | ab: (a: A) => B 97 | ): (a: A) => H 98 | declare export function compose( 99 | hi: (h: H) => I, 100 | gh: (g: G) => H, 101 | fg: (f: F) => G, 102 | ef: (e: E) => F, 103 | de: (d: D) => E, 104 | cd: (c: C) => D, 105 | bc: (b: B) => C, 106 | ab: (a: A) => B 107 | ): (a: A) => I 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/LocalizeProvider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { 4 | localizeReducer, 5 | getActiveLanguage, 6 | getOptions, 7 | getTranslationsForActiveLanguage, 8 | type LocalizeState, 9 | type Action, 10 | initialize as initializeAC, 11 | INITIALIZE, 12 | InitializePayload 13 | } from './localize'; 14 | import { 15 | LocalizeContext, 16 | type LocalizeContextProps, 17 | getContextPropsFromState 18 | } from './LocalizeContext'; 19 | import { storeDidChange } from './utils'; 20 | 21 | type LocalizeProviderState = { 22 | localize: LocalizeState 23 | }; 24 | 25 | export type LocalizeProviderProps = { 26 | store?: any, 27 | getState?: Function, 28 | initialize?: InitializePayload, 29 | children: any 30 | }; 31 | 32 | export class LocalizeProvider extends Component< 33 | LocalizeProviderProps, 34 | LocalizeProviderState 35 | > { 36 | unsubscribeFromStore: Function; 37 | getContextPropsSelector: any; 38 | contextProps: LocalizeContextProps; 39 | 40 | constructor(props: LocalizeProviderProps) { 41 | super(props); 42 | 43 | const dispatch = this.props.store 44 | ? this.props.store.dispatch 45 | : this.dispatch.bind(this); 46 | 47 | this.getContextPropsSelector = getContextPropsFromState(dispatch); 48 | 49 | const initialState = 50 | this.props.initialize !== undefined 51 | ? localizeReducer(undefined, { 52 | type: INITIALIZE, 53 | payload: this.props.initialize 54 | }) 55 | : localizeReducer(undefined, ({}: any)); 56 | 57 | this.state = { 58 | localize: initialState 59 | }; 60 | } 61 | 62 | componentDidMount() { 63 | this.initExternalStore(); 64 | this.subscribeToExternalStore(); 65 | } 66 | 67 | componentWillUnmount() { 68 | this.unsubscribeFromStore && this.unsubscribeFromStore(); 69 | } 70 | 71 | initExternalStore() { 72 | const { store, initialize } = this.props; 73 | if (store && initialize) { 74 | store.dispatch(initializeAC(initialize)); 75 | } 76 | } 77 | 78 | subscribeToExternalStore() { 79 | const { store } = this.props; 80 | if (store) { 81 | this.unsubscribeFromStore = storeDidChange( 82 | store, 83 | this.onStateDidChange.bind(this) 84 | ); 85 | } 86 | } 87 | 88 | onStateDidChange(prevState: LocalizeProviderState) { 89 | if (!this.props.store) { 90 | return; 91 | } 92 | const getState = this.props.getState || (state => state.localize); 93 | 94 | const prevLocalizeState = prevState && getState(prevState); 95 | const curLocalizeState = getState(this.props.store.getState()); 96 | 97 | const prevActiveLanguage = 98 | prevState && getActiveLanguage(prevLocalizeState); 99 | const curActiveLanguage = getActiveLanguage(curLocalizeState); 100 | 101 | const prevOptions = prevState && getOptions(prevLocalizeState); 102 | const curOptions = getOptions(curLocalizeState); 103 | 104 | const prevTranslations = 105 | prevState && getTranslationsForActiveLanguage(prevLocalizeState); 106 | const curTranslations = getTranslationsForActiveLanguage(curLocalizeState); 107 | 108 | const hasActiveLangaugeChanged = 109 | (prevActiveLanguage && prevActiveLanguage.code) !== 110 | (curActiveLanguage && curActiveLanguage.code); 111 | const hasOptionsChanged = prevOptions !== curOptions; 112 | const hasTranslationsChanged = prevTranslations !== curTranslations; 113 | 114 | if ( 115 | hasActiveLangaugeChanged || 116 | hasOptionsChanged || 117 | hasTranslationsChanged 118 | ) { 119 | this.setState({ localize: curLocalizeState }); 120 | } 121 | } 122 | 123 | dispatch(action: any) { 124 | this.setState(prevState => { 125 | return { 126 | localize: localizeReducer(prevState.localize, action) 127 | }; 128 | }); 129 | } 130 | 131 | render() { 132 | this.contextProps = this.getContextPropsSelector(this.state.localize); 133 | 134 | return ( 135 | 136 | {this.props.children} 137 | 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType, Component } from 'react'; 3 | import type { Selector, SelectorCreator } from 'reselect'; 4 | 5 | import type { Language as _Language } from './localize'; 6 | import type { NamedLanguage as _NamedLanguage } from './localize'; 7 | import type { InitializeOptions as _InitializeOptions } from './localize'; 8 | import type { TranslateOptions as _TranslateOptions } from './localize'; 9 | import type { LocalizeState as _LocalizeState } from './localize'; 10 | import type { Translations as _Translations } from './localize'; 11 | import type { SingleLanguageTranslation as _SingleLanguageTranslation } from './localize'; 12 | import type { MultipleLanguageTranslation as _MultipleLanguageTranslation } from './localize'; 13 | import type { TranslateFunction as _TranslateFunction } from './localize'; 14 | 15 | import type { InitializeAction as _InitializeAction } from './localize'; 16 | import type { AddTranslationAction as _AddTranslationAction } from './localize'; 17 | import type { AddTranslationForLanguageAction as _AddTranslationForLanguageAction } from './localize'; 18 | import type { SetActiveLanguageAction as _SetActiveLanguageAction } from './localize'; 19 | import type { Action as _Action } from './localize'; 20 | import type { InitializePayload as _InitializePayload } from './localize'; 21 | import type { AddTranslationOptions as _AddTranslationOptions } from './localize'; 22 | 23 | import type { 24 | TranslateProps as _TranslateProps, 25 | TranslateChildFunction as _TranslateChildFunction 26 | } from './Translate'; 27 | import type { LocalizeContextProps as _LocalizeContextProps } from './LocalizeContext'; 28 | import type { LocalizeProviderProps as _LocalizeProviderProps } from './LocalizeProvider'; 29 | import type { Context } from 'create-react-context'; 30 | 31 | export type Language = _Language; 32 | export type NamedLanguage = _NamedLanguage; 33 | export type InitializeOptions = _InitializeOptions; 34 | export type TranslateOptions = _TranslateOptions; 35 | export type LocalizeState = _LocalizeState; 36 | export type Translations = _Translations; 37 | export type SingleLanguageTranslation = _SingleLanguageTranslation; 38 | export type MultipleLanguageTranslation = _MultipleLanguageTranslation; 39 | export type TranslateFunction = _TranslateFunction; 40 | 41 | export type InitializeAction = _InitializeAction; 42 | export type AddTranslationAction = _AddTranslationAction; 43 | export type AddTranslationForLanguageAction = _AddTranslationForLanguageAction; 44 | export type SetActiveLanguageAction = _SetActiveLanguageAction; 45 | export type Action = _Action; 46 | export type InitializePayload = _InitializePayload; 47 | export type AddTranslationOptions = _AddTranslationOptions; 48 | 49 | export type TranslateProps = _TranslateProps; 50 | export type TranslateChildFunction = _TranslateChildFunction; 51 | export type LocalizeContextProps = _LocalizeContextProps; 52 | export type LocalizeProviderProps = _LocalizeProviderProps; 53 | 54 | declare export function localizeReducer( 55 | state: LocalizeState, 56 | action: Action 57 | ): LocalizeState; 58 | 59 | declare export function initialize( 60 | payload: InitializePayload 61 | ): InitializeAction; 62 | 63 | declare export function addTranslation( 64 | translation: MultipleLanguageTranslation, 65 | options?: AddTranslationOptions 66 | ): AddTranslationAction; 67 | 68 | declare export function addTranslationForLanguage( 69 | translation: SingleLanguageTranslation, 70 | language: string 71 | ): AddTranslationForLanguageAction; 72 | 73 | declare export function setActiveLanguage( 74 | languageCode: string 75 | ): SetActiveLanguageAction; 76 | 77 | declare export function getTranslations(state: LocalizeState): Translations; 78 | 79 | declare export function getLanguages(state: LocalizeState): Language[]; 80 | 81 | declare export function getOptions(state: LocalizeState): InitializeOptions; 82 | 83 | declare export function getActiveLanguage(state: LocalizeState): Language; 84 | 85 | declare export function getTranslate(state: LocalizeState): TranslateFunction; 86 | 87 | declare export function withLocalize( 88 | WrappedComponent: ComponentType 89 | ): ComponentType<$Diff>; 90 | 91 | declare export var LocalizeContext: Context; 92 | 93 | declare export var LocalizeProvider: ComponentType; 94 | 95 | declare export var Translate: ComponentType; 96 | -------------------------------------------------------------------------------- /tests/LocalizeProvider.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import { createStore, combineReducers } from 'redux'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import { Map } from 'immutable'; 7 | import { LocalizeProvider } from '../src/LocalizeProvider'; 8 | import { localizeReducer } from '../src/localize'; 9 | import { getTranslate, getLanguages, withLocalize, Translate } from '../src'; 10 | import { defaultTranslateOptions } from '../src/localize'; 11 | 12 | Enzyme.configure({ adapter: new Adapter() }); 13 | 14 | describe('', () => { 15 | const initialState = { 16 | languages: [{ code: 'en', active: true }, { code: 'fr', active: false }], 17 | translations: { 18 | hello: ['Hello', 'Hello FR'], 19 | bye: ['Goodbye', 'Goodbye FR'], 20 | multiline: [null, ''], 21 | placeholder: ['Hey ${name}!', ''] 22 | }, 23 | options: defaultTranslateOptions 24 | }; 25 | 26 | const getMockStore = () => { 27 | return createStore( 28 | combineReducers({ 29 | localize: localizeReducer 30 | }) 31 | ); 32 | }; 33 | const getImmutableStore = () => { 34 | const reducer = (s, a) => Map({ localize: localizeReducer(s, a) }); 35 | return createStore(reducer, Map({ localize: initialState })); 36 | }; 37 | 38 | it('should set default values for localize state', () => { 39 | const wrapper = shallow( 40 | 41 |
Hello
42 |
43 | ); 44 | 45 | expect(wrapper.state().localize).toEqual(localizeReducer(undefined, {})); 46 | }); 47 | 48 | it('should set default context props', () => { 49 | const wrapper = shallow( 50 | 51 |
Hello
52 |
53 | ); 54 | 55 | wrapper.setState({ localize: initialState }); 56 | 57 | expect(wrapper.instance().contextProps).toMatchObject({ 58 | translate: getTranslate(initialState), 59 | languages: getLanguages(initialState), 60 | defaultLanguage: 'en', 61 | activeLanguage: initialState.languages[0] 62 | }); 63 | }); 64 | 65 | it('should not throw error when store prop when passed', () => { 66 | const store = getMockStore(); 67 | const wrapper = expect(() => { 68 | shallow( 69 | 70 |
Hello
71 |
72 | ); 73 | }).not.toThrow(); 74 | }); 75 | 76 | it('should allow passing a custom function to access state', () => { 77 | const store = getImmutableStore(); 78 | expect(() => { 79 | shallow( 80 | state.get('localize')} 83 | > 84 |
Hello
85 |
86 | ); 87 | }).not.toThrow(); 88 | }); 89 | 90 | it('should work with SSR', () => { 91 | class App extends React.Component { 92 | constructor(props) { 93 | super(props); 94 | 95 | this.props.initialize({ 96 | languages: [ 97 | { name: 'English', code: 'en' }, 98 | { name: 'French', code: 'fr' } 99 | ], 100 | translation: { 101 | hello: ['hello', 'alo'] 102 | }, 103 | options: { 104 | defaultLanguage: 'en', 105 | renderToStaticMarkup: ReactDOMServer.renderToStaticMarkup 106 | } 107 | }); 108 | } 109 | 110 | render() { 111 | return ( 112 |
113 | 114 |
115 | ); 116 | } 117 | } 118 | 119 | const LocalizedApp = withLocalize(App); 120 | 121 | const result = ReactDOMServer.renderToString( 122 | 137 | 138 | 139 | ); 140 | 141 | expect(result).toEqual('
hello
'); 142 | }); 143 | 144 | it('should work when store and initialize are passed to Provider', () => { 145 | const store = getMockStore(); 146 | const initializePayload = { 147 | languages: [ 148 | { name: 'English', code: 'en' }, 149 | { name: 'French', code: 'fr' } 150 | ], 151 | translation: { 152 | hello: ['hello', 'alo'] 153 | }, 154 | options: { 155 | defaultLanguage: 'en', 156 | renderToStaticMarkup: ReactDOMServer.renderToStaticMarkup 157 | } 158 | }; 159 | shallow( 160 | 161 |
Hello
162 |
163 | ); 164 | const translation = store.getState().localize.translations; 165 | expect(translation).toEqual(initializePayload.translation); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReactElement, 3 | ReactNode, 4 | Component as ReactComponent, 5 | ComponentType 6 | } from 'react'; 7 | 8 | export as namespace ReactLocalizeRedux; 9 | 10 | export interface Language { 11 | name?: string; 12 | code: string; 13 | active: boolean; 14 | } 15 | 16 | export interface NamedLanguage { 17 | name: string; 18 | code: string; 19 | } 20 | 21 | export interface Translations { 22 | [key: string]: string[]; 23 | } 24 | 25 | type TransFormFunction = ( 26 | data: Object, 27 | languageCodes: string[] 28 | ) => Translations; 29 | 30 | type MissingTranslationOptions = { 31 | translationId: string; 32 | languageCode: string; 33 | defaultTranslation: LocalizedElement; 34 | }; 35 | 36 | export type onMissingTranslationFunction = ( 37 | options: MissingTranslationOptions 38 | ) => string | LocalizedElement; 39 | 40 | type renderToStaticMarkupFunction = (element: any) => string; 41 | 42 | export interface InitializeOptions { 43 | renderToStaticMarkup: renderToStaticMarkupFunction | false; 44 | renderInnerHtml?: boolean; 45 | onMissingTranslation?: onMissingTranslationFunction; 46 | defaultLanguage?: string; 47 | ignoreTranslateChildren?: boolean; 48 | } 49 | 50 | export interface TranslateOptions { 51 | language?: string; 52 | renderInnerHtml?: boolean; 53 | onMissingTranslation?: onMissingTranslationFunction; 54 | ignoreTranslateChildren?: boolean; 55 | } 56 | 57 | export interface AddTranslationOptions { 58 | translationTransform?: TransFormFunction; 59 | } 60 | 61 | export interface LocalizeState { 62 | languages: Language[]; 63 | translations: Translations; 64 | options: InitializeOptions; 65 | } 66 | 67 | export interface LocalizeContextProps { 68 | translate: TranslateFunction; 69 | languages: Language[]; 70 | activeLanguage: Language; 71 | defaultLanguage: string; 72 | initialize: (payload: InitializePayload) => void; 73 | addTranslation: (translation: MultipleLanguageTranslation) => void; 74 | addTranslationForLanguage: ( 75 | translation: SingleLanguageTranslation, 76 | language: string 77 | ) => void; 78 | setActiveLanguage: (languageCode: string) => void; 79 | renderToStaticMarkup: (element: any) => string | false; 80 | } 81 | 82 | export interface LocalizeProviderProps { 83 | store?: any; 84 | getState?: (state: any) => LocalizeState; 85 | initialize?: InitializePayload; 86 | children: any; 87 | } 88 | 89 | export interface TranslatedLanguage { 90 | [key: string]: string; 91 | } 92 | 93 | export type LocalizedElement = ReactElement<'span'> | string; 94 | 95 | export interface LocalizedElementMap { 96 | [key: string]: LocalizedElement; 97 | } 98 | 99 | export interface TranslatePlaceholderData { 100 | [key: string]: string | number | React.ReactNode; 101 | } 102 | 103 | export type TranslateChildFunction = (context: LocalizeContextProps) => any; 104 | 105 | export interface TranslateProps { 106 | id?: string; 107 | options?: InitializeOptions; 108 | data?: TranslatePlaceholderData; 109 | children?: TranslateChildFunction | ReactNode; 110 | } 111 | 112 | export type TranslateValue = string | string[]; 113 | 114 | interface BaseAction { 115 | type: T; 116 | payload: P; 117 | } 118 | 119 | export type TranslateFunction = ( 120 | value: TranslateValue, 121 | data?: TranslatePlaceholderData, 122 | options?: TranslateOptions 123 | ) => LocalizedElement | LocalizedElementMap; 124 | 125 | export type InitializePayload = { 126 | languages: Array; 127 | translation?: Object; 128 | options?: InitializeOptions; 129 | }; 130 | 131 | type AddTranslationPayload = { 132 | translation: Object; 133 | translationOptions?: AddTranslationOptions; 134 | }; 135 | 136 | type AddTranslationForLanguagePayload = { 137 | translation: Object; 138 | language: string; 139 | }; 140 | 141 | type SetActiveLanguagePayload = { 142 | languageCode: string; 143 | }; 144 | 145 | export type SingleLanguageTranslation = { 146 | [key: string]: Object | string; 147 | }; 148 | 149 | export type MultipleLanguageTranslation = { 150 | [key: string]: Object | string[]; 151 | }; 152 | 153 | export type InitializeAction = BaseAction< 154 | '@@localize/INITIALIZE', 155 | InitializePayload 156 | >; 157 | export type AddTranslationAction = BaseAction< 158 | '@@localize/ADD_TRANSLATION', 159 | AddTranslationPayload 160 | >; 161 | export type AddTranslationForLanguageAction = BaseAction< 162 | '@@localize/ADD_TRANSLATION_FOR_LANGUAGE', 163 | AddTranslationForLanguagePayload 164 | >; 165 | export type SetActiveLanguageAction = BaseAction< 166 | '@@localize/SET_ACTIVE_LANGUAGE', 167 | SetActiveLanguagePayload 168 | >; 169 | 170 | export type Action = BaseAction< 171 | string, 172 | InitializePayload & 173 | AddTranslationPayload & 174 | AddTranslationForLanguagePayload & 175 | SetActiveLanguagePayload 176 | >; 177 | 178 | export type ActionLanguageCodes = Action & { languageCodes: string[] }; 179 | 180 | export function localizeReducer( 181 | state: LocalizeState | undefined, 182 | action: Action 183 | ): LocalizeState; 184 | 185 | export function initialize(payload: InitializePayload): InitializeAction; 186 | 187 | export function addTranslation( 188 | translation: MultipleLanguageTranslation, 189 | options?: AddTranslationOptions 190 | ): AddTranslationAction; 191 | 192 | export function addTranslationForLanguage( 193 | translation: SingleLanguageTranslation, 194 | language: string 195 | ): AddTranslationForLanguageAction; 196 | 197 | export function setActiveLanguage( 198 | languageCode: string 199 | ): SetActiveLanguageAction; 200 | 201 | export function getTranslations(state: LocalizeState): Translations; 202 | 203 | export function getLanguages(state: LocalizeState): Language[]; 204 | 205 | export function getOptions(state: LocalizeState): InitializeOptions; 206 | 207 | export function getActiveLanguage(state: LocalizeState): Language; 208 | 209 | export function getTranslate(state: LocalizeState): TranslateFunction; 210 | 211 | export function withLocalize( 212 | WrappedComponent: ComponentType 213 | ): ComponentType>>; 214 | 215 | export function TranslateChildFunction( 216 | context: LocalizeContextProps 217 | ): ReactNode; 218 | 219 | export const Translate: React.SFC; 220 | 221 | export class LocalizeProvider extends ReactComponent {} 222 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.5.3 (April 18, 2019) 2 | 3 | - Update typescript definition for onMissingTranslationFunction function 4 | 5 | ## 3.5.2 (January 24, 2019) 6 | 7 | - Update typescript definition for localizeReducer function 8 | 9 | ## 3.5.1 (November 9, 2018) 10 | 11 | - ensure that store from props is initialized with initialize from props. [#136](https://github.com/ryandrewjohnson/react-localize-redux/pull/136) 12 | 13 | ## 3.5.0 (October 17, 2018) 14 | 15 | - adds to withLocalize HOC automatic hoisting to non react statics using hoist-non-react-statics [#130](https://github.com/ryandrewjohnson/react-localize-redux/pull/130) 16 | 17 | ## 3.4.1 (September 28, 2018) 18 | 19 | - Remove remaining references to redux in source [#129](https://github.com/ryandrewjohnson/react-localize-redux/pull/129) 20 | 21 | ## 3.4.0 (September 12, 2018) 22 | 23 | - Add initialize prop to LocaizeProvider to support SSR [#127](https://github.com/ryandrewjohnson/react-localize-redux/pull/127) 24 | 25 | ## 3.3.2 (August 22, 2018) 26 | 27 | - Fix too eager templater reg exp [#118](https://github.com/ryandrewjohnson/react-localize-redux/pull/118) 28 | 29 | ## 3.3.1 (August 16, 2018) 30 | 31 | - Handle falsy values in data. [#117](https://github.com/ryandrewjohnson/react-localize-redux/pull/117) 32 | 33 | ## 3.3.0 (July 25, 2018) 34 | 35 | - Add `getState` prop to `LocalizeProvider` to allow for ImmutableJS support [#112](https://github.com/ryandrewjohnson/react-localize-redux/pull/112) 36 | 37 | ## 3.2.4 (July 19, 2018) 38 | 39 | - Fix issue where bad `getOptions` selector was causing unnecessary re-renders [#111](https://github.com/ryandrewjohnson/react-localize-redux/issues/111) 40 | 41 | ## 3.2.3 (July 13, 2018) 42 | 43 | - Fix issue with onMissingTranslation not handling defaultTranslation properly [#110](https://github.com/ryandrewjohnson/react-localize-redux/pull/110) 44 | 45 | ## 3.2.2 (July 11, 2018) 46 | 47 | - Fix bad import in TypeScript definition [#106](https://github.com/ryandrewjohnson/react-localize-redux/pull/106) 48 | 49 | ## 3.2.1 (July 9, 2018) 50 | 51 | - Fix issue where onMissingTranslation would through error when defaultLanguage is not set [#101](https://github.com/ryandrewjohnson/react-localize-redux/issues/101) 52 | 53 | ## 3.2.0 (June 24, 2018) 54 | 55 | - Allow React components as dynamic data arguments [#100](https://github.com/ryandrewjohnson/react-localize-redux/pull/100) 56 | - Ensure that addDefaultTranslation gets called [#99](https://github.com/ryandrewjohnson/react-localize-redux/pull/99) 57 | 58 | ## 3.1.2 (June 22, 2018) 59 | 60 | - Update the TypeScript definition for withLocalize [#98](https://github.com/ryandrewjohnson/react-localize-redux/pull/98) 61 | 62 | ## 3.1.1 (June 17, 2018) 63 | 64 | - Make performance imporvements to `getTranslationsForLanguage` and `getSingleToMultilanguageTranslation` method. [#92](https://github.com/ryandrewjohnson/react-localize-redux/pull/92) 65 | 66 | ## 3.1.0 (June 14, 2018) 67 | 68 | - Add [ignoreTranslateChildren](https://ryandrewjohnson.github.io/react-localize-redux-docs/#initialize) to initialize options. [#91](https://github.com/ryandrewjohnson/react-localize-redux-docs/pull/1) 69 | - Fix issue where Warning: Cannot update during an existing state transition message was appearing [#88](https://github.com/ryandrewjohnson/react-localize-redux/pull/88) 70 | 71 | ## 3.0.2 (June 11, 2018) 72 | 73 | - remove `console.log` in `LocalizeProvider` 74 | 75 | ## 3.0.1 (June 4, 2018) 76 | 77 | - Update package.json peerDependencies react version to 16.3.0 as the `Translate` component requires [getDerivedStateFromProps](https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops). 78 | 79 | ## 3.0.0 (June 3, 2018) 80 | 81 | - Now works without Redux by defualt. 82 | - Add [LoclaizeProvider](https://ryandrewjohnson.github.io/react-localize-redux-docs//#localizeprovider) a wrapper around React's [Context.Provider](https://reactjs.org/docs/context.html#provider) 83 | - Add [LocalizeContext](https://ryandrewjohnson.github.io/react-localize-redux-docs/#localizecontext) built on [React.createContext](https://reactjs.org/docs/context.html#reactcreatecontext). 84 | - Add [withLocalize](https://ryandrewjohnson.github.io/react-localize-redux-docs/#withlocalize) higher-order component 85 | - Add [onMissingTranslation](https://ryandrewjohnson.github.io/react-localize-redux-docs/#handle-missing-translations) initialize option that provides more control over handling missing translations. 86 | - Optionally supports Redux by passing redux store to `LocalizeProvider`. 87 | 88 | ### Breaking Changes 89 | 90 | - The Redux action creators `initialize`, `addTranslation`, `addTranslationForLanguage`, and `setActiveLanguage` have been removed. Instead they are now methods available on [LocalizeContext](https://ryandrewjohnson.github.io/react-localize-redux-docs/#localizecontext), and can be added to your component's as props using the [withLocalize](https://ryandrewjohnson.github.io/react-localize-redux-docs/#withlocalize) higher-order component. 91 | 92 | - The `translationTransform` option is no longer available as an initialize option. Instead [addTranslation](https://ryandrewjohnson.github.io/react-localize-redux-docs/#addtranslation) now takes an options object, which accepts the `translationTransform` function. 93 | 94 | - This made more sense as this allows for adding transformations specific to a single translation instead of globally setting a transformation that you'd be stuck using for all translations. 95 | 96 | - [initialize](https://ryandrewjohnson.github.io/react-localize-redux-docs/#initialize) now takes a single options argument instead of multiple arguments. 97 | 98 | - [Translate](https://ryandrewjohnson.github.io/react-localize-redux-docs/#render-props-api) render props API now takes a single options object as an argument instead of multiple arguments. 99 | 100 | - `renderInnerHtml` option now set to `false` by default instead of `true`. 101 | 102 | - This is to mirror React's functionality where by default any children will be escaped by defualt. 103 | 104 | - Remove `showMissingTranslationMsg`, `missingTranslationMsg`, `missingTranslationCallback` initialize options. THe new `onMissingTranslation` option now covers all these scenarios. 105 | 106 | - Remove `setTranslations` action - instead pass `languages` to initialize. 107 | 108 | - Remove `localize` higher-order component - use new `withLoclize` higher-order component instead. Or if you only require access to `translate` function use `` component instead. 109 | 110 | - If using Redux, all state related to localize will be added under the `localize` key instead of `locale`. 111 | 112 | - If using Redux, `localeReducer` is now named `localizeReducer`. 113 | 114 | - Fix typos in `ADD_TRANSLATION_FOR_LANGUAGE` action [(Issue #65)](https://github.com/ryandrewjohnson/react-localize-redux/issues/65) 115 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Enzyme, { mount, shallow } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import * as utils from 'utils'; 5 | 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | 8 | describe('locale utils', () => { 9 | const defaultLanguage = { code: 'en' }; 10 | 11 | describe('getLocalizedElement', () => { 12 | 13 | it('should return element with localized string', () => { 14 | const translations = { test: 'Here is my test' }; 15 | const result = utils.getLocalizedElement({ 16 | translation: 'Here is my test' 17 | }); 18 | expect(result).toBe(translations.test); 19 | }); 20 | 21 | it('should render inner HTML when renderInnerHtml = true', () => { 22 | const translation = '

Here

is my test'; 23 | const wrapper = shallow(utils.getLocalizedElement({ 24 | translation, 25 | renderInnerHtml: true 26 | })); 27 | 28 | expect(wrapper.find('span').exists()).toBe(true); 29 | expect(wrapper.html()).toEqual(`${translation}`); 30 | }); 31 | 32 | it('should not render inner HTML when renderInnerHtml = false', () => { 33 | const translation = '

Here

is my test'; 34 | const result = utils.getLocalizedElement({ 35 | translation, 36 | renderInnerHtml: false 37 | }); 38 | expect(result).toBe(translation); 39 | }); 40 | 41 | it('should replace variables in translation string with data', () => { 42 | const translation = 'Hello ${ name }'; 43 | const result = utils.getLocalizedElement({ 44 | translation, 45 | renderInnerHtml: true, 46 | data: { name: 'Ted' } 47 | }); 48 | expect(result).toEqual('Hello Ted'); 49 | }); 50 | 51 | it('should handle React in data', () => { 52 | const Comp = () =>
ReactJS
53 | const translation = 'Hello ${ comp } data'; 54 | const result = utils.getLocalizedElement({ 55 | translation, 56 | data: { comp: } 57 | }); 58 | expect(mount(result).text()).toContain('ReactJS'); 59 | }) 60 | }); 61 | 62 | describe('hasHtmlTags', () => { 63 | it('should return true if string contains html tag', () => { 64 | const value1 = 'Here is some text with html tag'; 65 | const value2 = 'Here is a
Link'; 66 | expect(utils.hasHtmlTags(value1)).toBe(true); 67 | expect(utils.hasHtmlTags(value2)).toBe(true); 68 | }); 69 | }); 70 | 71 | describe('templater', () => { 72 | it('should replace all variables in the string', () => { 73 | const data = { name: 'Ryan', country: 'Canada' }; 74 | const before = 'Hi my name is ${ name } and I live in ${ country }'; 75 | const after = 'Hi my name is Ryan and I live in Canada'; 76 | const result = utils.templater(before, data); 77 | expect(result).toEqual(after); 78 | }); 79 | 80 | it('should replace all variables in the string even when variable are stucked together', () => { 81 | const data = { name: 'Ryan', type: 'Air' }; 82 | const before = 'Hi my airline company is ${name}${type}'; 83 | const after = 'Hi my airline company is RyanAir'; 84 | const result = utils.templater(before, data); 85 | expect(result).toEqual(after); 86 | }); 87 | 88 | it('should not modify string if no data param is provided', () => { 89 | const before = 'Hi my name is ${ name } and I live in ${ country }'; 90 | const result = utils.templater(before); 91 | expect(result).toEqual(before); 92 | }); 93 | 94 | it('should return an array if React components are passed in data', () => { 95 | const Comp = () =>
Test
; 96 | const data = { comp: }; 97 | const before = 'Hello this is a ${ comp } translation'; 98 | const after = ['Hello this is a ', , ' translation']; 99 | const result = utils.templater(before, data); 100 | expect(result).toEqual(after); 101 | }); 102 | 103 | it('should handle falsy data values (except undefined)', () => { 104 | const data = { zero: 0, empty: '' }; 105 | const before = 'Number ${zero}, empty ${empty}'; 106 | const after = 'Number 0, empty '; 107 | const result = utils.templater(before, data); 108 | expect(result).toEqual(after); 109 | }); 110 | }); 111 | 112 | describe('getIndexForLanguageCode', () => { 113 | it('should return the index for matching language code', () => { 114 | const languages = [{ code: 'en' }, { code: 'fr' }, { code: 'ne' }]; 115 | const result = utils.getIndexForLanguageCode('fr', languages); 116 | expect(result).toBe(1); 117 | }); 118 | 119 | it('should return -1 when no match is found', () => { 120 | const languages = [{ code: 'en' }, { code: 'fr' }, { code: 'ne' }]; 121 | const result = utils.getIndexForLanguageCode('zw', languages); 122 | expect(result).toBe(-1); 123 | }); 124 | }); 125 | 126 | describe('objectValuesToString', () => { 127 | let translationData = {}; 128 | 129 | beforeEach(() => { 130 | translationData = { 131 | one: ['1', '2', '3'], 132 | two: ['4', '5', '6'] 133 | }; 134 | }); 135 | 136 | describe('Object.values defined', () => { 137 | it('should return an stingified array of translation array data', () => { 138 | const result = utils.objectValuesToString(translationData); 139 | expect(result).toEqual('1,2,3,4,5,6'); 140 | }); 141 | }); 142 | 143 | describe('Object.values undefined', () => { 144 | it('should return an stingified array of translation array data', () => { 145 | Object.values = undefined; 146 | const result = utils.objectValuesToString(translationData); 147 | expect(result).toEqual('1,2,3,4,5,6'); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('validateOptions', () => { 153 | it('should return options object when valid', () => { 154 | const options = { 155 | renderInnerHtml: false, 156 | defaultLanguage: 'en', 157 | translationTransform: (data, codes) => ({}), 158 | renderToStaticMarkup: false 159 | }; 160 | const result = utils.validateOptions(options); 161 | expect(result).toEqual(options); 162 | }); 163 | 164 | it('should throw error if onMissingTranslation is not a function', () => { 165 | const options = { 166 | renderInnerHtml: false, 167 | defaultLanguage: 'en', 168 | onMissingTranslation: false, 169 | renderToStaticMarkup: false 170 | }; 171 | expect(() => utils.validateOptions(options)).toThrow(); 172 | }); 173 | 174 | it('should throw error if renderToStaticMarkup is not a function', () => { 175 | const options = { 176 | renderInnerHtml: false, 177 | defaultLanguage: 'en', 178 | renderToStaticMarkup: '' 179 | }; 180 | expect(() => utils.validateOptions(options)).toThrow(); 181 | }); 182 | 183 | it('should throw error if renderToStaticMarkup is not false', () => { 184 | const options = { 185 | renderInnerHtml: false, 186 | defaultLanguage: 'en', 187 | renderToStaticMarkup: true 188 | }; 189 | expect(() => utils.validateOptions(options)).toThrow(); 190 | }); 191 | }); 192 | 193 | describe('get', () => { 194 | 195 | const obj = { a: { b: { c: 'd' } } }; 196 | 197 | it('gets value at path', () => { 198 | const path = 'a.b.c'; 199 | expect(utils.get(obj, path)).toBe('d'); 200 | }); 201 | 202 | it('returns passed default value', () => { 203 | const path = 'foo'; 204 | expect(utils.get(obj, path, 'default')).toBe('default'); 205 | }); 206 | 207 | it('falls back to undefined', () => { 208 | const path = 'foo'; 209 | expect(utils.get(obj, path)).toBeUndefined(); 210 | }) 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { 4 | defaultTranslateOptions, 5 | type MultipleLanguageTranslation 6 | } from './localize'; 7 | import type { 8 | TranslatePlaceholderData, 9 | TranslatedLanguage, 10 | Translations, 11 | InitializeOptions, 12 | LocalizedElement, 13 | Language 14 | } from './localize'; 15 | 16 | type LocalizedElementOptions = { 17 | translation: string, 18 | data: TranslatePlaceholderData, 19 | renderInnerHtml: boolean 20 | }; 21 | 22 | export const getLocalizedElement = ( 23 | options: LocalizedElementOptions 24 | ): LocalizedElement => { 25 | const { translation, data, renderInnerHtml } = options; 26 | 27 | const translatedValueOrArray = templater(translation, data); 28 | 29 | // if result of templater is string, do the usual stuff 30 | if (typeof translatedValueOrArray === 'string') { 31 | return renderInnerHtml === true && hasHtmlTags(translatedValueOrArray) 32 | ? React.createElement('span', { 33 | dangerouslySetInnerHTML: { __html: translatedValueOrArray } 34 | }) 35 | : translatedValueOrArray; 36 | } 37 | 38 | // at this point we know we have react components; 39 | // check if there are HTMLTags in the translation (not allowed) 40 | for (let portion of translatedValueOrArray) { 41 | if (typeof portion === 'string' && hasHtmlTags(portion)) { 42 | warning( 43 | 'HTML tags in the translation string are not supported when passing React components as arguments to the translation.' 44 | ); 45 | return ''; 46 | } 47 | } 48 | 49 | // return as Element 50 | return React.createElement('span', null, ...translatedValueOrArray); 51 | }; 52 | 53 | export const hasHtmlTags = (value: string): boolean => { 54 | const pattern = /(&[^\s]*;|<\/?\w+((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[\^'">\s]+))?)+\s*|\s*)\/?>)/; 55 | return value.search(pattern) >= 0; 56 | }; 57 | 58 | /** 59 | * @func templater 60 | * @desc A poor mans template parser 61 | * @param {string} strings The template string 62 | * @param {object} data The data that should be inserted in template 63 | * @return {string} The template string with the data merged in 64 | */ 65 | export const templater = ( 66 | strings: string, 67 | data: Object = {} 68 | ): string | string[] => { 69 | if (!strings) return ''; 70 | 71 | // ${**} 72 | // brackets to include it in the result of .split() 73 | const genericPlaceholderPattern = '(\\${\\s*[^\\s}]+\\s*})'; 74 | 75 | // split: from 'Hey ${name}' -> ['Hey', '${name}'] 76 | // filter: clean empty strings 77 | // map: replace ${prop} with data[prop] 78 | let splitStrings = strings 79 | .split(new RegExp(genericPlaceholderPattern, 'gmi')) 80 | .filter(str => !!str) 81 | .map(templatePortion => { 82 | let matched; 83 | for (let prop in data) { 84 | if (matched) break; 85 | const pattern = '\\${\\s*' + prop + '\\s*}'; 86 | const regex = new RegExp(pattern, 'gmi'); 87 | if (regex.test(templatePortion)) matched = data[prop]; 88 | } 89 | if (typeof matched === 'undefined') return templatePortion; 90 | return matched; 91 | }); 92 | 93 | // if there is a React element, return as array 94 | if (splitStrings.some(portion => React.isValidElement(portion))) { 95 | return splitStrings; 96 | } 97 | 98 | // otherwise concatenate all portions into the translated value 99 | return splitStrings.reduce((translated, portion) => { 100 | return translated + `${portion}`; 101 | }, ''); 102 | }; 103 | 104 | export const getIndexForLanguageCode = ( 105 | code: string, 106 | languages: Language[] 107 | ): number => { 108 | return languages.map(language => language.code).indexOf(code); 109 | }; 110 | 111 | export const objectValuesToString = (data: Object): string => { 112 | return !Object.values 113 | ? Object.keys(data) 114 | .map(key => data[key].toString()) 115 | .toString() 116 | : Object.values(data).toString(); 117 | }; 118 | 119 | export const validateOptions = ( 120 | options: InitializeOptions 121 | ): InitializeOptions => { 122 | if ( 123 | options.onMissingTranslation !== undefined && 124 | typeof options.onMissingTranslation !== 'function' 125 | ) { 126 | throw new Error( 127 | 'react-localize-redux: an invalid onMissingTranslation function was provided.' 128 | ); 129 | } 130 | 131 | if ( 132 | options.renderToStaticMarkup !== false && 133 | typeof options.renderToStaticMarkup !== 'function' 134 | ) { 135 | throw new Error(` 136 | react-localize-redux: initialize option renderToStaticMarkup is invalid. 137 | Please see https://ryandrewjohnson.github.io/react-localize-redux-docs/#initialize. 138 | `); 139 | } 140 | 141 | return options; 142 | }; 143 | 144 | export const getTranslationsForLanguage = ( 145 | language: Language, 146 | languages: Language[], 147 | translations: Translations 148 | ): TranslatedLanguage => { 149 | // no language! return no translations 150 | if (!language) { 151 | return {}; 152 | } 153 | 154 | const { code: languageCode } = language; 155 | const languageIndex = getIndexForLanguageCode(languageCode, languages); 156 | const keys = Object.keys(translations); 157 | const totalKeys = keys.length; 158 | const translationsForLanguage = {}; 159 | 160 | for (let i = 0; i < totalKeys; i++) { 161 | const key = keys[i]; 162 | translationsForLanguage[key] = translations[key][languageIndex]; 163 | } 164 | 165 | return translationsForLanguage; 166 | }; 167 | 168 | export const storeDidChange = ( 169 | store: any, 170 | onChange: (prevState: any) => void 171 | ) => { 172 | let currentState; 173 | 174 | function handleChange() { 175 | const nextState = store.getState(); 176 | if (nextState !== currentState) { 177 | onChange(currentState); 178 | currentState = nextState; 179 | } 180 | } 181 | 182 | const unsubscribe = store.subscribe(handleChange); 183 | handleChange(); 184 | return unsubscribe; 185 | }; 186 | 187 | export const getSingleToMultilanguageTranslation = ( 188 | language: string, 189 | languageCodes: string[], 190 | flattenedTranslations: Object, 191 | existingTranslations: Object 192 | ): Translations => { 193 | const languageIndex = languageCodes.indexOf(language); 194 | const translations = languageIndex >= 0 ? flattenedTranslations : {}; 195 | const keys = Object.keys(translations); 196 | const totalKeys = keys.length; 197 | const singleLanguageTranslations = {}; 198 | 199 | for (let i = 0; i < totalKeys; i++) { 200 | const key = keys[i]; 201 | // loop through each language, and for languages that don't match languageIndex 202 | // keep existing translation data, and for languageIndex store new translation data 203 | const translationValues = languageCodes.map((code, index) => { 204 | const existingValues = existingTranslations[key] || []; 205 | 206 | return index === languageIndex 207 | ? flattenedTranslations[key] 208 | : existingValues[index]; 209 | }); 210 | 211 | singleLanguageTranslations[key] = translationValues; 212 | } 213 | 214 | return singleLanguageTranslations; 215 | }; 216 | 217 | export const get = ( 218 | obj: Object, 219 | path: string, 220 | defaultValue: any = undefined 221 | ) => { 222 | const pathArr = path.split('.').filter(Boolean); 223 | return pathArr.reduce((ret, key) => { 224 | return ret && ret[key] ? ret[key] : defaultValue; 225 | }, obj); 226 | }; 227 | 228 | // Thanks react-redux for utility function 229 | // https://github.com/reactjs/react-redux/blob/master/src/utils/warning.js 230 | export const warning = (message: string) => { 231 | if (typeof console !== 'undefined' && typeof console.error === 'function') { 232 | console.error(message); 233 | } 234 | 235 | try { 236 | // This error was thrown as a convenience so that if you enable 237 | // "break on all exceptions" in your console, 238 | // it would pause the execution at this line. 239 | throw new Error(message); 240 | } catch (e) {} 241 | }; 242 | -------------------------------------------------------------------------------- /MIGRATING.md: -------------------------------------------------------------------------------- 1 | # Migrating from v2 to v3 2 | 3 | If you are migrating from v2 the first thing you need to decide whether you still need Redux. 4 | If you don't need to have your translations in your Redux store, then you may consider following 5 | the [Getting Started](https://ryandrewjohnson.github.io/react-localize-redux-docs/#getting-started) docs for the non-redux implementation guide. 6 | 7 | For a list of all breaking changes see the [Change Log](CHANGELOG.md). 8 | 9 | ## Add Reducer 10 | 11 | If you're no longer using redux then you no longer need to `createStore`, which means you don't need a reducer. If you are planning 12 | on sticking with Redux the reducer function is now exported as `localizeReducer` instead of `localeReducer`. 13 | 14 | ### v2 15 | 16 | ```jsx 17 | import { createStore, combineReducers } from 'redux'; 18 | import { user } from './reducers'; 19 | import { localeReducer as locale } from 'react-localize-redux'; 20 | 21 | const store = createStore(combineReducers({ user, locale })); 22 | ``` 23 | 24 | ### v3 25 | 26 | ```jsx 27 | import { createStore, combineReducers } from 'redux'; 28 | import { localizeReducer } from 'react-localize-redux'; 29 | 30 | const store = createStore( 31 | combineReducers({ 32 | user, 33 | localize: localizeReducer 34 | }) 35 | ); 36 | ``` 37 | 38 | ## Add LocalizeProvider 39 | 40 | There is no longer a dependency on `react-redux`'s ``. Instead you will need to wrap your app with [LocalizeProvider](https://ryandrewjohnson.github.io/react-localize-redux-docs/#localizeprovider). 41 | 42 | ### v2 43 | 44 | ```jsx 45 | import React from 'react'; 46 | import { createStore, combineReducers } from 'redux'; 47 | import { user } from './reducers'; 48 | import { localeReducer as locale } from 'react-localize-redux'; 49 | 50 | const store = createStore(combineReducers({ user, locale })); 51 | 52 | const App = props => { 53 | return ...; 54 | }; 55 | ``` 56 | 57 | ### v3 58 | 59 | > Note: If you're not using redux then you don't need to pass `store` to LocalizeProvider 60 | 61 | ```jsx 62 | import React from 'react'; 63 | import { render } from 'react-dom'; 64 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 65 | import { createStore, combineReducers } from 'redux'; 66 | import { LocalizeProvider, localizeReducer } from 'react-localize-redux'; 67 | import Main from './Main'; 68 | 69 | const store = createStore(combineReducers({ user, locale })); 70 | 71 | const App = props => ( 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | 79 | render(, document.getElementById('root')); 80 | ``` 81 | 82 | ## Initialize 83 | 84 | ### v2 85 | 86 | ```jsx 87 | import { initialize } from 'react-localize-redux'; 88 | 89 | const languages = ['en', 'fr', 'es']; 90 | store.dispatch(initialize(languages)); 91 | ``` 92 | 93 | ### v3 94 | 95 | You no longer need to dispatch actions in v3. Instead you need to wrap you component with the [withLocalize](https://ryandrewjohnson.github.io/react-localize-redux-docs/#withlocalize) higher-order component. 96 | This will add all the props from [LocalizeContext](https://ryandrewjohnson.github.io/react-localize-redux-docs/#localizecontext) to your component including [initialize](https://ryandrewjohnson.github.io/react-localize-redux-docs/#initialize). 97 | 98 | You are also required to pass a reference to ReactDOMServer's [renderToStaticMarkup](https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup) 99 | as an option to `initialize`. See [Why do I need to pass renderToStaticMarkup to initialize?](https://ryandrewjohnson.github.io/react-localize-redux-docs/#why-do-i-need-to-pass-rendertostaticmarkup-to-initialize) for more info. 100 | 101 | ```jsx 102 | import React from 'react'; 103 | import { renderToStaticMarkup } from 'react-dom/server'; 104 | import { withLocalize } from 'react-localize-redux'; 105 | import globalTranslations from './translations/global.json'; 106 | 107 | class Main extends React.Component { 108 | constructor(props) { 109 | super(props); 110 | 111 | this.props.initialize({ 112 | languages: [ 113 | { name: 'English', code: 'en' }, 114 | { name: 'French', code: 'fr' } 115 | ], 116 | translation: globalTranslations, 117 | options: { renderToStaticMarkup } 118 | }); 119 | } 120 | 121 | render() { 122 | // render Main layout component 123 | } 124 | } 125 | 126 | export default withLocalize(Main); 127 | ``` 128 | 129 | ## Add translation data 130 | 131 | ### v2 132 | 133 | ```jsx 134 | import { addTranslation } from 'react-localize-redux'; 135 | import { addTranslationForLanguage } from 'react-localize-redux'; 136 | 137 | const translations = { 138 | greeting: ['Hello', 'Bonjour', 'Hola'], 139 | farewell: ['Goodbye', 'Au revoir', 'Adiós'] 140 | }; 141 | 142 | store.dispatch(addTranslation(translations)); 143 | ``` 144 | 145 | ### v3 146 | 147 | To add multi language translations, use addTranslation: 148 | 149 | ```jsx 150 | import React from 'react'; 151 | import { withLocalize } from 'react-localize-redux'; 152 | import movieTranslations from './translations/movies.json'; 153 | 154 | class Movies extends React.Component { 155 | constructor(props) { 156 | super(props); 157 | 158 | this.props.addTranslation(movieTranslations); 159 | } 160 | 161 | render() { 162 | // render movie component 163 | } 164 | } 165 | 166 | export default withLocalize(Movies); 167 | ``` 168 | 169 | To add single language translations, use addTranslationForLanguage: 170 | 171 | ```jsx 172 | import frenchMovieTranslations from './translations/fr.movies.json'; 173 | 174 | this.props.addTranslationForLanguage(frenchMovieTranslations, 'fr'); 175 | ``` 176 | 177 | ## Change language 178 | 179 | ### v2 180 | 181 | ```jsx 182 | import { setActiveLanguage } from 'react-localize-redux'; 183 | 184 | store.dispatch(setActiveLanguage('fr')); 185 | ``` 186 | 187 | ### v3 188 | 189 | ```jsx 190 | import React from 'react'; 191 | import { withLocalize } from 'react-localize-redux'; 192 | 193 | const LanguageToggle = ({ languages, activeLanguage, setActiveLanguage }) => ( 194 |
    195 | {languages.map(lang => ( 196 |
  • 197 | 200 |
  • 201 | ))} 202 |
203 | ); 204 | 205 | export default withLocalize(LanguageToggle); 206 | ``` 207 | 208 | ## onMissingTranslation initialize option 209 | 210 | If you are using any of the below `initialize` options from v2 they are no longer available as they can all be covered by 211 | the new [onMissingTranslation](https://ryandrewjohnson.github.io/react-localize-redux-docs/#initialize) option. 212 | 213 | Instead of `showMissingTranslationMsg = false`: 214 | 215 | ```jsx 216 | import { renderToStaticMarkup } from 'react-dom/server'; 217 | 218 | this.props.initialize({ 219 | languages: [{ name: 'English', code: 'en' }, { name: 'French', code: 'fr' }], 220 | options: { 221 | renderToStaticMarkup, 222 | onMissingTranslation: () => '' 223 | } 224 | }); 225 | ``` 226 | 227 | Instead of `missingTranslationMsg`: 228 | 229 | ```jsx 230 | import React from 'react'; 231 | import { Translate } from 'react-localize-redux'; 232 | 233 | const onMissingTranslation = ({ translationId, languageCode }) => { 234 | return `Nada for ${translationId} - ${languageCode}`; 235 | }; 236 | 237 | const Missing = props => ( 238 | 239 | ); 240 | ``` 241 | 242 | Instead of `missingTranslationCallback` just add your callback to `onMissingTranslation` function. 243 | 244 | ## The `translationTransform` option has moved 245 | 246 | The `translationTransform` option is no longer available as an `initialize` option. Instead you can 247 | pass [translationTransform](https://ryandrewjohnson.github.io/react-localize-redux-docs/#custom-translation-format) as an option to [addTranslation](https://ryandrewjohnson.github.io/react-localize-redux-docs/#addtranslation) as an option. This allows you to target a transform to specific translation data instead of applying translations globally. 248 | 249 | ```jsx 250 | import React from 'react'; 251 | import { withLocalize } from 'react-localize-redux'; 252 | 253 | const transformFunction = ( 254 | translationData: Object, 255 | languagesCodes: string[] 256 | ) => { 257 | // Your transformation logic goes here... 258 | }; 259 | 260 | class CustomStuff extends React.Component { 261 | constructor(props) { 262 | super(props); 263 | 264 | this.props.addTranslation(customTranslation, { 265 | translationTransform: transformFunction 266 | }); 267 | } 268 | } 269 | 270 | export default withLocalize(CustomStuff); 271 | ``` 272 | 273 | ## The `renderInnerHtml` option is now set to `false` by default 274 | 275 | If you have HTML markup in your translations then you will need to set this to true. This is to mirror React's functionality where by default any HTML will be escaped by defualt. 276 | 277 | ## localize has been removed 278 | 279 | The `localize` higher-order component has been removed. Instead use the new [withLocalize](https://ryandrewjohnson.github.io/react-localize-redux-docs/#withlocalize) higher-order component to get access 280 | to [activeLanguage](https://ryandrewjohnson.github.io/react-localize-redux-docs/#activelanguage), and the [Translate](https://ryandrewjohnson.github.io/react-localize-redux-docs/#translate-2) component 281 | instead of `translate` function. 282 | 283 | ## Other changes 284 | 285 | * [Translate](https://ryandrewjohnson.github.io/react-localize-redux-docs/#render-props-api) render props API now takes a single options object as an argument instead of multiple arguments. 286 | 287 | * If using Redux, all state related to localize will be added under the `localize` key instead of `locale`. 288 | -------------------------------------------------------------------------------- /tests/Translate.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { Map } from 'immutable'; 5 | import { 6 | localizeReducer, 7 | getTranslate, 8 | getLanguages, 9 | getActiveLanguage 10 | } from '../src'; 11 | import { defaultTranslateOptions } from '../src/localize'; 12 | import { renderToStaticMarkup } from 'react-dom/server'; 13 | 14 | Enzyme.configure({ adapter: new Adapter() }); 15 | 16 | beforeEach(() => { 17 | jest.resetModules(); 18 | }); 19 | 20 | describe('', () => { 21 | const initialState = { 22 | languages: [{ code: 'en', active: true }, { code: 'fr', active: false }], 23 | translations: { 24 | hello: ['Hello', 'Hello FR'], 25 | bye: ['Goodbye', 'Goodbye FR'], 26 | missing: ['Missing'], 27 | html: ['Hey google', ''], 28 | htmlPlaceholder: ['Translation with html and placeholder: ${ comp }.'], 29 | multiline: [null, ''], 30 | placeholder: ['Hey ${name}!', ''] 31 | }, 32 | options: defaultTranslateOptions 33 | }; 34 | 35 | let defaultContext = {}; 36 | 37 | const getTranslateWithContext = (state = initialState) => { 38 | const localizeState = localizeReducer(state, {}); 39 | 40 | defaultContext = { 41 | translate: getTranslate(localizeState), 42 | languages: getLanguages(localizeState), 43 | defaultLanguage: 44 | state.options.defaultLanguage || (getLanguages(localizeState)[0] && getLanguages(localizeState)[0].code), 45 | activeLanguage: getActiveLanguage(localizeState), 46 | initialize: jest.fn(), 47 | addTranslation: jest.fn(), 48 | addTranslationForLanguage: jest.fn(), 49 | setActiveLanguage: jest.fn(), 50 | renderToStaticMarkup, 51 | ignoreTranslateChildren: localizeState.options.ignoreTranslateChildren 52 | }; 53 | 54 | jest.doMock('../src/LocalizeContext', () => { 55 | return { 56 | LocalizeContext: { 57 | Consumer: props => props.children(defaultContext) 58 | } 59 | }; 60 | }); 61 | 62 | return require('Translate').Translate; 63 | }; 64 | 65 | it('should render HTML in translations when renderInnerHtml = true', () => { 66 | const Translate = getTranslateWithContext(); 67 | const wrapper = mount( 68 | 69 | Hey google 70 | 71 | ); 72 | 73 | expect(wrapper.html()).toEqual( 74 | 'Hey google' 75 | ); 76 | }); 77 | 78 | it('should render HTML text in translations when renderInnerHtml = false', () => { 79 | const Translate = getTranslateWithContext(); 80 | const wrapper = mount( 81 | 82 | Hey google 83 | 84 | ); 85 | 86 | expect(() => wrapper.html()).toThrowError(); 87 | expect(wrapper.text()).toEqual( 88 | 'Hey google' 89 | ); 90 | }); 91 | 92 | it("should convert 's children to a string when multi-line HTML markup is provided, and renderToStaticMarkup was set", () => { 93 | const Translate = getTranslateWithContext(); 94 | const wrapper = mount( 95 | 96 |

Heading

97 |
    98 |
  • Item #1
  • 99 |
100 |
101 | ); 102 | 103 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 104 | { multiline: '

Heading

  • Item #1
' }, 105 | 'en' 106 | ); 107 | }); 108 | 109 | it('should render React', () => { 110 | const Comp = ({name}) => {name}; 111 | const Translate = getTranslateWithContext(); 112 | const wrapper = mount( 113 | }} /> 114 | ); 115 | 116 | expect(wrapper.find(Comp).length).toBe(1); 117 | expect(wrapper.text()).toContain('ReactJS'); 118 | }) 119 | 120 | it('should render empty string if passing React placeholder data to translation with html', () => { 121 | const Comp = ({name}) => {name}; 122 | const Translate = getTranslateWithContext(); 123 | const wrapper = mount( 124 | }} /> 125 | ); 126 | 127 | expect(wrapper.text()).toBe(''); 128 | }) 129 | 130 | it('should just pass through string when renderToStaticMarkup not set', () => { 131 | const Translate = getTranslateWithContext({ 132 | ...initialState, 133 | options: { 134 | ...initialState.options, 135 | renderToStaticMarkup: false 136 | } 137 | }); 138 | 139 | const wrapper = mount(Hello); 140 | 141 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 142 | { test: 'Hello' }, 143 | 'en' 144 | ); 145 | }); 146 | 147 | it("should add 's children to translations under languages[0].code for id", () => { 148 | const Translate = getTranslateWithContext(); 149 | const wrapper = mount(Hey); 150 | 151 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 152 | { hello: 'Hey' }, 153 | 'en' 154 | ); 155 | }); 156 | 157 | it("should add 's children to translations when id changes", () => { 158 | const Translate = getTranslateWithContext(); 159 | const Parent = ({ condition }) => 160 | condition ? ( 161 | Hello 162 | ) : ( 163 | World 164 | ); 165 | 166 | const wrapper = mount(); 167 | 168 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 169 | { hello: 'Hello' }, 170 | 'en' 171 | ); 172 | 173 | wrapper.setProps({ condition: false }); 174 | 175 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 176 | { world: 'World' }, 177 | 'en' 178 | ); 179 | }); 180 | 181 | it("should add 's children to translations when default language is set", () => { 182 | const Translate = getTranslateWithContext({...initialState, languages: [] }); 183 | const wrapper = mount( 184 | 185 | Default Translation 186 | 187 | ); 188 | 189 | wrapper.setProps({options: {language: 'en'}}); 190 | 191 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 192 | { 'no_translation': 'Default Translation' }, 193 | 'en' 194 | ); 195 | }); 196 | 197 | it("should add 's children to translations under options.defaultLanguage for id", () => { 198 | const Translate = getTranslateWithContext(); 199 | const wrapper = mount( 200 | 201 | Hey 202 | 203 | ); 204 | 205 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 206 | { hello: 'Hey' }, 207 | 'fr' 208 | ); 209 | }); 210 | 211 | it('should not override default language translation if no children provided', () => { 212 | const Translate = getTranslateWithContext(); 213 | const wrapper = mount(); 214 | expect(defaultContext.addTranslationForLanguage).not.toHaveBeenCalled(); 215 | }); 216 | 217 | it('should not add default language translation if ignoreTranslateChildren = true', () => { 218 | const Translate = getTranslateWithContext(); 219 | const wrapper = mount( 220 | 221 | Hey 222 | 223 | ); 224 | expect(defaultContext.addTranslationForLanguage).not.toHaveBeenCalled(); 225 | }); 226 | 227 | it('should not add default language translation if ignoreTranslateChildren = true in context', () => { 228 | const Translate = getTranslateWithContext({ 229 | ...initialState, 230 | options: { 231 | ...initialState.options, 232 | ignoreTranslateChildren: true 233 | } 234 | }); 235 | const wrapper = mount(Hey); 236 | expect(defaultContext.addTranslationForLanguage).not.toHaveBeenCalled(); 237 | }); 238 | 239 | it('should override context ignoreTranslateChildren from props', () => { 240 | const Translate = getTranslateWithContext({ 241 | ...initialState, 242 | options: { 243 | ...initialState.options, 244 | ignoreTranslateChildren: true 245 | } 246 | }); 247 | const wrapper = mount( 248 | 249 | Override 250 | 251 | ); 252 | expect(defaultContext.addTranslationForLanguage).toHaveBeenLastCalledWith( 253 | { hello: 'Override' }, 254 | 'en' 255 | ); 256 | }); 257 | 258 | it('should insert data into translation placeholders when data attribute is provided', () => { 259 | const Translate = getTranslateWithContext(); 260 | const wrapper = mount( 261 | 262 | {'Hey ${name}!'} 263 | 264 | ); 265 | 266 | expect(wrapper.text()).toEqual('Hey Ted!'); 267 | }); 268 | 269 | it('should override avtiveLanguage when language prop provided for ', () => { 270 | const Translate = getTranslateWithContext(); 271 | const wrapper = mount( 272 | 273 | Hey 274 | 275 | ); 276 | expect(wrapper.text()).toEqual('Hello FR'); 277 | }); 278 | 279 | it('should use default onMissingTranslation option for ', () => { 280 | const Translate = getTranslateWithContext(); 281 | const wrapper = mount(Hey); 282 | expect(wrapper.text()).toEqual( 283 | 'Missing translationId: nope for language: en' 284 | ); 285 | }); 286 | 287 | it('should override onMissingTranslation option for ', () => { 288 | const Translate = getTranslateWithContext(); 289 | const onMissingTranslation = ({ translationId, languageCode }) => 290 | '${translationId} - ${languageCode}'; 291 | const wrapper = mount( 292 | 293 | Hey 294 | 295 | ); 296 | expect(wrapper.text()).toEqual('nope - en'); 297 | }); 298 | 299 | it('should override onMissingTranslation and provide defaultTranslation for ', () => { 300 | const Translate = getTranslateWithContext({ 301 | ...initialState, 302 | languages: [{ code: 'en', active: false }, { code: 'fr', active: true }], 303 | options: { 304 | ...initialState.options, 305 | defaultLanguage: 'en' 306 | } 307 | }); 308 | 309 | const onMissingTranslation = ({ defaultTranslation }) => defaultTranslation; 310 | const wrapper = mount( 311 | 312 | Hey 313 | 314 | ); 315 | expect(wrapper.text()).toEqual('Missing'); 316 | }); 317 | 318 | it('should accept function as child, and pass context as argument', () => { 319 | const Translate = getTranslateWithContext(); 320 | const wrapper = mount( 321 | 322 | {context => ( 323 |

324 | {context.translate('hello')} 325 | {context.activeLanguage.code} 326 | {context.languages.map(lang => lang.code).toString()} 327 |

328 | )} 329 |
330 | ); 331 | 332 | expect(wrapper.text()).toEqual('Helloenen,fr'); 333 | }); 334 | }); 335 | -------------------------------------------------------------------------------- /src/localize.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { flatten } from 'flat'; 4 | import { 5 | createSelector, 6 | createSelectorCreator, 7 | defaultMemoize 8 | } from 'reselect'; 9 | import { 10 | getLocalizedElement, 11 | getIndexForLanguageCode, 12 | objectValuesToString, 13 | validateOptions, 14 | getTranslationsForLanguage, 15 | warning, 16 | getSingleToMultilanguageTranslation 17 | } from './utils'; 18 | import type { Selector, SelectorCreator } from 'reselect'; 19 | import type { Element } from 'react'; 20 | 21 | /** 22 | * TYPES 23 | */ 24 | export type Language = { 25 | name?: string, 26 | code: string, 27 | active: boolean 28 | }; 29 | 30 | export type NamedLanguage = { 31 | name: string, 32 | code: string 33 | }; 34 | 35 | export type Translations = { 36 | [key: string]: string[] 37 | }; 38 | 39 | export type TransFormFunction = ( 40 | data: Object, 41 | languageCodes: string[] 42 | ) => Translations; 43 | 44 | export type renderToStaticMarkupFunction = (element: any) => string; 45 | 46 | export type InitializeOptions = { 47 | renderToStaticMarkup: renderToStaticMarkupFunction | false, 48 | renderInnerHtml?: boolean, 49 | onMissingTranslation?: onMissingTranslationFunction, 50 | defaultLanguage?: string, 51 | ignoreTranslateChildren?: boolean 52 | }; 53 | 54 | // This is to get around the whole default options issue with Flow 55 | // I tried using the $Diff approach, but with no luck so for now stuck with this terd. 56 | // Because sometimes you just want flow to shut up! 57 | type InitializeOptionsRequired = { 58 | renderToStaticMarkup: renderToStaticMarkupFunction | false, 59 | renderInnerHtml: boolean, 60 | onMissingTranslation: onMissingTranslationFunction, 61 | defaultLanguage: string, 62 | ignoreTranslateChildren: boolean 63 | }; 64 | 65 | export type TranslateOptions = { 66 | language?: string, 67 | renderInnerHtml?: boolean, 68 | onMissingTranslation?: onMissingTranslationFunction, 69 | defaultLanguage?: string, 70 | ignoreTranslateChildren?: boolean 71 | }; 72 | 73 | export type AddTranslationOptions = { 74 | translationTransform?: TransFormFunction 75 | }; 76 | 77 | export type LocalizeState = { 78 | +languages: Language[], 79 | +translations: Translations, 80 | +options: InitializeOptionsRequired 81 | }; 82 | 83 | export type TranslatedLanguage = { 84 | [string]: string 85 | }; 86 | 87 | export type LocalizedElement = Element<'span'> | string; 88 | 89 | export type LocalizedElementMap = { 90 | [string]: LocalizedElement 91 | }; 92 | 93 | export type TranslatePlaceholderData = { 94 | [string]: string | number | React.Node 95 | }; 96 | 97 | export type TranslateValue = string | string[]; 98 | 99 | export type TranslateFunction = ( 100 | value: TranslateValue, 101 | data?: TranslatePlaceholderData, 102 | options?: TranslateOptions 103 | ) => LocalizedElement | LocalizedElementMap; 104 | 105 | export type SingleLanguageTranslation = { 106 | [key: string]: Object | string 107 | }; 108 | 109 | export type MultipleLanguageTranslation = { 110 | [key: string]: Object | string[] 111 | }; 112 | 113 | type MissingTranslationOptions = { 114 | translationId: string, 115 | languageCode: string, 116 | defaultTranslation: LocalizedElement 117 | }; 118 | 119 | export type onMissingTranslationFunction = ( 120 | options: MissingTranslationOptions 121 | ) => string; 122 | 123 | export type InitializePayload = { 124 | languages: Array, 125 | translation?: Object, 126 | options?: InitializeOptions 127 | }; 128 | 129 | type AddTranslationPayload = { 130 | translation: Object, 131 | translationOptions?: AddTranslationOptions 132 | }; 133 | 134 | type AddTranslationForLanguagePayload = { 135 | translation: Object, 136 | language: string 137 | }; 138 | 139 | type SetActiveLanguagePayload = { 140 | languageCode: string 141 | }; 142 | 143 | type BaseAction = { 144 | type: T, 145 | payload: P 146 | }; 147 | 148 | export type InitializeAction = BaseAction< 149 | '@@localize/INITIALIZE', 150 | InitializePayload 151 | >; 152 | export type AddTranslationAction = BaseAction< 153 | '@@localize/ADD_TRANSLATION', 154 | AddTranslationPayload 155 | >; 156 | export type AddTranslationForLanguageAction = BaseAction< 157 | '@@localize/ADD_TRANSLATION_FOR_LANGUAGE', 158 | AddTranslationForLanguagePayload 159 | >; 160 | export type SetActiveLanguageAction = BaseAction< 161 | '@@localize/SET_ACTIVE_LANGUAGE', 162 | SetActiveLanguagePayload 163 | >; 164 | 165 | export type Action = BaseAction< 166 | string, 167 | InitializePayload & 168 | AddTranslationPayload & 169 | AddTranslationForLanguagePayload & 170 | SetActiveLanguagePayload 171 | >; 172 | 173 | export type ActionDetailed = Action & { 174 | languageCodes: string[] 175 | }; 176 | 177 | /** 178 | * ACTIONS 179 | */ 180 | export const INITIALIZE = '@@localize/INITIALIZE'; 181 | export const ADD_TRANSLATION = '@@localize/ADD_TRANSLATION'; 182 | export const ADD_TRANSLATION_FOR_LANGUAGE = 183 | '@@localize/ADD_TRANSLATION_FOR_LANGUAGE'; 184 | export const SET_ACTIVE_LANGUAGE = '@@localize/SET_ACTIVE_LANGUAGE'; 185 | export const TRANSLATE = '@@localize/TRANSLATE'; 186 | 187 | /** 188 | * REDUCERS 189 | */ 190 | export function languages(state: Language[] = [], action: Action): Language[] { 191 | switch (action.type) { 192 | case INITIALIZE: 193 | const options = action.payload.options || {}; 194 | return action.payload.languages.map((language, index) => { 195 | const isActive = code => { 196 | return options.defaultLanguage !== undefined 197 | ? code === options.defaultLanguage 198 | : index === 0; 199 | }; 200 | // check if it's using array of Language objects, or array of language codes 201 | return typeof language === 'string' 202 | ? { code: language, active: isActive(language) } // language codes 203 | : { ...language, active: isActive(language.code) }; // language objects 204 | }); 205 | case SET_ACTIVE_LANGUAGE: 206 | return state.map(language => { 207 | return language.code === action.payload.languageCode 208 | ? { ...language, active: true } 209 | : { ...language, active: false }; 210 | }); 211 | default: 212 | return state; 213 | } 214 | } 215 | 216 | export function translations( 217 | state: Translations = {}, 218 | action: ActionDetailed 219 | ): Translations { 220 | let flattenedTranslations; 221 | let translationWithTransform; 222 | 223 | switch (action.type) { 224 | case INITIALIZE: 225 | if (!action.payload.translation) { 226 | return state; 227 | } 228 | 229 | flattenedTranslations = flatten(action.payload.translation, { 230 | safe: true 231 | }); 232 | const options = action.payload.options || {}; 233 | const firstLanguage = 234 | typeof action.payload.languages[0] === 'string' 235 | ? action.payload.languages[0] 236 | : action.payload.languages[0].code; 237 | const defaultLanguage = options.defaultLanguage || firstLanguage; 238 | const isMultiLanguageTranslation = Object.keys( 239 | flattenedTranslations 240 | ).some(item => Array.isArray(flattenedTranslations[item])); 241 | 242 | // add translation based on whether it is single vs multi language translation data 243 | const newTranslation = isMultiLanguageTranslation 244 | ? flattenedTranslations 245 | : getSingleToMultilanguageTranslation( 246 | defaultLanguage, 247 | action.languageCodes, 248 | flattenedTranslations, 249 | state 250 | ); 251 | 252 | return { 253 | ...state, 254 | ...newTranslation 255 | }; 256 | case ADD_TRANSLATION: 257 | translationWithTransform = 258 | action.payload.translationOptions && 259 | action.payload.translationOptions.translationTransform !== undefined 260 | ? action.payload.translationOptions.translationTransform( 261 | action.payload.translation || {}, 262 | action.languageCodes 263 | ) 264 | : action.payload.translation; 265 | return { 266 | ...state, 267 | ...flatten(translationWithTransform, { safe: true }) 268 | }; 269 | case ADD_TRANSLATION_FOR_LANGUAGE: 270 | flattenedTranslations = flatten(action.payload.translation, { 271 | safe: true 272 | }); 273 | return { 274 | ...state, 275 | ...getSingleToMultilanguageTranslation( 276 | action.payload.language, 277 | action.languageCodes, 278 | flattenedTranslations, 279 | state 280 | ) 281 | }; 282 | default: 283 | return state; 284 | } 285 | } 286 | 287 | export function options( 288 | state: InitializeOptionsRequired = defaultTranslateOptions, 289 | action: ActionDetailed 290 | ): InitializeOptionsRequired { 291 | switch (action.type) { 292 | case INITIALIZE: 293 | const options: any = action.payload.options || {}; 294 | const defaultLanguage = 295 | options.defaultLanguage || action.languageCodes[0]; 296 | return { ...state, ...validateOptions(options), defaultLanguage }; 297 | default: 298 | return state; 299 | } 300 | } 301 | 302 | export const defaultTranslateOptions: InitializeOptionsRequired = { 303 | renderToStaticMarkup: false, 304 | renderInnerHtml: false, 305 | ignoreTranslateChildren: false, 306 | defaultLanguage: '', 307 | onMissingTranslation: ({ translationId, languageCode }) => 308 | 'Missing translationId: ${ translationId } for language: ${ languageCode }' 309 | }; 310 | 311 | const initialState: LocalizeState = { 312 | languages: [], 313 | translations: {}, 314 | options: defaultTranslateOptions 315 | }; 316 | 317 | export const localizeReducer = ( 318 | state: LocalizeState = initialState, 319 | action: Action 320 | ): LocalizeState => { 321 | // execute the languages reducer first as we need access to those values for other reducers 322 | const languagesState = languages(state.languages, action); 323 | const languageCodes = languagesState.map(language => language.code); 324 | 325 | return { 326 | languages: languagesState, 327 | translations: translations(state.translations, { 328 | ...action, 329 | languageCodes 330 | }), 331 | options: options(state.options, { ...action, languageCodes }) 332 | }; 333 | }; 334 | 335 | /** 336 | * ACTION CREATORS 337 | */ 338 | export const initialize = (payload: InitializePayload): InitializeAction => ({ 339 | type: INITIALIZE, 340 | payload 341 | }); 342 | 343 | export const addTranslation = ( 344 | translation: MultipleLanguageTranslation, 345 | options?: AddTranslationOptions 346 | ): AddTranslationAction => ({ 347 | type: ADD_TRANSLATION, 348 | payload: { 349 | translation, 350 | translationOptions: options 351 | } 352 | }); 353 | 354 | export const addTranslationForLanguage = ( 355 | translation: SingleLanguageTranslation, 356 | language: string 357 | ): AddTranslationForLanguageAction => ({ 358 | type: ADD_TRANSLATION_FOR_LANGUAGE, 359 | payload: { translation, language } 360 | }); 361 | 362 | export const setActiveLanguage = ( 363 | languageCode: string 364 | ): SetActiveLanguageAction => ({ 365 | type: SET_ACTIVE_LANGUAGE, 366 | payload: { languageCode } 367 | }); 368 | 369 | /** 370 | * SELECTORS 371 | */ 372 | export const getTranslations = (state: LocalizeState): Translations => { 373 | return state.translations; 374 | }; 375 | 376 | export const getLanguages = (state: LocalizeState): Language[] => 377 | state.languages; 378 | 379 | export const getOptions = (state: LocalizeState): InitializeOptionsRequired => { 380 | return state.options; 381 | }; 382 | 383 | export const getActiveLanguage = (state: LocalizeState): Language => { 384 | const languages = getLanguages(state); 385 | return languages.filter(language => language.active === true)[0]; 386 | }; 387 | 388 | /** 389 | * A custom equality checker that checker that compares an objects keys and values instead of === comparison 390 | * e.g. {name: 'Ted', sport: 'hockey'} would result in 'name,sport - Ted,hocker' which would be used for comparison 391 | * 392 | * NOTE: This works with activeLanguage, languages, and translations data types. 393 | * If a new data type is added to selector this would need to be updated to accomodate 394 | */ 395 | export const translationsEqualSelector = createSelectorCreator( 396 | defaultMemoize, 397 | (prev, cur) => { 398 | const prevKeys: any = 399 | typeof prev === 'object' ? Object.keys(prev).toString() : undefined; 400 | const curKeys: any = 401 | typeof cur === 'object' ? Object.keys(cur).toString() : undefined; 402 | 403 | const prevValues: any = 404 | typeof prev === 'object' ? objectValuesToString(prev) : undefined; 405 | const curValues: any = 406 | typeof cur === 'object' ? objectValuesToString(cur) : undefined; 407 | 408 | const prevCacheValue = 409 | prevKeys !== undefined && prevValues !== undefined 410 | ? `${prevKeys} - ${prevValues}` 411 | : prev; 412 | 413 | const curCacheValue = 414 | curKeys !== undefined && curValues !== undefined 415 | ? `${curKeys} - ${curValues}` 416 | : cur; 417 | 418 | return prevCacheValue === curCacheValue; 419 | } 420 | ); 421 | 422 | export const getTranslationsForActiveLanguage: Selector< 423 | LocalizeState, 424 | void, 425 | TranslatedLanguage 426 | > = translationsEqualSelector( 427 | getActiveLanguage, 428 | getLanguages, 429 | getTranslations, 430 | getTranslationsForLanguage 431 | ); 432 | 433 | export const getTranslationsForSpecificLanguage = translationsEqualSelector( 434 | getLanguages, 435 | getTranslations, 436 | (languages, translations) => 437 | defaultMemoize((languageCode: string) => 438 | getTranslationsForLanguage( 439 | { code: languageCode, active: false }, 440 | languages, 441 | translations 442 | ) 443 | ) 444 | ); 445 | 446 | export const getTranslate: Selector< 447 | LocalizeState, 448 | void, 449 | TranslateFunction 450 | > = createSelector( 451 | getTranslationsForActiveLanguage, 452 | getTranslationsForSpecificLanguage, 453 | getActiveLanguage, 454 | getOptions, 455 | ( 456 | translationsForActiveLanguage, 457 | getTranslationsForLanguage, 458 | activeLanguage, 459 | initializeOptions 460 | ) => { 461 | return (value, data = {}, translateOptions = {}) => { 462 | const { defaultLanguage, ...defaultOptions } = initializeOptions; 463 | const overrideLanguage = translateOptions.language; 464 | 465 | const translations = 466 | overrideLanguage !== undefined 467 | ? getTranslationsForLanguage(overrideLanguage) 468 | : translationsForActiveLanguage; 469 | 470 | const defaultTranslations = 471 | activeLanguage && activeLanguage.code === defaultLanguage 472 | ? translationsForActiveLanguage 473 | : getTranslationsForLanguage(defaultLanguage); 474 | 475 | const languageCode = 476 | overrideLanguage !== undefined 477 | ? overrideLanguage 478 | : activeLanguage && activeLanguage.code; 479 | 480 | const mergedOptions = { ...defaultOptions, ...translateOptions }; 481 | 482 | const getTranslation = (translationId: string) => { 483 | const hasValidTranslation = translations[translationId] !== undefined; 484 | const hasValidDefaultTranslation = 485 | defaultTranslations[translationId] !== undefined; 486 | 487 | const defaultTranslation = hasValidDefaultTranslation 488 | ? getLocalizedElement({ 489 | translation: defaultTranslations[translationId], 490 | data, 491 | renderInnerHtml: mergedOptions.renderInnerHtml 492 | }) 493 | : "No default translation found! Ensure you've added translations for your default langauge."; 494 | 495 | // if translation is not valid then generate the on missing translation message in it's place 496 | const translation = hasValidTranslation 497 | ? translations[translationId] 498 | : mergedOptions.onMissingTranslation({ 499 | translationId, 500 | languageCode, 501 | defaultTranslation 502 | }); 503 | 504 | // if translations are missing than ovrride data to include translationId, languageCode 505 | // as these will be needed to render missing translations message 506 | const translationData = hasValidTranslation 507 | ? data 508 | : { translationId, languageCode }; 509 | 510 | return getLocalizedElement({ 511 | translation, 512 | data: translationData, 513 | languageCode, 514 | renderInnerHtml: mergedOptions.renderInnerHtml 515 | }); 516 | }; 517 | 518 | if (typeof value === 'string') { 519 | return getTranslation(value); 520 | } else if (Array.isArray(value)) { 521 | return value.reduce((prev, cur) => { 522 | return { 523 | ...prev, 524 | [cur]: getTranslation(cur) 525 | }; 526 | }, {}); 527 | } else { 528 | throw new Error( 529 | 'react-localize-redux: Invalid key passed to getTranslate.' 530 | ); 531 | } 532 | }; 533 | } 534 | ); 535 | -------------------------------------------------------------------------------- /flow-typed/npm/reselect_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 7242133add1d3bd16fc3e9d648152c63 2 | // flow-typed version: 00301f0d29/reselect_v3.x.x/flow_>=v0.47.x 3 | 4 | // flow-typed signature: 0199525b667f385f2e61dbeae3215f21 5 | // flow-typed version: b43dff3e0e/reselect_v3.x.x/flow_>=v0.28.x 6 | 7 | declare module "reselect" { 8 | declare type Selector<-TState, TProps, TResult> = 9 | (state: TState, props: TProps, ...rest: any[]) => TResult 10 | 11 | declare type SelectorCreator = { 12 | ( 13 | selector1: Selector, 14 | resultFunc: (arg1: T1) => TResult 15 | ): Selector, 16 | ( 17 | selectors: [Selector], 18 | resultFunc: (arg1: T1) => TResult 19 | ): Selector, 20 | 21 | ( 22 | selector1: Selector, 23 | selector2: Selector, 24 | resultFunc: (arg1: T1, arg2: T2) => TResult 25 | ): Selector, 26 | ( 27 | selectors: [Selector, Selector], 28 | resultFunc: (arg1: T1, arg2: T2) => TResult 29 | ): Selector, 30 | 31 | ( 32 | selector1: Selector, 33 | selector2: Selector, 34 | selector3: Selector, 35 | resultFunc: (arg1: T1, arg2: T2, arg3: T3) => TResult 36 | ): Selector, 37 | ( 38 | selectors: [ 39 | Selector, 40 | Selector, 41 | Selector 42 | ], 43 | resultFunc: (arg1: T1, arg2: T2, arg3: T3) => TResult 44 | ): Selector, 45 | 46 | ( 47 | selector1: Selector, 48 | selector2: Selector, 49 | selector3: Selector, 50 | selector4: Selector, 51 | resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => TResult 52 | ): Selector, 53 | ( 54 | selectors: [ 55 | Selector, 56 | Selector, 57 | Selector, 58 | Selector 59 | ], 60 | resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => TResult 61 | ): Selector, 62 | 63 | ( 64 | selector1: Selector, 65 | selector2: Selector, 66 | selector3: Selector, 67 | selector4: Selector, 68 | selector5: Selector, 69 | resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => TResult 70 | ): Selector, 71 | ( 72 | selectors: [ 73 | Selector, 74 | Selector, 75 | Selector, 76 | Selector, 77 | Selector 78 | ], 79 | resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => TResult 80 | ): Selector, 81 | 82 | ( 83 | selector1: Selector, 84 | selector2: Selector, 85 | selector3: Selector, 86 | selector4: Selector, 87 | selector5: Selector, 88 | selector6: Selector, 89 | resultFunc: ( 90 | arg1: T1, 91 | arg2: T2, 92 | arg3: T3, 93 | arg4: T4, 94 | arg5: T5, 95 | arg6: T6 96 | ) => TResult 97 | ): Selector, 98 | ( 99 | selectors: [ 100 | Selector, 101 | Selector, 102 | Selector, 103 | Selector, 104 | Selector, 105 | Selector 106 | ], 107 | resultFunc: ( 108 | arg1: T1, 109 | arg2: T2, 110 | arg3: T3, 111 | arg4: T4, 112 | arg5: T5, 113 | arg6: T6 114 | ) => TResult 115 | ): Selector, 116 | 117 | ( 118 | selector1: Selector, 119 | selector2: Selector, 120 | selector3: Selector, 121 | selector4: Selector, 122 | selector5: Selector, 123 | selector6: Selector, 124 | selector7: Selector, 125 | resultFunc: ( 126 | arg1: T1, 127 | arg2: T2, 128 | arg3: T3, 129 | arg4: T4, 130 | arg5: T5, 131 | arg6: T6, 132 | arg7: T7 133 | ) => TResult 134 | ): Selector, 135 | ( 136 | selectors: [ 137 | Selector, 138 | Selector, 139 | Selector, 140 | Selector, 141 | Selector, 142 | Selector, 143 | Selector 144 | ], 145 | resultFunc: ( 146 | arg1: T1, 147 | arg2: T2, 148 | arg3: T3, 149 | arg4: T4, 150 | arg5: T5, 151 | arg6: T6, 152 | arg7: T7 153 | ) => TResult 154 | ): Selector, 155 | 156 | ( 157 | selector1: Selector, 158 | selector2: Selector, 159 | selector3: Selector, 160 | selector4: Selector, 161 | selector5: Selector, 162 | selector6: Selector, 163 | selector7: Selector, 164 | selector8: Selector, 165 | resultFunc: ( 166 | arg1: T1, 167 | arg2: T2, 168 | arg3: T3, 169 | arg4: T4, 170 | arg5: T5, 171 | arg6: T6, 172 | arg7: T7, 173 | arg8: T8 174 | ) => TResult 175 | ): Selector, 176 | ( 177 | selectors: [ 178 | Selector, 179 | Selector, 180 | Selector, 181 | Selector, 182 | Selector, 183 | Selector, 184 | Selector, 185 | Selector 186 | ], 187 | resultFunc: ( 188 | arg1: T1, 189 | arg2: T2, 190 | arg3: T3, 191 | arg4: T4, 192 | arg5: T5, 193 | arg6: T6, 194 | arg7: T7, 195 | arg8: T8 196 | ) => TResult 197 | ): Selector, 198 | 199 | ( 200 | selector1: Selector, 201 | selector2: Selector, 202 | selector3: Selector, 203 | selector4: Selector, 204 | selector5: Selector, 205 | selector6: Selector, 206 | selector7: Selector, 207 | selector8: Selector, 208 | selector9: Selector, 209 | resultFunc: ( 210 | arg1: T1, 211 | arg2: T2, 212 | arg3: T3, 213 | arg4: T4, 214 | arg5: T5, 215 | arg6: T6, 216 | arg7: T7, 217 | arg8: T8, 218 | arg9: T9 219 | ) => TResult 220 | ): Selector, 221 | ( 222 | selectors: [ 223 | Selector, 224 | Selector, 225 | Selector, 226 | Selector, 227 | Selector, 228 | Selector, 229 | Selector, 230 | Selector, 231 | Selector 232 | ], 233 | resultFunc: ( 234 | arg1: T1, 235 | arg2: T2, 236 | arg3: T3, 237 | arg4: T4, 238 | arg5: T5, 239 | arg6: T6, 240 | arg7: T7, 241 | arg8: T8, 242 | arg9: T9 243 | ) => TResult 244 | ): Selector, 245 | 246 | ( 247 | selector1: Selector, 248 | selector2: Selector, 249 | selector3: Selector, 250 | selector4: Selector, 251 | selector5: Selector, 252 | selector6: Selector, 253 | selector7: Selector, 254 | selector8: Selector, 255 | selector9: Selector, 256 | selector10: Selector, 257 | resultFunc: ( 258 | arg1: T1, 259 | arg2: T2, 260 | arg3: T3, 261 | arg4: T4, 262 | arg5: T5, 263 | arg6: T6, 264 | arg7: T7, 265 | arg8: T8, 266 | arg9: T9, 267 | arg10: T10 268 | ) => TResult 269 | ): Selector, 270 | ( 271 | selectors: [ 272 | Selector, 273 | Selector, 274 | Selector, 275 | Selector, 276 | Selector, 277 | Selector, 278 | Selector, 279 | Selector, 280 | Selector, 281 | Selector 282 | ], 283 | resultFunc: ( 284 | arg1: T1, 285 | arg2: T2, 286 | arg3: T3, 287 | arg4: T4, 288 | arg5: T5, 289 | arg6: T6, 290 | arg7: T7, 291 | arg8: T8, 292 | arg9: T9, 293 | arg10: T10 294 | ) => TResult 295 | ): Selector, 296 | 297 | ( 298 | selector1: Selector, 299 | selector2: Selector, 300 | selector3: Selector, 301 | selector4: Selector, 302 | selector5: Selector, 303 | selector6: Selector, 304 | selector7: Selector, 305 | selector8: Selector, 306 | selector9: Selector, 307 | selector10: Selector, 308 | selector11: Selector, 309 | resultFunc: ( 310 | arg1: T1, 311 | arg2: T2, 312 | arg3: T3, 313 | arg4: T4, 314 | arg5: T5, 315 | arg6: T6, 316 | arg7: T7, 317 | arg8: T8, 318 | arg9: T9, 319 | arg10: T10, 320 | arg11: T11 321 | ) => TResult 322 | ): Selector, 323 | ( 324 | selectors: [ 325 | Selector, 326 | Selector, 327 | Selector, 328 | Selector, 329 | Selector, 330 | Selector, 331 | Selector, 332 | Selector, 333 | Selector, 334 | Selector, 335 | Selector 336 | ], 337 | resultFunc: ( 338 | arg1: T1, 339 | arg2: T2, 340 | arg3: T3, 341 | arg4: T4, 342 | arg5: T5, 343 | arg6: T6, 344 | arg7: T7, 345 | arg8: T8, 346 | arg9: T9, 347 | arg10: T10, 348 | arg11: T11 349 | ) => TResult 350 | ): Selector, 351 | 352 | < 353 | TState, 354 | TProps, 355 | TResult, 356 | T1, 357 | T2, 358 | T3, 359 | T4, 360 | T5, 361 | T6, 362 | T7, 363 | T8, 364 | T9, 365 | T10, 366 | T11, 367 | T12 368 | >( 369 | selector1: Selector, 370 | selector2: Selector, 371 | selector3: Selector, 372 | selector4: Selector, 373 | selector5: Selector, 374 | selector6: Selector, 375 | selector7: Selector, 376 | selector8: Selector, 377 | selector9: Selector, 378 | selector10: Selector, 379 | selector11: Selector, 380 | selector12: Selector, 381 | resultFunc: ( 382 | arg1: T1, 383 | arg2: T2, 384 | arg3: T3, 385 | arg4: T4, 386 | arg5: T5, 387 | arg6: T6, 388 | arg7: T7, 389 | arg8: T8, 390 | arg9: T9, 391 | arg10: T10, 392 | arg11: T11, 393 | arg12: T12 394 | ) => TResult 395 | ): Selector, 396 | < 397 | TState, 398 | TProps, 399 | TResult, 400 | T1, 401 | T2, 402 | T3, 403 | T4, 404 | T5, 405 | T6, 406 | T7, 407 | T8, 408 | T9, 409 | T10, 410 | T11, 411 | T12 412 | >( 413 | selectors: [ 414 | Selector, 415 | Selector, 416 | Selector, 417 | Selector, 418 | Selector, 419 | Selector, 420 | Selector, 421 | Selector, 422 | Selector, 423 | Selector, 424 | Selector, 425 | Selector 426 | ], 427 | resultFunc: ( 428 | arg1: T1, 429 | arg2: T2, 430 | arg3: T3, 431 | arg4: T4, 432 | arg5: T5, 433 | arg6: T6, 434 | arg7: T7, 435 | arg8: T8, 436 | arg9: T9, 437 | arg10: T10, 438 | arg11: T11, 439 | arg12: T12 440 | ) => TResult 441 | ): Selector, 442 | 443 | < 444 | TState, 445 | TProps, 446 | TResult, 447 | T1, 448 | T2, 449 | T3, 450 | T4, 451 | T5, 452 | T6, 453 | T7, 454 | T8, 455 | T9, 456 | T10, 457 | T11, 458 | T12, 459 | T13 460 | >( 461 | selector1: Selector, 462 | selector2: Selector, 463 | selector3: Selector, 464 | selector4: Selector, 465 | selector5: Selector, 466 | selector6: Selector, 467 | selector7: Selector, 468 | selector8: Selector, 469 | selector9: Selector, 470 | selector10: Selector, 471 | selector11: Selector, 472 | selector12: Selector, 473 | selector13: Selector, 474 | resultFunc: ( 475 | arg1: T1, 476 | arg2: T2, 477 | arg3: T3, 478 | arg4: T4, 479 | arg5: T5, 480 | arg6: T6, 481 | arg7: T7, 482 | arg8: T8, 483 | arg9: T9, 484 | arg10: T10, 485 | arg11: T11, 486 | arg12: T12, 487 | arg13: T13 488 | ) => TResult 489 | ): Selector, 490 | < 491 | TState, 492 | TProps, 493 | TResult, 494 | T1, 495 | T2, 496 | T3, 497 | T4, 498 | T5, 499 | T6, 500 | T7, 501 | T8, 502 | T9, 503 | T10, 504 | T11, 505 | T12, 506 | T13 507 | >( 508 | selectors: [ 509 | Selector, 510 | Selector, 511 | Selector, 512 | Selector, 513 | Selector, 514 | Selector, 515 | Selector, 516 | Selector, 517 | Selector, 518 | Selector, 519 | Selector, 520 | Selector, 521 | Selector 522 | ], 523 | resultFunc: ( 524 | arg1: T1, 525 | arg2: T2, 526 | arg3: T3, 527 | arg4: T4, 528 | arg5: T5, 529 | arg6: T6, 530 | arg7: T7, 531 | arg8: T8, 532 | arg9: T9, 533 | arg10: T10, 534 | arg11: T11, 535 | arg12: T12, 536 | arg13: T13 537 | ) => TResult 538 | ): Selector, 539 | 540 | < 541 | TState, 542 | TProps, 543 | TResult, 544 | T1, 545 | T2, 546 | T3, 547 | T4, 548 | T5, 549 | T6, 550 | T7, 551 | T8, 552 | T9, 553 | T10, 554 | T11, 555 | T12, 556 | T13, 557 | T14 558 | >( 559 | selector1: Selector, 560 | selector2: Selector, 561 | selector3: Selector, 562 | selector4: Selector, 563 | selector5: Selector, 564 | selector6: Selector, 565 | selector7: Selector, 566 | selector8: Selector, 567 | selector9: Selector, 568 | selector10: Selector, 569 | selector11: Selector, 570 | selector12: Selector, 571 | selector13: Selector, 572 | selector14: Selector, 573 | resultFunc: ( 574 | arg1: T1, 575 | arg2: T2, 576 | arg3: T3, 577 | arg4: T4, 578 | arg5: T5, 579 | arg6: T6, 580 | arg7: T7, 581 | arg8: T8, 582 | arg9: T9, 583 | arg10: T10, 584 | arg11: T11, 585 | arg12: T12, 586 | arg13: T13, 587 | arg14: T14 588 | ) => TResult 589 | ): Selector, 590 | < 591 | TState, 592 | TProps, 593 | TResult, 594 | T1, 595 | T2, 596 | T3, 597 | T4, 598 | T5, 599 | T6, 600 | T7, 601 | T8, 602 | T9, 603 | T10, 604 | T11, 605 | T12, 606 | T13, 607 | T14 608 | >( 609 | selectors: [ 610 | Selector, 611 | Selector, 612 | Selector, 613 | Selector, 614 | Selector, 615 | Selector, 616 | Selector, 617 | Selector, 618 | Selector, 619 | Selector, 620 | Selector, 621 | Selector, 622 | Selector, 623 | Selector 624 | ], 625 | resultFunc: ( 626 | arg1: T1, 627 | arg2: T2, 628 | arg3: T3, 629 | arg4: T4, 630 | arg5: T5, 631 | arg6: T6, 632 | arg7: T7, 633 | arg8: T8, 634 | arg9: T9, 635 | arg10: T10, 636 | arg11: T11, 637 | arg12: T12, 638 | arg13: T13, 639 | arg14: T14 640 | ) => TResult 641 | ): Selector, 642 | 643 | < 644 | TState, 645 | TProps, 646 | TResult, 647 | T1, 648 | T2, 649 | T3, 650 | T4, 651 | T5, 652 | T6, 653 | T7, 654 | T8, 655 | T9, 656 | T10, 657 | T11, 658 | T12, 659 | T13, 660 | T14, 661 | T15 662 | >( 663 | selector1: Selector, 664 | selector2: Selector, 665 | selector3: Selector, 666 | selector4: Selector, 667 | selector5: Selector, 668 | selector6: Selector, 669 | selector7: Selector, 670 | selector8: Selector, 671 | selector9: Selector, 672 | selector10: Selector, 673 | selector11: Selector, 674 | selector12: Selector, 675 | selector13: Selector, 676 | selector14: Selector, 677 | selector15: Selector, 678 | resultFunc: ( 679 | arg1: T1, 680 | arg2: T2, 681 | arg3: T3, 682 | arg4: T4, 683 | arg5: T5, 684 | arg6: T6, 685 | arg7: T7, 686 | arg8: T8, 687 | arg9: T9, 688 | arg10: T10, 689 | arg11: T11, 690 | arg12: T12, 691 | arg13: T13, 692 | arg14: T14, 693 | arg15: T15 694 | ) => TResult 695 | ): Selector, 696 | < 697 | TState, 698 | TProps, 699 | TResult, 700 | T1, 701 | T2, 702 | T3, 703 | T4, 704 | T5, 705 | T6, 706 | T7, 707 | T8, 708 | T9, 709 | T10, 710 | T11, 711 | T12, 712 | T13, 713 | T14, 714 | T15 715 | >( 716 | selectors: [ 717 | Selector, 718 | Selector, 719 | Selector, 720 | Selector, 721 | Selector, 722 | Selector, 723 | Selector, 724 | Selector, 725 | Selector, 726 | Selector, 727 | Selector, 728 | Selector, 729 | Selector, 730 | Selector, 731 | Selector 732 | ], 733 | resultFunc: ( 734 | arg1: T1, 735 | arg2: T2, 736 | arg3: T3, 737 | arg4: T4, 738 | arg5: T5, 739 | arg6: T6, 740 | arg7: T7, 741 | arg8: T8, 742 | arg9: T9, 743 | arg10: T10, 744 | arg11: T11, 745 | arg12: T12, 746 | arg13: T13, 747 | arg14: T14, 748 | arg15: T15 749 | ) => TResult 750 | ): Selector, 751 | 752 | < 753 | TState, 754 | TProps, 755 | TResult, 756 | T1, 757 | T2, 758 | T3, 759 | T4, 760 | T5, 761 | T6, 762 | T7, 763 | T8, 764 | T9, 765 | T10, 766 | T11, 767 | T12, 768 | T13, 769 | T14, 770 | T15, 771 | T16 772 | >( 773 | selector1: Selector, 774 | selector2: Selector, 775 | selector3: Selector, 776 | selector4: Selector, 777 | selector5: Selector, 778 | selector6: Selector, 779 | selector7: Selector, 780 | selector8: Selector, 781 | selector9: Selector, 782 | selector10: Selector, 783 | selector11: Selector, 784 | selector12: Selector, 785 | selector13: Selector, 786 | selector14: Selector, 787 | selector15: Selector, 788 | selector16: Selector, 789 | resultFunc: ( 790 | arg1: T1, 791 | arg2: T2, 792 | arg3: T3, 793 | arg4: T4, 794 | arg5: T5, 795 | arg6: T6, 796 | arg7: T7, 797 | arg8: T8, 798 | arg9: T9, 799 | arg10: T10, 800 | arg11: T11, 801 | arg12: T12, 802 | arg13: T13, 803 | arg14: T14, 804 | arg15: T15, 805 | arg16: T16 806 | ) => TResult 807 | ): Selector, 808 | < 809 | TState, 810 | TProps, 811 | TResult, 812 | T1, 813 | T2, 814 | T3, 815 | T4, 816 | T5, 817 | T6, 818 | T7, 819 | T8, 820 | T9, 821 | T10, 822 | T11, 823 | T12, 824 | T13, 825 | T14, 826 | T15, 827 | T16 828 | >( 829 | selectors: [ 830 | Selector, 831 | Selector, 832 | Selector, 833 | Selector, 834 | Selector, 835 | Selector, 836 | Selector, 837 | Selector, 838 | Selector, 839 | Selector, 840 | Selector, 841 | Selector, 842 | Selector, 843 | Selector, 844 | Selector, 845 | Selector 846 | ], 847 | resultFunc: ( 848 | arg1: T1, 849 | arg2: T2, 850 | arg3: T3, 851 | arg4: T4, 852 | arg5: T5, 853 | arg6: T6, 854 | arg7: T7, 855 | arg8: T8, 856 | arg9: T9, 857 | arg10: T10, 858 | arg11: T11, 859 | arg12: T12, 860 | arg13: T13, 861 | arg14: T14, 862 | arg15: T15, 863 | arg16: T16 864 | ) => TResult 865 | ): Selector 866 | }; 867 | 868 | declare type Reselect = { 869 | createSelector: SelectorCreator, 870 | 871 | defaultMemoize: ( 872 | func: TFunc, 873 | equalityCheck?: (a: any, b: any) => boolean 874 | ) => TFunc, 875 | 876 | createSelectorCreator: ( 877 | memoize: Function, 878 | ...memoizeOptions: any[] 879 | ) => SelectorCreator, 880 | 881 | createStructuredSelector: ( 882 | inputSelectors: { 883 | [k: string | number]: Selector 884 | }, 885 | selectorCreator?: SelectorCreator 886 | ) => Selector 887 | }; 888 | 889 | declare module.exports: Reselect; 890 | } 891 | -------------------------------------------------------------------------------- /tests/localize.test.js: -------------------------------------------------------------------------------- 1 | import Enzyme, { shallow } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | import { 4 | languages, 5 | translations, 6 | getOptions, 7 | getActiveLanguage, 8 | getTranslationsForActiveLanguage, 9 | getTranslationsForSpecificLanguage, 10 | translationsEqualSelector, 11 | getTranslate, 12 | defaultTranslateOptions, 13 | options, 14 | localizeReducer 15 | } from 'localize'; 16 | import { 17 | INITIALIZE, 18 | SET_ACTIVE_LANGUAGE, 19 | ADD_TRANSLATION, 20 | ADD_TRANSLATION_FOR_LANGUAGE 21 | } from 'localize'; 22 | import { initialize } from '../src/localize'; 23 | 24 | Enzyme.configure({ adapter: new Adapter() }); 25 | 26 | describe('localize', () => { 27 | const transformFunction = (data, codes) => { 28 | ``; 29 | return Object.keys(data).reduce((prev, cur, index) => { 30 | const languageData = data[cur]; 31 | 32 | for (let prop in languageData) { 33 | const values = prev[prop] || []; 34 | prev[prop] = codes.map((code, languageIndex) => { 35 | return index === languageIndex 36 | ? languageData[prop] 37 | : values[languageIndex]; 38 | }); 39 | } 40 | 41 | return prev; 42 | }, {}); 43 | }; 44 | 45 | describe('reducer: languages', () => { 46 | let initialState = []; 47 | 48 | beforeEach(() => { 49 | initialState = [ 50 | { code: 'en', active: false }, 51 | { code: 'fr', active: false }, 52 | { code: 'ne', active: false } 53 | ]; 54 | }); 55 | 56 | describe('INITIALIZE', () => { 57 | describe('set with string[]', () => { 58 | it('should set languages and set default language to first language', () => { 59 | const action = { 60 | type: INITIALIZE, 61 | payload: { 62 | languages: ['en', 'fr', 'ne'] 63 | } 64 | }; 65 | const result = languages([], action); 66 | expect(result).toEqual([ 67 | { code: 'en', active: true }, 68 | { code: 'fr', active: false }, 69 | { code: 'ne', active: false } 70 | ]); 71 | }); 72 | 73 | it('should set active language based on defaultLanguage option', () => { 74 | const action = { 75 | type: INITIALIZE, 76 | payload: { 77 | languages: ['en', 'fr', 'ne'], 78 | options: { defaultLanguage: 'ne' } 79 | } 80 | }; 81 | const result = languages([], action); 82 | expect(result).toEqual([ 83 | { code: 'en', active: false }, 84 | { code: 'fr', active: false }, 85 | { code: 'ne', active: true } 86 | ]); 87 | }); 88 | }); 89 | }); 90 | 91 | describe('SET_ACTIVE_LANGUAGE', () => { 92 | it('should set active language', () => { 93 | const action = { 94 | type: SET_ACTIVE_LANGUAGE, 95 | payload: { 96 | languageCode: 'ne' 97 | } 98 | }; 99 | 100 | const result = languages(initialState, action); 101 | expect(result).toEqual([ 102 | { code: 'en', active: false }, 103 | { code: 'fr', active: false }, 104 | { code: 'ne', active: true } 105 | ]); 106 | }); 107 | 108 | it('should update active language', () => { 109 | const action = { 110 | type: SET_ACTIVE_LANGUAGE, 111 | payload: { 112 | languageCode: 'en' 113 | } 114 | }; 115 | 116 | initialState[1].active = true; 117 | const result = languages(initialState, action); 118 | expect(result).toEqual([ 119 | { code: 'en', active: true }, 120 | { code: 'fr', active: false }, 121 | { code: 'ne', active: false } 122 | ]); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('reducer: translations', () => { 128 | let initialState = {}; 129 | 130 | beforeEach(() => { 131 | initialState = { 132 | hi: ['hi'], 133 | bye: ['bye'] 134 | }; 135 | }); 136 | 137 | describe('INITIALIZE', () => { 138 | it('should not modify translations if !payload.translation', () => { 139 | const action = initialize({ 140 | languages: ['en', 'fr', 'ne'] 141 | }); 142 | 143 | const result = translations(initialState, action); 144 | expect(result).toEqual(initialState); 145 | }); 146 | 147 | it('should add multi-language translation data', () => { 148 | const action = initialize({ 149 | languages: ['en', 'fr', 'ne'], 150 | translation: initialState 151 | }); 152 | 153 | const result = translations({}, action); 154 | expect(result).toEqual(initialState); 155 | }); 156 | 157 | it('should add single language translation data to first language', () => { 158 | const action = { 159 | type: INITIALIZE, 160 | payload: { 161 | languages: ['en', 'fr', 'ne'], 162 | translation: { 163 | greeting: 'hello', 164 | bye: 'goodbye' 165 | } 166 | }, 167 | languageCodes: ['en', 'fr', 'ne'] 168 | }; 169 | 170 | const result = translations({}, action); 171 | expect(result).toEqual({ 172 | greeting: ['hello', undefined, undefined], 173 | bye: ['goodbye', undefined, undefined] 174 | }); 175 | }); 176 | 177 | it('should add single language translation data to default language', () => { 178 | const action = { 179 | type: INITIALIZE, 180 | payload: { 181 | languages: ['en', 'fr', 'ne'], 182 | translation: { 183 | greeting: 'hello', 184 | bye: 'goodbye' 185 | }, 186 | options: { defaultLanguage: 'fr' } 187 | }, 188 | languageCodes: ['en', 'fr', 'ne'] 189 | }; 190 | 191 | const result = translations({}, action); 192 | expect(result).toEqual({ 193 | greeting: [undefined, 'hello', undefined], 194 | bye: [undefined, 'goodbye', undefined] 195 | }); 196 | }); 197 | }); 198 | 199 | describe('ADD_TRANSLATION', () => { 200 | it('should add new translations', () => { 201 | const action = { 202 | type: ADD_TRANSLATION, 203 | payload: { 204 | translation: { 205 | test: ['test'], 206 | test2: ['test2'] 207 | } 208 | } 209 | }; 210 | 211 | const result = translations({}, action); 212 | expect(result).toEqual({ 213 | test: ['test'], 214 | test2: ['test2'] 215 | }); 216 | }); 217 | 218 | it('should merge new translations with existing translations', () => { 219 | const action = { 220 | type: ADD_TRANSLATION, 221 | payload: { 222 | translation: { new: ['new'] } 223 | } 224 | }; 225 | 226 | const result = translations(initialState, action); 227 | expect(result).toEqual({ 228 | ...initialState, 229 | new: ['new'] 230 | }); 231 | }); 232 | 233 | it('should overwrite existing translation key if it already exists', () => { 234 | const action = { 235 | type: ADD_TRANSLATION, 236 | payload: { 237 | translation: { hi: ['new'] } 238 | } 239 | }; 240 | 241 | const result = translations(initialState, action); 242 | expect(result).toEqual({ 243 | ...initialState, 244 | hi: ['new'] 245 | }); 246 | }); 247 | 248 | it('should flatten nested objects in translation', () => { 249 | const action = { 250 | type: ADD_TRANSLATION, 251 | payload: { 252 | translation: { 253 | first: { second: { third: ['nested'] } }, 254 | more: { nested: ['one'] } 255 | } 256 | } 257 | }; 258 | 259 | const result = translations({}, action); 260 | expect(result).toEqual({ 261 | 'first.second.third': ['nested'], 262 | 'more.nested': ['one'] 263 | }); 264 | }); 265 | 266 | it('should use translationTransform from options', () => { 267 | const translationData = { 268 | en: { 269 | title: 'Title', 270 | subtitle: 'Subtitle' 271 | }, 272 | fr: { 273 | title: 'FR - Title', 274 | subtitle: 'FR - Subtitle' 275 | } 276 | }; 277 | 278 | const action = { 279 | type: ADD_TRANSLATION, 280 | payload: { 281 | translation: translationData, 282 | translationOptions: { 283 | translationTransform: transformFunction 284 | } 285 | }, 286 | languageCodes: ['en', 'fr'] 287 | }; 288 | 289 | const result = translations({}, action); 290 | expect(result).toEqual({ 291 | title: ['Title', 'FR - Title'], 292 | subtitle: ['Subtitle', 'FR - Subtitle'] 293 | }); 294 | }); 295 | }); 296 | 297 | describe('ADD_TRANSLATION_FOR_LANGUAGE', () => { 298 | it('should add translation for specific language', () => { 299 | const action = { 300 | type: ADD_TRANSLATION_FOR_LANGUAGE, 301 | payload: { 302 | language: 'en', 303 | translation: { title: 'title', description: 'description' } 304 | }, 305 | languageCodes: ['en', 'fr'] 306 | }; 307 | 308 | const result = translations({}, action); 309 | expect(result).toEqual({ 310 | title: ['title', undefined], 311 | description: ['description', undefined] 312 | }); 313 | }); 314 | 315 | it('should add nested translation for specific language', () => { 316 | const action = { 317 | type: ADD_TRANSLATION_FOR_LANGUAGE, 318 | payload: { 319 | language: 'en', 320 | translation: { 321 | movie: { title: 'title', description: 'description' } 322 | } 323 | }, 324 | languageCodes: ['en', 'fr'] 325 | }; 326 | 327 | const result = translations({}, action); 328 | expect(result).toEqual({ 329 | 'movie.title': ['title', undefined], 330 | 'movie.description': ['description', undefined] 331 | }); 332 | }); 333 | 334 | it('should add translation for specific language to existing translation', () => { 335 | const action = { 336 | type: ADD_TRANSLATION_FOR_LANGUAGE, 337 | payload: { 338 | language: 'en', 339 | translation: { title: 'title', description: 'description' } 340 | }, 341 | languageCodes: ['en', 'fr'] 342 | }; 343 | 344 | const result = translations( 345 | { 346 | title: [undefined, 'titlefr'], 347 | description: [undefined, 'descriptionfr'] 348 | }, 349 | action 350 | ); 351 | 352 | expect(result).toEqual({ 353 | title: ['title', 'titlefr'], 354 | description: ['description', 'descriptionfr'] 355 | }); 356 | }); 357 | 358 | it('should add translation for specific language and override existing translation', () => { 359 | const action = { 360 | type: ADD_TRANSLATION_FOR_LANGUAGE, 361 | payload: { 362 | language: 'fr', 363 | translation: { title: 'title', description: 'description' } 364 | }, 365 | languageCodes: ['en', 'fr'] 366 | }; 367 | 368 | const result = translations( 369 | { 370 | title: [undefined, 'titlefr'], 371 | description: [undefined, 'descriptionfr'] 372 | }, 373 | action 374 | ); 375 | 376 | expect(result).toEqual({ 377 | title: [undefined, 'title'], 378 | description: [undefined, 'description'] 379 | }); 380 | }); 381 | }); 382 | }); 383 | 384 | describe('reducer: options', () => { 385 | describe('INITIALIZE', () => { 386 | it('should set defaultLanguage option', () => { 387 | const action = { 388 | type: INITIALIZE, 389 | languageCodes: ['en', 'fr', 'ne'], 390 | payload: { 391 | options: { 392 | defaultLanguage: 'fr', 393 | renderToStaticMarkup: false 394 | } 395 | } 396 | }; 397 | const result = options(defaultTranslateOptions, action); 398 | expect(result).toEqual({ 399 | ...defaultTranslateOptions, 400 | defaultLanguage: 'fr', 401 | renderToStaticMarkup: false 402 | }); 403 | }); 404 | 405 | it('should set renderInnerHtml option', () => { 406 | const action = { 407 | type: INITIALIZE, 408 | languageCodes: ['en', 'fr', 'ne'], 409 | payload: { 410 | options: { 411 | renderInnerHtml: false, 412 | renderToStaticMarkup: false 413 | } 414 | } 415 | }; 416 | const result = options(defaultTranslateOptions, action); 417 | expect(result).toEqual({ 418 | ...defaultTranslateOptions, 419 | defaultLanguage: 'en', 420 | renderInnerHtml: false, 421 | renderToStaticMarkup: false 422 | }); 423 | }); 424 | }); 425 | 426 | it('should set translationTransform option', () => { 427 | const action = { 428 | type: INITIALIZE, 429 | languageCodes: ['en', 'fr', 'ne'], 430 | payload: { 431 | options: { 432 | translationTransform: () => ({}), 433 | renderToStaticMarkup: false 434 | } 435 | } 436 | }; 437 | 438 | const result = options({}, action); 439 | expect(result.translationTransform).toBeDefined(); 440 | expect(typeof result.translationTransform).toEqual('function'); 441 | }); 442 | 443 | it('should override default onMissingTranslation option', () => { 444 | const callback = jest.fn(); 445 | callback.mockReturnValueOnce('Override missing!'); 446 | 447 | const action = { 448 | type: INITIALIZE, 449 | languageCodes: ['en', 'fr', 'ne'], 450 | payload: { 451 | options: { 452 | onMissingTranslation: callback, 453 | renderToStaticMarkup: false 454 | } 455 | } 456 | }; 457 | 458 | const result = options({}, action); 459 | const value = result.onMissingTranslation(); 460 | 461 | expect(result.onMissingTranslation).toBeDefined(); 462 | expect(callback).toHaveBeenCalled(); 463 | expect(value).toEqual('Override missing!'); 464 | }); 465 | 466 | it('should set renderToStaticMarkup when function provided', () => { 467 | const action = { 468 | type: INITIALIZE, 469 | languageCodes: ['en', 'fr', 'ne'], 470 | payload: { 471 | options: { 472 | renderToStaticMarkup: () => {} 473 | } 474 | } 475 | }; 476 | const result = options({}, action); 477 | expect(result.renderToStaticMarkup).toEqual( 478 | action.payload.options.renderToStaticMarkup 479 | ); 480 | }); 481 | 482 | it('should set renderToStaticMarkup to false when provided', () => { 483 | const action = { 484 | type: INITIALIZE, 485 | languageCodes: ['en', 'fr', 'ne'], 486 | payload: { 487 | options: { 488 | renderToStaticMarkup: false 489 | } 490 | } 491 | }; 492 | const result = options({}, action); 493 | expect(result.renderToStaticMarkup).toBe(false); 494 | }); 495 | 496 | it('should throw an error when invalid renderToStaticMarkup not provided', () => { 497 | const action = { 498 | type: INITIALIZE, 499 | languageCodes: ['en', 'fr', 'ne'], 500 | payload: { 501 | options: {} 502 | } 503 | }; 504 | const result = () => options({}, action); 505 | expect(result).toThrow(); 506 | }); 507 | 508 | it('should set ignoreTranslateChildren', () => { 509 | const action = { 510 | type: INITIALIZE, 511 | languageCodes: ['en', 'fr', 'ne'], 512 | payload: { 513 | options: { 514 | renderToStaticMarkup: false, 515 | ignoreTranslateChildren: true 516 | } 517 | } 518 | }; 519 | const result = options({}, action); 520 | expect(result.ignoreTranslateChildren).toBe(true); 521 | }); 522 | 523 | it('should use first language as default language if not set', () => { 524 | const action = { 525 | type: INITIALIZE, 526 | languageCodes: ['en', 'fr', 'ne'], 527 | payload: { options: { renderToStaticMarkup: false } } 528 | }; 529 | const result = options({}, action); 530 | expect(result.defaultLanguage).toBe('en'); 531 | }); 532 | }); 533 | 534 | describe('getActiveLanguage', () => { 535 | it('should return the active language object', () => { 536 | const state = { 537 | languages: [ 538 | { code: 'en', active: false }, 539 | { code: 'fr', active: true }, 540 | { code: 'ne', active: false } 541 | ] 542 | }; 543 | const result = getActiveLanguage(state); 544 | expect(result.code).toBe('fr'); 545 | }); 546 | 547 | it('should return undefined if no active language found', () => { 548 | const state = { 549 | languages: [ 550 | { code: 'en', active: false }, 551 | { code: 'fr', active: false } 552 | ] 553 | }; 554 | const result = getActiveLanguage(state); 555 | expect(result).toBe(undefined); 556 | }); 557 | 558 | it('should return undefined if no languages found', () => { 559 | const state = { 560 | languages: [] 561 | }; 562 | const result = getActiveLanguage(state); 563 | expect(result).toBe(undefined); 564 | }); 565 | 566 | it('should return activeLanguage with name', () => { 567 | const state = { 568 | languages: [ 569 | { code: 'en', name: 'English', active: false }, 570 | { code: 'fr', name: 'French', active: true } 571 | ] 572 | }; 573 | const result = getActiveLanguage(state); 574 | expect(result).toEqual({ 575 | code: 'fr', 576 | name: 'French', 577 | active: true 578 | }); 579 | }); 580 | }); 581 | 582 | describe('getTranslationsForActiveLanguage', () => { 583 | it('should return translations only for the active language', () => { 584 | const state = { 585 | languages: [ 586 | { code: 'en', active: false }, 587 | { code: 'fr', active: true } 588 | ], 589 | translations: { 590 | hi: ['hi-en', 'hi-fr'], 591 | bye: ['bye-en', 'bye-fr'] 592 | } 593 | }; 594 | const result = getTranslationsForActiveLanguage(state); 595 | expect(result).toEqual({ 596 | hi: 'hi-fr', 597 | bye: 'bye-fr' 598 | }); 599 | }); 600 | 601 | it('should return empty object if no active language found', () => { 602 | const state = { 603 | languages: [], 604 | translations: { 605 | hi: ['hi-en', 'hi-fr'], 606 | bye: ['bye-en', 'bye-fr'] 607 | } 608 | }; 609 | const result = getTranslationsForActiveLanguage(state); 610 | expect(result).toEqual({}); 611 | }); 612 | }); 613 | 614 | describe('getTranslationsForSpecificLanguage', () => { 615 | it('should return translations only for specific language', () => { 616 | const state = { 617 | languages: [ 618 | { code: 'en', active: false }, 619 | { code: 'fr', active: true } 620 | ], 621 | translations: { 622 | hi: ['hi-en', 'hi-fr'], 623 | bye: ['bye-en', 'bye-fr'] 624 | } 625 | }; 626 | const result = getTranslationsForSpecificLanguage(state)('en'); 627 | expect(result).toEqual({ 628 | hi: 'hi-en', 629 | bye: 'bye-en' 630 | }); 631 | }); 632 | 633 | it('should return empty object if language not found', () => { 634 | const state = { 635 | languages: [], 636 | translations: { 637 | hi: ['hi-en', 'hi-fr'], 638 | bye: ['bye-en', 'bye-fr'] 639 | } 640 | }; 641 | const result = getTranslationsForSpecificLanguage(state)({ code: 'ze' }); 642 | expect(result).toEqual({}); 643 | }); 644 | }); 645 | 646 | describe('getTranslate', () => { 647 | let state = {}; 648 | 649 | beforeEach(() => { 650 | state = { 651 | languages: [ 652 | { code: 'en', active: false }, 653 | { code: 'fr', active: true } 654 | ], 655 | translations: { 656 | hi: ['hi-en', 'hi-fr'], 657 | bye: ['bye-en', 'bye-fr'], 658 | yo: ['yo ${ name }', 'yo-fr ${ name }'], 659 | foo: ['foo ${ bar }', 'foo-fr ${ bar }'], 660 | html: ['hi-en', 'hi-fr'], 661 | money_no_translation: ['save $${ amount }'] 662 | }, 663 | options: defaultTranslateOptions 664 | }; 665 | }); 666 | 667 | it('should throw an error when invalid key provided to translate function', () => { 668 | const translate = getTranslate(state); 669 | expect(() => translate(23)).toThrow(); 670 | }); 671 | 672 | it('should return single translated element when valid key provided', () => { 673 | const translate = getTranslate(state); 674 | const value = translate('hi'); 675 | expect(value).toBe('hi-fr'); 676 | }); 677 | 678 | it('should not render inner html if renderInnerHtml option is false', () => { 679 | const stateWithOpts = { ...state, options: { renderInnerHtml: false } }; 680 | const translate = getTranslate(stateWithOpts); 681 | 682 | const value = translate('html'); 683 | expect(value).toBe('hi-fr'); 684 | }); 685 | 686 | it('should render inner html if renderInnerHtml option is true', () => { 687 | const stateWithOpts = { ...state, options: { renderInnerHtml: true } }; 688 | const translate = getTranslate(stateWithOpts); 689 | const wrapper = shallow(translate('html')); 690 | 691 | expect(wrapper.find('span').exists()).toBe(true); 692 | expect(wrapper.html()).toEqual(`hi-fr`); 693 | }); 694 | 695 | it('should override renderInnerHtml to true when passed to translate', () => { 696 | const newState = { ...state, options: { renderInnerHtml: false } }; 697 | const translate = getTranslate(newState); 698 | const wrapper = shallow( 699 | translate('html', null, { renderInnerHtml: true }) 700 | ); 701 | 702 | expect(wrapper.find('span').exists()).toBe(true); 703 | expect(wrapper.html()).toEqual(`hi-fr`); 704 | }); 705 | 706 | it('should override renderInnerHtml to false when passed to translate', () => { 707 | const newState = { ...state, options: { renderInnerHtml: true } }; 708 | const translate = getTranslate(newState); 709 | const result = translate('html', null, { renderInnerHtml: false }); 710 | 711 | expect(result).toEqual('hi-fr'); 712 | }); 713 | 714 | it('should return an object of translation keys matched with translated element', () => { 715 | const translate = getTranslate(state); 716 | const result = translate(['hi', 'bye']); 717 | 718 | Object.keys(result).map((key, index) => { 719 | const value = result[key]; 720 | expect(value).toBe(state.translations[key][1]); 721 | }); 722 | }); 723 | 724 | it('should insert dynamic data for single translation', () => { 725 | const translate = getTranslate(state); 726 | const result = translate('yo', { name: 'ted' }); 727 | // const wrapper = shallow(element); 728 | expect(result).toBe('yo-fr ted'); 729 | }); 730 | 731 | it('should insert dynamic data for multiple translations', () => { 732 | const translate = getTranslate(state); 733 | const result = translate(['yo', 'foo'], { name: 'ted', bar: 'bar' }); 734 | const results = ['yo-fr ted', 'foo-fr bar']; 735 | 736 | Object.keys(result).map((key, index) => { 737 | const value = result[key]; 738 | expect(value).toBe(results[index]); 739 | }); 740 | }); 741 | 742 | it('should return value from default onMissingTranslation option', () => { 743 | state.options.onMissingTranslation = 744 | defaultTranslateOptions.onMissingTranslation; 745 | const translate = getTranslate(state); 746 | const result = translate('nothinghere'); 747 | expect(result).toEqual( 748 | 'Missing translationId: nothinghere for language: fr' 749 | ); 750 | }); 751 | 752 | it('should set first language available as default when no default is set', () => { 753 | state = localizeReducer({ languages: [] }, { 754 | type: INITIALIZE, 755 | payload: { 756 | languages: ['en', 'fr', 'es'], 757 | options: { renderToStaticMarkup: false } 758 | } 759 | }); 760 | 761 | const options = getOptions(state); 762 | expect(options.defaultLanguage).toBe(state.languages[0].code); 763 | }); 764 | 765 | it('should return value using default language when missing a translation with USD hard coded into translation', () => { 766 | state = localizeReducer({ languages: [], translations: state.translations }, { 767 | type: INITIALIZE, 768 | payload: { 769 | languages: ['en', 'fr', 'es'], 770 | options: { renderToStaticMarkup: false }, 771 | } 772 | }); 773 | state.options.onMissingTranslation = ({ defaultTranslation }) => defaultTranslation; 774 | const key = 'money_no_translation'; 775 | const translate = getTranslate(state); 776 | const result = translate([key], { amount: 100 }); 777 | expect(result[key]).toBe('save $100') 778 | }); 779 | 780 | it('should return value from onMissingTranslation option override', () => { 781 | state.options.onMissingTranslation = ({ translationId, languageCode }) => 782 | '${translationId} - ${languageCode}'; 783 | const translate = getTranslate(state); 784 | const result = translate('nothinghere'); 785 | expect(result).toEqual('nothinghere - fr'); 786 | }); 787 | 788 | it('should use language option instead of activeLanguage for translations', () => { 789 | const translate = getTranslate(state); 790 | const result = translate('hi', null, { language: 'en' }); 791 | expect(result).toEqual('hi-en'); 792 | }); 793 | }); 794 | 795 | describe('translationsEqualSelector', () => { 796 | let languages = []; 797 | let activeLanguage = {}; 798 | let translations = {}; 799 | 800 | beforeEach(() => { 801 | languages = [{ code: 'en', active: false }, { code: 'fr', active: true }]; 802 | activeLanguage = { code: 'en', active: true }; 803 | translations = { 804 | one: 'one', 805 | two: 'two', 806 | three: 'three' 807 | }; 808 | }); 809 | 810 | it('should call result function when languages changes', () => { 811 | const result = jest.fn(); 812 | const selector = translationsEqualSelector(() => languages, result); 813 | selector({}); 814 | languages = [...languages, [...{ code: 'ca', active: false }]]; 815 | selector({}); 816 | expect(result).toHaveBeenCalledTimes(2); 817 | }); 818 | 819 | it("should not call result function when languages haven't changed", () => { 820 | const result = jest.fn(); 821 | const selector = translationsEqualSelector(() => languages, result); 822 | selector({}); 823 | selector({}); 824 | expect(result).toHaveBeenCalledTimes(1); 825 | }); 826 | 827 | it('should calla result function when active language changes', () => { 828 | const result = jest.fn(); 829 | const selector = translationsEqualSelector(() => activeLanguage, result); 830 | selector({}); 831 | activeLanguage = { code: 'ca', active: false }; 832 | selector({}); 833 | expect(result).toHaveBeenCalledTimes(2); 834 | }); 835 | 836 | it("should not call result function when active language hasn't changed", () => { 837 | const result = jest.fn(); 838 | const selector = translationsEqualSelector(() => activeLanguage, result); 839 | selector({}); 840 | selector({}); 841 | expect(result).toHaveBeenCalledTimes(1); 842 | }); 843 | 844 | it('should call result function when translations change', () => { 845 | const result = jest.fn(); 846 | const selector = translationsEqualSelector(() => translations, result); 847 | selector({}); 848 | translations = { ...translations, four: 'four' }; 849 | selector({}); 850 | expect(result).toHaveBeenCalledTimes(2); 851 | }); 852 | 853 | it("should not call result function when translations haven't changed", () => { 854 | const result = jest.fn(); 855 | const selector = translationsEqualSelector(() => translations, result); 856 | selector({}); 857 | selector({}); 858 | expect(result).toHaveBeenCalledTimes(1); 859 | }); 860 | 861 | it('should call result function when new value is added to translation', () => { 862 | const result = jest.fn(); 863 | let initialTranslations = { 864 | title: ['title', undefined, undefined] 865 | }; 866 | 867 | const selector = translationsEqualSelector( 868 | () => initialTranslations, 869 | result 870 | ); 871 | selector({}); 872 | initialTranslations = { title: ['title', 'title FR', undefined] }; 873 | selector({}); 874 | expect(result).toHaveBeenCalledTimes(2); 875 | }); 876 | 877 | it('should not call result function when value has not changed', () => { 878 | const result = jest.fn(); 879 | let initialTranslations = { 880 | title: ['title', 'title2', undefined] 881 | }; 882 | 883 | const selector = translationsEqualSelector( 884 | () => initialTranslations, 885 | result 886 | ); 887 | selector({}); 888 | selector({}); 889 | expect(result).toHaveBeenCalledTimes(1); 890 | }); 891 | }); 892 | }); 893 | --------------------------------------------------------------------------------