├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .noderequirer.json
├── .npmignore
├── LICENSE
├── README.md
├── package.json
└── src
├── actionTypes.js
├── actions.js
├── getLang.js
├── index.js
└── store.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0"],
3 | "plugins": ["transform-runtime"]
4 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | lib
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "no-undef": [2],
5 | "no-trailing-spaces": [1],
6 | "space-before-blocks": [2, "always"],
7 | "no-unused-expressions": [0],
8 | "no-underscore-dangle": [0],
9 | "quote-props": [1, "as-needed"],
10 | "no-multi-spaces": [0],
11 | "no-unused-vars": [1],
12 | "no-loop-func": [0],
13 | "key-spacing": [0],
14 | "max-len": [1, 100],
15 | "strict": [0],
16 | "eol-last": [1],
17 | "no-console": [1],
18 | "indent": [1, 2],
19 | "quotes": [2, "single", "avoid-escape"],
20 | "curly": [0],
21 |
22 | "generator-star-spacing": 0,
23 | "new-cap": 0,
24 | "object-curly-spacing": 0,
25 | "object-shorthand": 0,
26 |
27 | "babel/generator-star-spacing": 1,
28 | "babel/new-cap": 1,
29 | "babel/object-curly-spacing": [1, "always"],
30 | "babel/object-shorthand": 1
31 | },
32 | "plugins": [
33 | "babel"
34 | ],
35 | "settings": {
36 | "ecmascript": 6,
37 | "jsx": true
38 | },
39 | "env": {
40 | "browser": true,
41 | "node": true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 | lib
5 |
--------------------------------------------------------------------------------
/.noderequirer.json:
--------------------------------------------------------------------------------
1 | {
2 | "import": true
3 | }
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | static
2 | src
3 | demo
4 | .*
5 | server.js
6 | webpack.config.js
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Alexander Kuznetsov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-pagan
2 | managing internationalization via redux
3 |
4 | (see `react-pagan` [demo](http://alexkuz.github.io/react-pagan/))
5 |
6 | #### Setup redux store
7 |
8 | Include i18n reducer in redux:
9 |
10 | ```js
11 | import { i18n } from 'redux-pagan';
12 |
13 | const createStoreWithMiddleware = applyMiddleware(
14 | thunk
15 | )(createStore);
16 |
17 | const rootReducer = combineReducers({
18 | i18n
19 | });
20 |
21 | const store = createStoreWithMiddleware(rootReducer);
22 | ...
23 | ```
24 |
25 | #### Loading lang data
26 |
27 | ```js
28 | import { loadLang } from 'redux-pagan';
29 |
30 | function getLangData(locale) {
31 | // here we use promise-loader to load lang data by demand
32 | return require('promise?global,[name].i18n!json!./i18n/' + locale + '.i18n.json');
33 | }
34 |
35 | @connect(...)
36 | class App extends Component {
37 | componentDidMount() {
38 | this.props.dispatch(loadLang(cookie.get('lang') || 'en-US', getLangData));
39 | }
40 |
41 | render() {
42 | return (
43 |
49 | );
50 | }
51 |
52 | handleLocaleChange = e => {
53 | this.props.dispatch(loadLang(e.target.value, getLangData));
54 | }
55 | }
56 | ```
57 |
58 | #### Using lang data
59 |
60 | `getLang(state.i18n, ...path)` is a special method that safely obtains strings from lang data. If no strings were found on stated path, last key is returned. This method's calling is chainable:
61 | ```js
62 | getLang(state.i18n, 'some', 'path', 'to')('lang', 'string')
63 | ```
64 | Every call is memoized. To receive string value from last call, use `.toString()` or `.s` property.
65 |
66 | ```js
67 | // in this case lang data looks like:
68 | // {
69 | // "app": {
70 | // "some": {
71 | // "text": "foobar"
72 | // },
73 | // "element": {
74 | // "something": {
75 | // "else": "foobar"
76 | // }
77 | // }
78 | // }
79 | // }
80 |
81 | import { connect } from 'react-redux';
82 | import { getLang } from 'redux-pagan';
83 |
84 | @connect(state => ({
85 | lang: getLang(state.i18n, 'app'),
86 | locale: state.i18n.locale
87 | }))
88 | class App extends Component {
89 | render() {
90 | // string methods are proxied, no need to call .toString() either
91 | const str = this.props.lang('some', 'text').replace('foo', 'bar');
92 |
93 | return (
94 |
95 | {this.props.lang('some', 'text').s}
96 |
97 |
98 | );
99 | }
100 | }
101 |
102 | class Element extends Component {
103 | render() {
104 | return (
105 |
106 | {this.props.lang('something', 'else').s}
107 |
108 | );
109 | }
110 | }
111 | ```
112 |
113 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-pagan",
3 | "version": "0.2.0",
4 | "description": "redux binding for react-pagan internationalization module",
5 | "scripts": {
6 | "build": "./node_modules/.bin/babel src --out-dir lib",
7 | "start": "node server.js",
8 | "lint": "eslint src",
9 | "version": "npm run build && git add -A .",
10 | "postversion": "git push",
11 | "prepublish": "npm run build"
12 | },
13 | "main": "lib/index.js",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/alexkuz/redux-pagan.git"
17 | },
18 | "keywords": [
19 | "redux",
20 | "intl",
21 | "i18n"
22 | ],
23 | "author": "Alexander (http://kuzya.org/)",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/alexkuz/redux-pagan/issues"
27 | },
28 | "homepage": "https://github.com/alexkuz/redux-pagan",
29 | "devDependencies": {
30 | "babel-cli": "^6.0.0",
31 | "babel-core": "^6.0.0",
32 | "babel-eslint": "^6.0.0",
33 | "babel-runtime": "^6.0.0",
34 | "babel-preset-es2015": "^6.0.0",
35 | "babel-preset-stage-0": "^6.0.0",
36 | "babel-plugin-transform-runtime": "^6.0.0",
37 | "eslint-loader": "^1.0.0",
38 | "eslint-plugin-babel": "^3.2.0"
39 | },
40 | "dependencies": {
41 | "lodash.memoize": "^3.0.4"
42 | },
43 | "peerDependencies": {
44 | "intl-messageformat": "^1.1.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const LOAD_LANG = '@@i18n/loadLang';
2 | export const LOAD_LANG_SUCCESS = '@@i18n/loadLangSuccess';
3 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | import { LOAD_LANG, LOAD_LANG_SUCCESS } from './actionTypes';
2 |
3 |
4 | export function loadLang(locale, data) {
5 | return async dispatch => {
6 | dispatch({
7 | type: LOAD_LANG,
8 | locale
9 | });
10 |
11 | let loader = typeof data === 'function' ? data(locale) : data;
12 | let lang;
13 |
14 | if (typeof loader === 'function') {
15 | lang = await loader();
16 | } else {
17 | lang = loader;
18 | }
19 |
20 | dispatch({
21 | type: LOAD_LANG_SUCCESS,
22 | locale,
23 | lang
24 | });
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/getLang.js:
--------------------------------------------------------------------------------
1 | import memoize from 'lodash.memoize';
2 | import IntlMessageFormat from 'intl-messageformat';
3 |
4 | /* eslint-disable no-console */
5 |
6 | function isEmpty(data) {
7 | return !data || Object.keys(data).length === 0;
8 | }
9 |
10 | const emptyLocaleDataWarned = {};
11 | const notFoundKeyWarned = {};
12 |
13 | function getLangString(locale, data, fullpath) {
14 | if (fullpath.filter(key => typeof key !== 'string').length > 0) {
15 | if (process.env.NODE_ENV !== 'production') {
16 | console.error('Invalid langpack path: ', fullpath);
17 | }
18 | }
19 |
20 | const str = fullpath.reduce(
21 | (obj, key, idx) => {
22 | if (idx === fullpath.length - 1) {
23 | if (obj && (obj[key] !== undefined)) {
24 | return obj[key].toString();
25 | } else {
26 | const keyPath = `${locale}:${fullpath.join('/')}`;
27 | if (!isEmpty(data) && !notFoundKeyWarned[keyPath]) {
28 | if (process.env.NODE_ENV !== 'production') {
29 | console.error(`Redux-Pagan: key was not found at path: ${keyPath}`);
30 | }
31 | notFoundKeyWarned[keyPath] = true;
32 | }
33 | return key;
34 | }
35 | } else {
36 | return (obj && obj[key]) ? obj[key] : null
37 | }
38 | },
39 | data
40 | );
41 |
42 | if (str && !(typeof str === 'string')) {
43 | if (process.env.NODE_ENV !== 'production') {
44 | console.error('String expected, got: ', str);
45 | }
46 | }
47 |
48 | return str;
49 | }
50 |
51 | function concatPath(locale, data, path, subpath) {
52 | if (isEmpty(data) && !emptyLocaleDataWarned[locale] && locale !== null) {
53 | if (process.env.NODE_ENV !== 'production') {
54 | console.error(`Redux-Pagan: got empty data for locale '${locale}'`);
55 | }
56 | emptyLocaleDataWarned[locale] = true;
57 | }
58 |
59 | if (subpath.length === 0) {
60 | return getLangString(locale, data, path);
61 | }
62 | const _path = [...path, ...subpath];
63 |
64 | const i18nPartial = function(..._subpath) {
65 | return concatPath(locale, data, _path, _subpath);
66 | };
67 |
68 | const memoizedI18nPartial = memoize(i18nPartial, function(..._subpath) {
69 | return [locale, ..._path, ..._subpath].join(',');
70 | });
71 |
72 | memoizedI18nPartial.toString = function() {
73 | return getLangString(locale, data, _path);
74 | }
75 |
76 | memoizedI18nPartial.format = function(values) {
77 | const str = getLangString(locale, data, _path);
78 | const formatter = new IntlMessageFormat(str, locale);
79 | return formatter.format(values);
80 | }
81 |
82 | Object.defineProperty(memoizedI18nPartial, 's', {
83 | get() { return this.toString(); }
84 | });
85 |
86 | // proxy string methods
87 | Object.getOwnPropertyNames(String.prototype).forEach(prop => {
88 | if (typeof String.prototype[prop] === 'function' &&
89 | ['constructor', 'toString', 'valueOf'].indexOf(prop) === -1) { // find more elegant way maybe
90 | Object.defineProperty(memoizedI18nPartial, prop, {
91 | get() { return this.toString()[prop]; }
92 | });
93 | }
94 | });
95 |
96 | const desc = Object.getOwnPropertyDescriptor(memoizedI18nPartial, Symbol.iterator);
97 |
98 | if (!desc || desc.configurable) {
99 | memoizedI18nPartial[Symbol.iterator] = function *() {
100 | yield memoizedI18nPartial.toString();
101 | }
102 | }
103 |
104 | return memoizedI18nPartial;
105 | }
106 |
107 | /* eslint-enable no-console */
108 |
109 | const i18nResolver = memoize(function i18nResolver(locale, data, version, ...path) {
110 | return concatPath(locale, data, [], path);
111 | }, function(locale, data, version, ...path) {
112 | return [locale, version, ...path].join(',');
113 | });
114 |
115 | export default function getLang(i18n, ...args) {
116 | return i18nResolver(i18n.locale, i18n.data[i18n.locale], i18n.__version__, ...args);
117 | }
118 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { loadLang } from './actions';
2 | export i18n from './store';
3 | export getLang from './getLang';
4 | export { LOAD_LANG, LOAD_LANG_SUCCESS } from './actions';
5 |
6 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { LOAD_LANG_SUCCESS } from './actionTypes';
2 |
3 | const DEFAULT_STATE = {
4 | __version__: 0,
5 | locale: null,
6 | data: {}
7 | };
8 |
9 | const mergeLang = (state, { locale, lang }) => ({
10 | ...state,
11 | data: {
12 | ...state.data,
13 | [locale]: lang
14 | },
15 | locale,
16 | __version__: state.__version__ + 1
17 | });
18 |
19 | export default function i18n(state = DEFAULT_STATE, action) {
20 | return (({
21 | [LOAD_LANG_SUCCESS]: mergeLang
22 | })[action.type] || (s => s))(state, action);
23 | }
24 |
--------------------------------------------------------------------------------