From a4ae3019959b2c545b81e35fb10fea09f24c198b Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 30 Apr 2017 19:25:26 +0300 Subject: [PATCH 01/25] Added version to the readme file --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8e71f2..d4db019 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# react-gettext +# react-gettext 0.3.0 Tiny React library for implementing gettext localization in your application. It provides HOC function to enhance your application by exposing gettext functions in the context scope. @@ -181,4 +181,4 @@ What to help or have a suggestion? Open a [new ticket](https://github.com/eugene ## License -MIT \ No newline at end of file +MIT From 62dc40aba3f3ea04ff7ec17cc067a4d8ee4f0e88 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 30 Apr 2017 19:57:59 +0300 Subject: [PATCH 02/25] Added travis ci config --- .travis.yml | 6 ++++++ README.md | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bb899cf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: javascript +before_install: + - "yarn" + - "npm run build" +script: + - "npm test" diff --git a/README.md b/README.md index d4db019..b139b4d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # react-gettext 0.3.0 +[![Build Status](https://travis-ci.org/eugene-manuilov/react-gettext.svg?branch=master)](https://travis-ci.org/eugene-manuilov/react-gettext) + Tiny React library for implementing gettext localization in your application. It provides HOC function to enhance your application by exposing gettext functions in the context scope. ## Instalation @@ -177,7 +179,7 @@ If you use Poedit app to translate your messages, then you can use `gettext;nget ## Contribute -What to help or have a suggestion? Open a [new ticket](https://github.com/eugene-manuilov/react-gettext/issues/new) and we can discuss it or submit pull request. Please, make sure you run `npm run test` before submitting a pull request. +What to help or have a suggestion? Open a [new ticket](https://github.com/eugene-manuilov/react-gettext/issues/new) and we can discuss it or submit pull request. Please, make sure you run `npm test` before submitting a pull request. ## License From 9454251d0de15fef160d120d7f790b4dd69f1cdc Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 30 Apr 2017 20:02:11 +0300 Subject: [PATCH 03/25] Updated language in .travis.yml file to be node_js --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bb899cf..265a27a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -language: javascript +language: node_js before_install: - "yarn" - "npm run build" From 1cba1b8bab1ca64f357f79061a6cda47a3fcb0e7 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 30 Apr 2017 20:04:57 +0300 Subject: [PATCH 04/25] Added node_js version to use 6.x --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 265a27a..15dbbc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: node_js +node_js: + - "6" + before_install: - "yarn" - "npm run build" + script: - "npm test" From da0959e3d28ac2e6de6dfc0fe358d348f84c4135 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 30 Apr 2017 20:11:10 +0300 Subject: [PATCH 05/25] Removed redundant before_install commands --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 15dbbc4..75aab84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,5 @@ language: node_js node_js: - "6" -before_install: - - "yarn" - - "npm run build" - script: - "npm test" From 08d566997690d1a4c30ecd74bb9369861a4c08a5 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 30 Apr 2017 20:27:38 +0300 Subject: [PATCH 06/25] Added nxgettext function --- .travis.yml | 1 - src/index.js | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 75aab84..cd30b24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js node_js: - "6" - script: - "npm test" diff --git a/src/index.js b/src/index.js index 25ec5db..d5d78bf 100644 --- a/src/index.js +++ b/src/index.js @@ -59,11 +59,16 @@ export default function textdomain(translations, pluralForm) { return messages[key] ? messages[key] : message; } + static nxgettext(singular, plural, n, context) { + return WithGettext.ngettex(singular, plural, n); + } + getChildContext() { return { gettext: WithGettext.gettext, ngettext: WithGettext.ngettext, xgettext: WithGettext.xgettext, + nxgettext: WithGettext.nxgettext }; } @@ -78,6 +83,7 @@ export default function textdomain(translations, pluralForm) { gettext: PropTypes.func, ngettext: PropTypes.func, xgettext: PropTypes.func, + nxgettext: PropTypes.func }; return hoistNonReactStatic(WithGettext, WrappedComponent); From 0a616ae192f2707a166d10be6e25c59ef61612e4 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 08:06:04 +0300 Subject: [PATCH 07/25] Added nxgettext function to context tests --- README.md | 2 +- __tests__/__snapshots__/context.js.snap | 14 +++++++++++--- __tests__/context.js | 15 ++++++++++----- __tests__/higher-order-component.js | 7 +++++-- src/index.js | 8 ++++---- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b139b4d..fa28b86 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ this.context.xgettext('some text', 'context where this message is used'); ## Poedit -If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c` as keywords list to properly parse and extract strings from your javascript files. +If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c` as keywords list to properly parse and extract strings from your javascript files. ## Contribute diff --git a/__tests__/__snapshots__/context.js.snap b/__tests__/__snapshots__/context.js.snap index 2383584..6d857c7 100644 --- a/__tests__/__snapshots__/context.js.snap +++ b/__tests__/__snapshots__/context.js.snap @@ -7,7 +7,7 @@ exports[`Child component can use context functions 1`] = ` gettext: - 7[|UyK-%T\`CKw-%j $1/6XTzdd(8+cY,/JCngNVv+wQO6NAv:+^=4GA[,+G@^GI< + thiwwext
  • @@ -15,7 +15,7 @@ exports[`Child component can use context functions 1`] = ` xgettext: - T6%(h7m%J*O(Bm6!FiRk0 9;V]r\`kPz-ROW7E8*t.Aki9}j=hU9xfU8X|.7wEuZ0 + thfefeext
  • @@ -23,7 +23,15 @@ exports[`Child component can use context functions 1`] = ` ngettext: - ZgcaUXD7<qhRY#>A~@-\`y2%xm@t{TLsN1LvUL\`6zamv|e}G|rY\`ms-|aMxn#_2f{ + thfeferal + +
  • +
  • +

    + nxgettext +

    + + two
  • diff --git a/__tests__/context.js b/__tests__/context.js index a0ff835..7bdf304 100644 --- a/__tests__/context.js +++ b/__tests__/context.js @@ -17,15 +17,19 @@ class C1 extends Component {
    • gettext:

      - {this.context.gettext(this.props.gettext)} + {this.context.gettext('thiwwext')}
    • xgettext:

      - {this.context.xgettext(this.props.xgettext, this.props.context)} + {this.context.xgettext('thfefeext', 'twfwfwtext')}
    • ngettext:

      - {this.context.ngettext(this.props.singular, this.props.plural, this.props.num)} + {this.context.ngettext('fefengular', 'thfeferal', this.props.num)} +
    • +
    • +

      nxgettext

      + {this.context.nxgettext('single', 'two', 4, 'test context')}
    ); @@ -35,7 +39,8 @@ class C1 extends Component { C1.contextTypes = { gettext: PropTypes.func.isRequired, xgettext: PropTypes.func.isRequired, - ngettext: PropTypes.func.isRequired + ngettext: PropTypes.func.isRequired, + nxgettext: PropTypes.func.isRequired, }; test('Child component can use context functions', () => { @@ -65,4 +70,4 @@ test('Child component can use context functions', () => { let tree = component.toJSON(); expect(tree).toMatchSnapshot(); -}); \ No newline at end of file +}); diff --git a/__tests__/higher-order-component.js b/__tests__/higher-order-component.js index 46b3025..330137e 100644 --- a/__tests__/higher-order-component.js +++ b/__tests__/higher-order-component.js @@ -30,7 +30,8 @@ describe('Higher-order component', () => { expect(hoc.childContextTypes).toEqual({ gettext: PropTypes.func, ngettext: PropTypes.func, - xgettext: PropTypes.func + xgettext: PropTypes.func, + nxgettext: PropTypes.func, }); }); @@ -38,6 +39,7 @@ describe('Higher-order component', () => { expect(typeof hoc.gettext).toBe('function'); expect(typeof hoc.ngettext).toBe('function'); expect(typeof hoc.xgettext).toBe('function'); + expect(typeof hoc.nxgettext).toBe('function'); }); }); @@ -50,7 +52,8 @@ describe('Higher-order component instance', () => { expect(hocObject.getChildContext()).toEqual({ gettext: hoc.gettext, ngettext: hoc.ngettext, - xgettext: hoc.xgettext + xgettext: hoc.xgettext, + nxgettext: hoc.nxgettext, }); }); }); \ No newline at end of file diff --git a/src/index.js b/src/index.js index d5d78bf..4b991c3 100644 --- a/src/index.js +++ b/src/index.js @@ -59,8 +59,8 @@ export default function textdomain(translations, pluralForm) { return messages[key] ? messages[key] : message; } - static nxgettext(singular, plural, n, context) { - return WithGettext.ngettex(singular, plural, n); + static nxgettext(singular, plural, n) { + return WithGettext.ngettext(singular, plural, n); } getChildContext() { @@ -68,7 +68,7 @@ export default function textdomain(translations, pluralForm) { gettext: WithGettext.gettext, ngettext: WithGettext.ngettext, xgettext: WithGettext.xgettext, - nxgettext: WithGettext.nxgettext + nxgettext: WithGettext.nxgettext, }; } @@ -83,7 +83,7 @@ export default function textdomain(translations, pluralForm) { gettext: PropTypes.func, ngettext: PropTypes.func, xgettext: PropTypes.func, - nxgettext: PropTypes.func + nxgettext: PropTypes.func, }; return hoistNonReactStatic(WithGettext, WrappedComponent); From 6d5894c8040594c8ea75f38257bd290f776c4d83 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 08:20:48 +0300 Subject: [PATCH 08/25] Added basic standalone component --- src/Textdomain.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Textdomain.js diff --git a/src/Textdomain.js b/src/Textdomain.js new file mode 100644 index 0000000..31b8264 --- /dev/null +++ b/src/Textdomain.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react'; + +class Textdomain extends Component { + + getTranslations() { + const { catalog } = this.props; + + return catalog; + } + + getPluralForm() { + const { pluralForm } = this.props; + + return pluralForm; + } + + render() { + return this.props.children; + } + +} + +Textdomain.contextTypes = { + +}; + +export default Textdomain; \ No newline at end of file From 8f0f2e8483e8b908adb290b968e94f0965666c1a Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 08:32:17 +0300 Subject: [PATCH 09/25] Added gettext functions to standalone component --- src/Textdomain.js | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Textdomain.js b/src/Textdomain.js index 31b8264..fd349e4 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class Textdomain extends Component { @@ -9,9 +10,47 @@ class Textdomain extends Component { } getPluralForm() { - const { pluralForm } = this.props; + const { plural } = this.props; - return pluralForm; + return plural; + } + + getDelimiter() { + return '\u0004'; // End of Transmission (EOT) + } + + gettext(message) { + const messages = this.getTranslations(); + return messages.hasOwnProperty(message) ? messages[message] : message; + } + + ngettext(singular, plural, n) { + const self = this; + const messages = self.getTranslations(); + const pluralIndex = self.getPluralForm(n); + const defaultValue = n > 1 ? plural : singular; + + return messages.hasOwnProperty(singular) && Array.isArray(messages[singular]) && messages[singular][pluralIndex] + ? messages[singular][pluralIndex] + : defaultValue; + } + + xgettext(message, context) { + const self = this; + const EOT = self.getDelimiter(); + const messages = self.getTranslations(); + const key = context + EOT + message; + + return messages.hasOwnProperty(key) ? messages[key] : message; + } + + getChildContext() { + const self = this; + return { + gettext: self.gettext, + xgettext: self.xgettext, + ngettext: self.ngettext, + }; } render() { @@ -20,8 +59,15 @@ class Textdomain extends Component { } -Textdomain.contextTypes = { +Textdomain.propTypes = { + catalog: PropTypes.object, + plural: PropTypes.string, +}; +Textdomain.contextTypes = { + gettext: PropTypes.func, + ngettext: PropTypes.func, + xgettext: PropTypes.func, }; export default Textdomain; \ No newline at end of file From ce256a0a4b3b3caf570ba76e7ba7cb087c3c94f8 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 08:38:13 +0300 Subject: [PATCH 10/25] Renamed HOF to be withGettext --- .eslintrc | 2 +- src/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index cfbf631..69256d2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,6 @@ "func-names": 0, "no-tabs": 0, "react/jsx-filename-extension": 0, - "indent": ["error", "tab"] + "indent": [2, "tab"] } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index 4b991c3..bd0ecdc 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import hoistNonReactStatic from 'hoist-non-react-statics'; -export default function textdomain(translations, pluralForm) { +export default function withGettext(translations, pluralForm) { const EOT = '\u0004'; // End of Transmission const getTranslations = function () { @@ -77,7 +77,7 @@ export default function textdomain(translations, pluralForm) { } } - WithGettext.displayName = `WithGettext(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; + WithGettext.displayName = `withGettext(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; WithGettext.childContextTypes = { gettext: PropTypes.func, From d3d6f515c90d0fb837c12a15541f0c41ec6f3a9f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 10:32:51 +0300 Subject: [PATCH 11/25] Updated version to 0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e0e0c20..e95905f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "bugs": { "url": "https://github.com/eugene-manuilov/react-gettext/issues" }, - "version": "0.2.0", + "version": "0.3.0", "main": "lib/index", "files": [ "*.md", From 6fbe8da3db588827597e07dd09d2ca40c115136b Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 11:21:20 +0300 Subject: [PATCH 12/25] Reworked HOF to use Textdomain component --- .eslintrc | 6 ++- src/Textdomain.js | 89 +++++++++++++++++++++++++++++++++-------- src/index.js | 100 +++++++++++++--------------------------------- 3 files changed, 104 insertions(+), 91 deletions(-) diff --git a/.eslintrc b/.eslintrc index 69256d2..9a44ac7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,9 @@ "rules": { "func-names": 0, "no-tabs": 0, - "react/jsx-filename-extension": 0, - "indent": [2, "tab"] + "max-len": 1, + "class-methods-use-this": 0, + "indent": [2, "tab"], + "react/jsx-filename-extension": 0 } } \ No newline at end of file diff --git a/src/Textdomain.js b/src/Textdomain.js index fd349e4..d365da2 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -1,18 +1,45 @@ -import React, { Component } from 'react'; +import { Component } from 'react'; import PropTypes from 'prop-types'; class Textdomain extends Component { + getChildContext() { + const self = this; + return { + gettext: self.gettext, + xgettext: self.xgettext, + ngettext: self.ngettext, + nxgettext: self.nxgettext, + }; + } + getTranslations() { const { catalog } = this.props; - - return catalog; + return typeof catalog === 'function' ? catalog() : catalog; } - getPluralForm() { + getPluralForm(n) { const { plural } = this.props; - return plural; + // return 0 if n is not integer + if (isNaN(parseInt(n, 10))) { + return 0; + } + + // if pluralForm is function, use it to get plural form index + if (typeof plural === 'function') { + return plural(n); + } + + // if pluralForm is string and contains only "n", "0-9", " ", "=?:%+-/*><&|" + // characters, then we can eval it to calculate plural form + if (typeof plural === 'string' && !plural.match(/[^n0-9 =?:%+-/*><&|]/i)) { + /* eslint-disable no-eval */ + return 0 + eval(plural.toLowerCase().split('n').join(n)); + /* eslint-enable no-eval */ + } + + return 0; } getDelimiter() { @@ -21,7 +48,9 @@ class Textdomain extends Component { gettext(message) { const messages = this.getTranslations(); - return messages.hasOwnProperty(message) ? messages[message] : message; + return Object.prototype.hasOwnProperty.call(messages, message) + ? messages[message] + : message; } ngettext(singular, plural, n) { @@ -30,7 +59,10 @@ class Textdomain extends Component { const pluralIndex = self.getPluralForm(n); const defaultValue = n > 1 ? plural : singular; - return messages.hasOwnProperty(singular) && Array.isArray(messages[singular]) && messages[singular][pluralIndex] + return Object.prototype.hasOwnProperty.call(messages, singular) + && Array.isArray(messages[singular]) + && messages[singular].length >= pluralIndex + && pluralIndex >= 0 ? messages[singular][pluralIndex] : defaultValue; } @@ -41,16 +73,25 @@ class Textdomain extends Component { const messages = self.getTranslations(); const key = context + EOT + message; - return messages.hasOwnProperty(key) ? messages[key] : message; + return Object.prototype.hasOwnProperty.call(messages, key) + ? messages[key] + : message; } - getChildContext() { + nxgettext(singular, plural, n, context) { const self = this; - return { - gettext: self.gettext, - xgettext: self.xgettext, - ngettext: self.ngettext, - }; + const messages = self.getTranslations(); + const pluralIndex = self.getPluralForm(n); + const defaultValue = n > 1 ? plural : singular; + const EOT = self.getDelimiter(); + const key = context + EOT + singular; + + return Object.prototype.hasOwnProperty.call(messages, key) + && Array.isArray(messages[key]) + && messages[key].length >= pluralIndex + && pluralIndex >= 0 + ? messages[key][pluralIndex] + : defaultValue; } render() { @@ -60,14 +101,28 @@ class Textdomain extends Component { } Textdomain.propTypes = { - catalog: PropTypes.object, - plural: PropTypes.string, + catalog: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.objectOf(PropTypes.string), + ]), + plural: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + ]), + children: PropTypes.arrayOf(PropTypes.node), +}; + +Textdomain.defaultProps = { + catalog: {}, + plural: 'n != 1', + children: [], }; Textdomain.contextTypes = { gettext: PropTypes.func, ngettext: PropTypes.func, xgettext: PropTypes.func, + nxgettext: PropTypes.func, }; -export default Textdomain; \ No newline at end of file +export default Textdomain; diff --git a/src/index.js b/src/index.js index bd0ecdc..f663b45 100644 --- a/src/index.js +++ b/src/index.js @@ -1,91 +1,47 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React from 'react'; import hoistNonReactStatic from 'hoist-non-react-statics'; +import Textdomain from './Textdomain'; export default function withGettext(translations, pluralForm) { - const EOT = '\u0004'; // End of Transmission - - const getTranslations = function () { - // if translations is function, call it to get translations object, - // otherwise just use incoming translations object - return typeof translations === 'function' ? translations() : translations; - }; - - const getPluralForm = (n) => { - // return 0 if n is not integer - if (isNaN(parseInt(n, 10))) { - return 0; - } - - // if pluralForm is function, use it to get plural form index - if (typeof pluralForm === 'function') { - return pluralForm(n); - } - - // if pluralForm is string and contains only "n", "0-9", " ", "=?:%+-/*><&|" - // characters, then we can eval it to calculate plural form - if (typeof pluralForm === 'string' && !pluralForm.match(/[^n0-9 =?:%+-/*><&|]/i)) { - /* eslint-disable no-eval */ - return +eval(pluralForm.toLowerCase().split('n').join(n)); - /* eslint-enable no-eval */ - } - - return 0; - }; - - // return HOC function return (WrappedComponent) => { - class WithGettext extends Component { - static gettext(message) { - const messages = getTranslations(); + class WithGettext extends Textdomain { - return messages[message] ? messages[message] : message; + getTranslations() { + // if translations is function, call it to get translations object, + // otherwise just use incoming translations object + return typeof translations === 'function' ? translations() : translations; } - static ngettext(singular, plural, n) { - const messages = getTranslations(); - const pluralIndex = getPluralForm(n); - const defaultValue = n > 1 ? plural : singular; - - return Array.isArray(messages[singular]) && messages[singular][pluralIndex] - ? messages[singular][pluralIndex] - : defaultValue; - } - - static xgettext(message, context) { - const messages = getTranslations(); - const key = context + EOT + message; - - return messages[key] ? messages[key] : message; - } - - static nxgettext(singular, plural, n) { - return WithGettext.ngettext(singular, plural, n); - } - - getChildContext() { - return { - gettext: WithGettext.gettext, - ngettext: WithGettext.ngettext, - xgettext: WithGettext.xgettext, - nxgettext: WithGettext.nxgettext, - }; + getPluralForm(n) { + // return 0 if n is not integer + if (isNaN(parseInt(n, 10))) { + return 0; + } + + // if pluralForm is function, use it to get plural form index + if (typeof pluralForm === 'function') { + return pluralForm(n); + } + + // if pluralForm is string and contains only "n", "0-9", " ", "=?:%+-/*><&|" + // characters, then we can eval it to calculate plural form + if (typeof pluralForm === 'string' && !pluralForm.match(/[^n0-9 =?:%+-/*><&|]/i)) { + /* eslint-disable no-eval */ + return +eval(pluralForm.toLowerCase().split('n').join(n)); + /* eslint-enable no-eval */ + } + + return 0; } render() { return React.createElement(WrappedComponent, this.props); } + } WithGettext.displayName = `withGettext(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; - WithGettext.childContextTypes = { - gettext: PropTypes.func, - ngettext: PropTypes.func, - xgettext: PropTypes.func, - nxgettext: PropTypes.func, - }; - return hoistNonReactStatic(WithGettext, WrappedComponent); }; } From f3214f6b259491f058e0bd55750945450e0bc8cd Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 11:43:35 +0300 Subject: [PATCH 13/25] Refactored HOF component --- src/Textdomain.js | 12 +++++++----- src/index.js | 33 +++++---------------------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/src/Textdomain.js b/src/Textdomain.js index d365da2..b296a5e 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -32,11 +32,13 @@ class Textdomain extends Component { } // if pluralForm is string and contains only "n", "0-9", " ", "=?:%+-/*><&|" - // characters, then we can eval it to calculate plural form + // characters, then we can "eval" it to calculate plural form if (typeof plural === 'string' && !plural.match(/[^n0-9 =?:%+-/*><&|]/i)) { - /* eslint-disable no-eval */ - return 0 + eval(plural.toLowerCase().split('n').join(n)); - /* eslint-enable no-eval */ + /* eslint-disable no-new-func */ + const fnc = Function('n', `return ${plural}`); + /* eslint-enable no-new-func */ + + return +fnc(n); } return 0; @@ -118,7 +120,7 @@ Textdomain.defaultProps = { children: [], }; -Textdomain.contextTypes = { +Textdomain.childContextTypes = { gettext: PropTypes.func, ngettext: PropTypes.func, xgettext: PropTypes.func, diff --git a/src/index.js b/src/index.js index f663b45..e37cf52 100644 --- a/src/index.js +++ b/src/index.js @@ -6,40 +6,17 @@ export default function withGettext(translations, pluralForm) { return (WrappedComponent) => { class WithGettext extends Textdomain { - getTranslations() { - // if translations is function, call it to get translations object, - // otherwise just use incoming translations object - return typeof translations === 'function' ? translations() : translations; - } - - getPluralForm(n) { - // return 0 if n is not integer - if (isNaN(parseInt(n, 10))) { - return 0; - } - - // if pluralForm is function, use it to get plural form index - if (typeof pluralForm === 'function') { - return pluralForm(n); - } - - // if pluralForm is string and contains only "n", "0-9", " ", "=?:%+-/*><&|" - // characters, then we can eval it to calculate plural form - if (typeof pluralForm === 'string' && !pluralForm.match(/[^n0-9 =?:%+-/*><&|]/i)) { - /* eslint-disable no-eval */ - return +eval(pluralForm.toLowerCase().split('n').join(n)); - /* eslint-enable no-eval */ - } - - return 0; - } - render() { return React.createElement(WrappedComponent, this.props); } } + WithGettext.defaultProps = { + catalog: translations, + plural: pluralForm, + }; + WithGettext.displayName = `withGettext(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; return hoistNonReactStatic(WithGettext, WrappedComponent); From bfd425544cad29e8757b2180d2de0ab5059fece3 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 11:45:17 +0300 Subject: [PATCH 14/25] Removed deprecated tests --- __tests__/__snapshots__/context.js.snap | 38 --------- __tests__/context.js | 73 ---------------- __tests__/default-export.js | 9 -- __tests__/higher-order-component.js | 59 ------------- __tests__/translations.js | 107 ------------------------ 5 files changed, 286 deletions(-) delete mode 100644 __tests__/__snapshots__/context.js.snap delete mode 100644 __tests__/context.js delete mode 100644 __tests__/default-export.js delete mode 100644 __tests__/higher-order-component.js delete mode 100644 __tests__/translations.js diff --git a/__tests__/__snapshots__/context.js.snap b/__tests__/__snapshots__/context.js.snap deleted file mode 100644 index 6d857c7..0000000 --- a/__tests__/__snapshots__/context.js.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Child component can use context functions 1`] = ` -
      -
    • -

      - gettext: -

      - - thiwwext - -
    • -
    • -

      - xgettext: -

      - - thfefeext - -
    • -
    • -

      - ngettext: -

      - - thfeferal - -
    • -
    • -

      - nxgettext -

      - - two - -
    • -
    -`; diff --git a/__tests__/context.js b/__tests__/context.js deleted file mode 100644 index 7bdf304..0000000 --- a/__tests__/context.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import faker from 'faker'; -import renderer from 'react-test-renderer'; - -import Textdomain from '../lib/index'; - -class App extends Component { - render() { - return ; - } -}; - -class C1 extends Component { - render() { - return ( -
      -
    • -

      gettext:

      - {this.context.gettext('thiwwext')} -
    • -
    • -

      xgettext:

      - {this.context.xgettext('thfefeext', 'twfwfwtext')} -
    • -
    • -

      ngettext:

      - {this.context.ngettext('fefengular', 'thfeferal', this.props.num)} -
    • -
    • -

      nxgettext

      - {this.context.nxgettext('single', 'two', 4, 'test context')} -
    • -
    - ); - } -}; - -C1.contextTypes = { - gettext: PropTypes.func.isRequired, - xgettext: PropTypes.func.isRequired, - ngettext: PropTypes.func.isRequired, - nxgettext: PropTypes.func.isRequired, -}; - -test('Child component can use context functions', () => { - const po = {}; - const sentence1 = faker.lorem.sentence(); - const sentence2 = faker.lorem.sentence(); - const sentence3 = faker.lorem.sentence(); - const sentence4 = faker.lorem.sentence(); - const sentence5 = faker.lorem.sentence(); - - po[sentence1] = '7[|UyK-%T`CKw-%j $1/6XTzdd(8+cY,/JCngNVv+wQO6NAv:+^=4GA[,+G@^GI<'; - po[`${sentence3}\u0004${sentence2}`] = 'T6%(h7m%J*O(Bm6!FiRk0 9;V]r`kPz-ROW7E8*t.Aki9}j=hU9xfU8X|.7wEuZ0'; - po[sentence4] = [ - 'ZgcaUXD7A~@-`y2%xm@t{TLsN1LvUL`6zamv|e}G|rY`ms-|aMxn#_2f{', - 'v.,rfN>-w1>j#KBX%eRO[nm@|MgGv,8E.o8-|`t.9Sz{9n#k=+V?W.#k@/tjWq|>' - ]; - - const hoc = Textdomain(po, 'n!=1')(App); - const component = renderer.create(React.createElement(hoc, { - gettext: sentence1, - xgettext: sentence2, - context: sentence3, - singular: sentence4, - plural: sentence5, - num: 2 - }, [])); - - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); diff --git a/__tests__/default-export.js b/__tests__/default-export.js deleted file mode 100644 index 6e241d9..0000000 --- a/__tests__/default-export.js +++ /dev/null @@ -1,9 +0,0 @@ -import Textdomain from '../lib/index'; - -test('Test default export', () => { - expect(typeof Textdomain).toBe('function'); -}); - -test('Test HOF returns HOC function', () => { - expect(typeof Textdomain({}, '')).toBe('function'); -}); \ No newline at end of file diff --git a/__tests__/higher-order-component.js b/__tests__/higher-order-component.js deleted file mode 100644 index 330137e..0000000 --- a/__tests__/higher-order-component.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import faker from 'faker'; - -import Textdomain from '../lib/index'; - -const getComponent = () => class extends Component { - render() { - return React.Children.only(this.props.children); - } -}; - -describe('Higher-order component', () => { - const name = faker.lorem.word(); - - const baseComponent = getComponent(); - baseComponent.displayName = name; - - const hoc = Textdomain({}, '')(baseComponent); - - test('is React Component', () => { - expect(Component.isPrototypeOf(hoc)).toBeTruthy(); - }); - - test('has proper display name', () => { - expect(hoc.displayName).toBe(`WithGettext(${name})`); - }); - - test('has childContextTypes', () => { - expect(hoc.childContextTypes).toEqual({ - gettext: PropTypes.func, - ngettext: PropTypes.func, - xgettext: PropTypes.func, - nxgettext: PropTypes.func, - }); - }); - - test('has static functions for context types', () => { - expect(typeof hoc.gettext).toBe('function'); - expect(typeof hoc.ngettext).toBe('function'); - expect(typeof hoc.xgettext).toBe('function'); - expect(typeof hoc.nxgettext).toBe('function'); - }); -}); - -describe('Higher-order component instance', () => { - const baseComponent = getComponent(); - const hoc = Textdomain({}, '')(baseComponent); - - test('returns child context', () => { - const hocObject = new hoc({}); - expect(hocObject.getChildContext()).toEqual({ - gettext: hoc.gettext, - ngettext: hoc.ngettext, - xgettext: hoc.xgettext, - nxgettext: hoc.nxgettext, - }); - }); -}); \ No newline at end of file diff --git a/__tests__/translations.js b/__tests__/translations.js deleted file mode 100644 index 97e0cf4..0000000 --- a/__tests__/translations.js +++ /dev/null @@ -1,107 +0,0 @@ -import React, { Component } from 'react'; -import faker from 'faker'; - -import Textdomain from '../lib/index'; - -const getComponent = () => class extends Component { - render() { - return null; - } -}; - -test('Default values when translations not found', () => { - const baseComponent = getComponent(); - const hoc = Textdomain({}, '')(baseComponent); - - let sentence = ''; - - sentence = faker.lorem.sentence(); - expect(hoc.gettext(sentence)).toBe(sentence); - - sentence = faker.lorem.sentence(); - expect(hoc.xgettext(sentence, 'context')).toBe(sentence); - - const sentence1 = faker.lorem.sentence(); - const sentence2 = faker.lorem.sentence(); - const number = faker.random.number({min: 1, max: 3}); - const expected = number > 1 ? sentence2 : sentence1; - expect(hoc.ngettext(sentence1, sentence2, number)).toBe(expected); -}); - -test('Test gettext function', () => { - const sentence = faker.lorem.sentence(); - const translation = faker.lorem.sentence(); - - const po = {}; - po[sentence] = translation; - - const baseComponent = getComponent(); - const hoc = Textdomain(po, '')(baseComponent); - - expect(hoc.gettext(sentence)).toBe(translation); -}); - -test('Test xgettext function', () => { - const sentence = faker.lorem.sentence(); - const context = faker.lorem.sentence(); - const translation = faker.lorem.sentence(); - - const po = {}; - po[`${context}\u0004${sentence}`] = translation; - - const baseComponent = getComponent(); - const hoc = Textdomain(po, '')(baseComponent); - - expect(hoc.xgettext(sentence, context)).toBe(translation); - expect(hoc.xgettext(sentence, faker.lorem.sentence())).not.toBe(translation); -}); - -test('Test ngettext function', () => { - const sentence = faker.lorem.sentence(); - const translation1 = faker.lorem.sentence(); - const translation2 = faker.lorem.sentence(); - const translation3 = faker.lorem.sentence(); - - const po = {}; - po[sentence] = [translation1, translation2, translation3]; - - const baseComponent = getComponent(); - const hoc = Textdomain(po, 'n % 3 == 2 ? 2 : n % 2 == 1 ? 1 : 0')(baseComponent); - - const n = faker.random.number(); - const translation = po[sentence][n % 3 == 2 ? 2 : n % 2 == 1 ? 1 : 0]; - expect(hoc.ngettext(sentence, faker.lorem.sentence(), n)).toBe(translation); -}); - -test('Function callbacks for translations and plural forms', () => { - const sentence1 = faker.lorem.sentence(); - const sentence2 = faker.lorem.sentence(); - const context = faker.lorem.sentence(); - - const sentence3 = faker.lorem.sentence(); - const translation1 = faker.lorem.sentence(); - const translation2 = faker.lorem.sentence(); - const translation3 = faker.lorem.sentence(); - - const getPO = () => { - const po = {}; - - po[sentence1] = translation1; - po[`${context}\u0004${sentence2}`] = translation2; - po[sentence3] = [translation1, translation2, translation3]; - - return po; - }; - - const pluralForm = n => n % 3 == 2 ? 2 : n % 2 == 1 ? 1 : 0; - - const baseComponent = getComponent(); - const hoc = Textdomain(getPO, pluralForm)(baseComponent); - - expect(hoc.gettext(sentence1)).toBe(translation1); - expect(hoc.xgettext(sentence2, context)).toBe(translation2); - - const n = faker.random.number(); - const translation = getPO()[sentence3][n % 3 == 2 ? 2 : n % 2 == 1 ? 1 : 0]; - expect(hoc.ngettext(sentence3, faker.lorem.sentence(), n)).toBe(translation); -}) \ No newline at end of file From dc3db4e82076da311a7cd3f4acd3d9349558e565 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 12:17:28 +0300 Subject: [PATCH 15/25] Added enzyme and basic test for higher-order-component --- __tests__/hoc.js | 43 +++++++ package.json | 6 +- yarn.lock | 300 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 __tests__/hoc.js diff --git a/__tests__/hoc.js b/__tests__/hoc.js new file mode 100644 index 0000000..0896b48 --- /dev/null +++ b/__tests__/hoc.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { mount } from 'enzyme'; +import faker from 'faker'; + +import withGettext from '../lib/index'; + +describe('Higher-order-component', () => { + class baseComponent extends Component { + render() { + return ( +
    + +
    + ); + } + }; + + const name = faker.lorem.word(); + baseComponent.displayName = name; + + const catalog = { + }; + + const Textdomain = withGettext(catalog, 'n != 1')(baseComponent); + + test('is React component', () => { + expect(Component.isPrototypeOf(Textdomain)).toBeTruthy(); + }); + + test('has proper displayName', () => { + expect(Textdomain.displayName).toBe(`withGettext(${name})`); + }); + + test('has proper childContextType', () => { + expect(Textdomain.childContextTypes).toEqual({ + gettext: PropTypes.func, + ngettext: PropTypes.func, + xgettext: PropTypes.func, + nxgettext: PropTypes.func, + }); + }); +}); diff --git a/package.json b/package.json index e0e0c20..a40f19e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "babel-preset-env": "^1.4.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", + "enzyme": "^2.9.1", "eslint": "^3.19.0", "eslint-config-airbnb": "^14.1.0", "eslint-loader": "^1.7.1", @@ -50,6 +51,7 @@ "eslint-plugin-react": "^6.10.3", "faker": "^4.1.0", "jest": "^19.0.2", + "jest-enzyme": "^3.3.0", "prop-types": "^15.5.8", "react": "^15.5.4", "react-dom": "^15.5.4", @@ -58,9 +60,7 @@ }, "peerDependencies": { "prop-types": "^15.0.0-0 || ^16.0.0-0", - "react": "^15.0.0-0 || ^16.0.0-0" - }, - "dependencies": { + "react": "^15.0.0-0 || ^16.0.0-0", "hoist-non-react-statics": "^1.2.0" } } diff --git a/yarn.lock b/yarn.lock index e5b9411..347c90c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@types/react@^15.0.22": + version "15.0.31" + resolved "https://registry.yarnpkg.com/@types/react/-/react-15.0.31.tgz#21dfc5d41ee1600ff7d7b738ad21a9502aa3ecf2" + abab@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -875,6 +879,10 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + boom@2.x.x: version "2.10.1" resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" @@ -1047,6 +1055,27 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +cheerio@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash.assignin "^4.0.9" + lodash.bind "^4.1.4" + lodash.defaults "^4.0.1" + lodash.filter "^4.4.0" + lodash.flatten "^4.2.0" + lodash.foreach "^4.3.0" + lodash.map "^4.4.0" + lodash.merge "^4.4.0" + lodash.pick "^4.2.1" + lodash.reduce "^4.4.0" + lodash.reject "^4.4.0" + lodash.some "^4.4.0" + chokidar@^1.4.3, chokidar@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" @@ -1230,6 +1259,19 @@ crypto-browserify@^3.11.0: public-encrypt "^4.0.0" randombytes "^2.0.0" +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" @@ -1276,6 +1318,12 @@ decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-equal-ident@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal-ident/-/deep-equal-ident-1.1.1.tgz#06f4b89e53710cd6cea4a7781c7a956642de8dc9" + dependencies: + lodash.isequal "^3.0" + deep-extend@~0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" @@ -1356,10 +1404,38 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +dom-serializer@0, dom-serializer@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@1.5.1, domutils@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -1405,6 +1481,43 @@ enhanced-resolve@^3.0.0: object-assign "^4.0.1" tapable "^0.2.5" +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +enzyme-matchers@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/enzyme-matchers/-/enzyme-matchers-3.3.0.tgz#cec99edfcb6117ed7d405b3d9b98d1ee3ccdbc47" + dependencies: + deep-equal-ident "^1.1.1" + +enzyme-to-json@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-1.5.1.tgz#e34f4d126bb3f4696ce3800b51f9ed83df708799" + dependencies: + lodash.filter "^4.6.0" + lodash.isnil "^4.0.0" + lodash.isplainobject "^4.0.6" + lodash.omitby "^4.5.0" + lodash.range "^3.2.0" + object-values "^1.0.0" + object.entries "^1.0.3" + +enzyme@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.9.1.tgz#07d5ce691241240fb817bf2c4b18d6e530240df6" + dependencies: + cheerio "^0.22.0" + function.prototype.name "^1.0.0" + is-subset "^0.1.1" + lodash "^4.17.4" + object-is "^1.0.1" + object.assign "^4.0.4" + object.entries "^1.0.4" + object.values "^1.0.4" + prop-types "^15.5.10" + uuid "^3.0.1" + "errno@>=0.1.1 <0.2.0-0", errno@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" @@ -1417,7 +1530,7 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.7.0: +es-abstract@^1.6.1, es-abstract@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" dependencies: @@ -1878,6 +1991,14 @@ function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +function.prototype.name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.0.tgz#5f523ca64e491a5f95aba80cc1e391080a14482e" + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.0" + is-callable "^1.1.2" + gauge@~2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09" @@ -2051,6 +2172,17 @@ html-encoding-sniffer@^1.0.1: dependencies: whatwg-encoding "^1.0.1" +htmlparser2@^3.9.1: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -2154,7 +2286,7 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-callable@^1.1.1, is-callable@^1.1.3: +is-callable@^1.1.1, is-callable@^1.1.2, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" @@ -2267,6 +2399,10 @@ is-stream@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + is-symbol@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" @@ -2439,6 +2575,14 @@ jest-environment-node@^19.0.2: jest-mock "^19.0.0" jest-util "^19.0.2" +jest-enzyme@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/jest-enzyme/-/jest-enzyme-3.3.0.tgz#c5b50682e336bf30ed5ff335e89c61a50e64bd04" + dependencies: + "@types/react" "^15.0.22" + enzyme-matchers "^3.3.0" + enzyme-to-json "^1.5.0" + jest-file-exists@^19.0.0: version "19.0.0" resolved "https://registry.yarnpkg.com/jest-file-exists/-/jest-file-exists-19.0.0.tgz#cca2e587a11ec92e24cfeab3f8a94d657f3fceb8" @@ -2738,11 +2882,118 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash._baseisequal@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1" + dependencies: + lodash.isarray "^3.0.0" + lodash.istypedarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash.assignin@^4.0.9: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" + +lodash.bind@^4.1.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" -lodash@^4.0.0, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.3.0: +lodash.defaults@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.filter@^4.4.0, lodash.filter@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" + +lodash.flatten@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + +lodash.foreach@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isequal@^3.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-3.0.4.tgz#1c35eb3b6ef0cd1ff51743e3ea3cf7fdffdacb64" + dependencies: + lodash._baseisequal "^3.0.0" + lodash._bindcallback "^3.0.0" + +lodash.isnil@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.istypedarray@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.map@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + +lodash.merge@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" + +lodash.omitby@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.omitby/-/lodash.omitby-4.6.0.tgz#5c15ff4754ad555016b53c041311e8f079204791" + +lodash.pick@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + +lodash.range@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.range/-/lodash.range-3.2.0.tgz#f461e588f66683f7eadeade513e38a69a565a15d" + +lodash.reduce@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" + +lodash.reject@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" + +lodash.some@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" + +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2750,7 +3001,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -2949,6 +3200,12 @@ npmlog@^4.0.2: gauge "~2.7.1" set-blocking "~2.0.0" +nth-check@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" + dependencies: + boolbase "~1.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -2969,10 +3226,18 @@ object-hash@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.1.8.tgz#28a659cf987d96a4dabe7860289f3b5326c4a03c" +object-is@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" + object-keys@^1.0.10, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object-values/-/object-values-1.0.0.tgz#72af839630119e5b98c3b02bb8c27e3237158105" + object.assign@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" @@ -2981,6 +3246,15 @@ object.assign@^4.0.4: function-bind "^1.1.0" object-keys "^1.0.10" +object.entries@^1.0.3, object.entries@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.6.1" + function-bind "^1.1.0" + has "^1.0.1" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -2988,6 +3262,15 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" +object.values@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.6.1" + function-bind "^1.1.0" + has "^1.0.1" + once@^1.3.0, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3202,6 +3485,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.5.10: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: version "15.5.8" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" @@ -3868,7 +4158,7 @@ util@0.10.3, util@^0.10.3: dependencies: inherits "2.0.1" -uuid@^3.0.0: +uuid@^3.0.0, uuid@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" From ad178ba2fab5657581ea3fd5a6068cf89a92fed3 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 12:46:57 +0300 Subject: [PATCH 16/25] Fixed plural form calculation and added a test for it --- __tests__/hoc.js | 10 +++++++++- src/Textdomain.js | 5 ++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/__tests__/hoc.js b/__tests__/hoc.js index 0896b48..19e7bbd 100644 --- a/__tests__/hoc.js +++ b/__tests__/hoc.js @@ -19,10 +19,11 @@ describe('Higher-order-component', () => { const name = faker.lorem.word(); baseComponent.displayName = name; + const plural = 'n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2'; const catalog = { }; - const Textdomain = withGettext(catalog, 'n != 1')(baseComponent); + const Textdomain = withGettext(catalog, plural)(baseComponent); test('is React component', () => { expect(Component.isPrototypeOf(Textdomain)).toBeTruthy(); @@ -40,4 +41,11 @@ describe('Higher-order-component', () => { nxgettext: PropTypes.func, }); }); + + test('properly calculates plural form', () => { + const instance = new Textdomain({plural}); + expect(instance.getPluralForm(1)).toBe(0); + expect(instance.getPluralForm(2)).toBe(1); + expect(instance.getPluralForm(5)).toBe(2); + }); }); diff --git a/src/Textdomain.js b/src/Textdomain.js index b296a5e..6aea207 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -31,13 +31,12 @@ class Textdomain extends Component { return plural(n); } - // if pluralForm is string and contains only "n", "0-9", " ", "=?:%+-/*><&|" + // if pluralForm is string and contains only "n", "0-9", " ", "!=?:%+-/*><&|()" // characters, then we can "eval" it to calculate plural form - if (typeof plural === 'string' && !plural.match(/[^n0-9 =?:%+-/*><&|]/i)) { + if (typeof plural === 'string' && !plural.match(/[^n0-9 !=?:%+-/*><&|()]/i)) { /* eslint-disable no-new-func */ const fnc = Function('n', `return ${plural}`); /* eslint-enable no-new-func */ - return +fnc(n); } From 7f8c49dfe7890bd179e762b2a0b0e7e70b1faf0f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Sun, 25 Jun 2017 13:52:37 +0300 Subject: [PATCH 17/25] Added tests for translations --- __tests__/context.js | 149 +++++++++++++++++++++++++++++++++++++++++++ __tests__/hoc.js | 14 +--- src/Textdomain.js | 17 +++-- 3 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 __tests__/context.js diff --git a/__tests__/context.js b/__tests__/context.js new file mode 100644 index 0000000..a34deba --- /dev/null +++ b/__tests__/context.js @@ -0,0 +1,149 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { mount } from 'enzyme'; +import faker from 'faker'; + +import withGettext from '../lib/index'; + +describe('Higher-order-component translates', () => { + const pluralForm = 'n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2'; + const catalog = {}; + + const gettext1 = faker.lorem.sentence(); + const gettext2 = faker.lorem.sentence(); + catalog[gettext1] = faker.lorem.sentence(); + + const single1 = faker.lorem.sentence(); + const plural1 = faker.lorem.sentence(); + catalog[single1] = [ + faker.lorem.sentence(), + faker.lorem.sentence(), + faker.lorem.sentence(), + ]; + + const single2 = faker.lorem.sentence(); + const plural2 = faker.lorem.sentence(); + + const context1 = faker.lorem.sentence(); + const gettext3 = faker.lorem.sentence(); + const contextKey1 = `${context1}\u0004${gettext3}`; + catalog[contextKey1] = faker.lorem.sentence(); + + const context2 = faker.lorem.sentence(); + const gettext4 = faker.lorem.sentence(); + + const single3 = faker.lorem.sentence(); + const plural3 = faker.lorem.sentence(); + const context3 = faker.lorem.sentence(); + const contextKey2 = `${context3}\u0004${single3}`; + catalog[contextKey2] = [ + faker.lorem.sentence(), + faker.lorem.sentence(), + faker.lorem.sentence(), + ]; + + const single4 = faker.lorem.sentence(); + const plural4 = faker.lorem.sentence(); + const context4 = faker.lorem.sentence(); + + class baseComponent extends Component { + render() { + const { type, num } = this.props; + + let message = ''; + switch (type) { + case 'gettext1': + message = this.context.gettext(gettext1); + break; + case 'gettext2': + message = this.context.gettext(gettext2); + break; + case 'ngettext1': + message = this.context.ngettext(single1, plural1, num); + break; + case 'ngettext2': + message = this.context.ngettext(single2, plural2, num); + break; + case 'xgettext1': + message = this.context.xgettext(gettext3, context1); + break; + case 'xgettext2': + message = this.context.xgettext(gettext4, context2); + break; + case 'nxgettext1': + message = this.context.nxgettext(single3, plural3, num, context3); + break; + case 'nxgettext2': + message = this.context.nxgettext(single4, plural4, num, context4); + break; + } + + return
    {message}
    ; + } + }; + + baseComponent.contextTypes = { + gettext: PropTypes.func.isRequired, + xgettext: PropTypes.func.isRequired, + ngettext: PropTypes.func.isRequired, + nxgettext: PropTypes.func.isRequired, + }; + + const Textdomain = withGettext(catalog, pluralForm)(baseComponent); + + test('gettext', () => { + let wrapper; + + // check when translation exists + wrapper = mount(); + expect(wrapper.text()).toBe(catalog[gettext1]); + + // check when translation exists + wrapper = mount(); + expect(wrapper.text()).toBe(gettext2); + }); + + test('ngettext', () => { + let wrapper; + + // check when translation exists + for (let i = 0; i < 3; i++) { + wrapper = mount(); + expect(wrapper.text()).toBe(catalog[single1][i]); + } + + // check fallbacks when a translation doesn't exist + for (let i = 0; i < 2; i++) { + wrapper = mount(); + expect(wrapper.text()).toBe(i == 0 ? single2 : plural2); + } + }); + + test('xgettext', () => { + let wrapper; + + // check when translation exists + wrapper = mount(); + expect(wrapper.text()).toBe(catalog[contextKey1]); + + // check when translation exists + wrapper = mount(); + expect(wrapper.text()).toBe(gettext4); + }); + + test('ngettext', () => { + let wrapper; + + // check when translation exists + for (let i = 0; i < 3; i++) { + wrapper = mount(); + expect(wrapper.text()).toBe(catalog[contextKey2][i]); + } + + // check fallbacks when a translation doesn't exist + for (let i = 0; i < 2; i++) { + wrapper = mount(); + expect(wrapper.text()).toBe(i == 0 ? single4 : plural4); + } + }); +}); diff --git a/__tests__/hoc.js b/__tests__/hoc.js index 19e7bbd..807eeb6 100644 --- a/__tests__/hoc.js +++ b/__tests__/hoc.js @@ -1,6 +1,5 @@ -import React, { Component } from 'react'; +import { Component } from 'react'; import PropTypes from 'prop-types'; -import { mount } from 'enzyme'; import faker from 'faker'; import withGettext from '../lib/index'; @@ -8,11 +7,7 @@ import withGettext from '../lib/index'; describe('Higher-order-component', () => { class baseComponent extends Component { render() { - return ( -
    - -
    - ); + return this.props.children; } }; @@ -20,10 +15,7 @@ describe('Higher-order-component', () => { baseComponent.displayName = name; const plural = 'n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2'; - const catalog = { - }; - - const Textdomain = withGettext(catalog, plural)(baseComponent); + const Textdomain = withGettext({}, plural)(baseComponent); test('is React component', () => { expect(Component.isPrototypeOf(Textdomain)).toBeTruthy(); diff --git a/src/Textdomain.js b/src/Textdomain.js index 6aea207..817442d 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -6,10 +6,10 @@ class Textdomain extends Component { getChildContext() { const self = this; return { - gettext: self.gettext, - xgettext: self.xgettext, - ngettext: self.ngettext, - nxgettext: self.nxgettext, + gettext: self.gettext.bind(this), + xgettext: self.xgettext.bind(this), + ngettext: self.ngettext.bind(this), + nxgettext: self.nxgettext.bind(this), }; } @@ -62,7 +62,7 @@ class Textdomain extends Component { return Object.prototype.hasOwnProperty.call(messages, singular) && Array.isArray(messages[singular]) - && messages[singular].length >= pluralIndex + && messages[singular].length > pluralIndex && pluralIndex >= 0 ? messages[singular][pluralIndex] : defaultValue; @@ -89,7 +89,7 @@ class Textdomain extends Component { return Object.prototype.hasOwnProperty.call(messages, key) && Array.isArray(messages[key]) - && messages[key].length >= pluralIndex + && messages[key].length > pluralIndex && pluralIndex >= 0 ? messages[key][pluralIndex] : defaultValue; @@ -104,7 +104,10 @@ class Textdomain extends Component { Textdomain.propTypes = { catalog: PropTypes.oneOfType([ PropTypes.func, - PropTypes.objectOf(PropTypes.string), + PropTypes.objectOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + ])), ]), plural: PropTypes.oneOfType([ PropTypes.func, From 1d0e3ca99ff8396ae4c1f23d019cc89a044f0d16 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 26 Jun 2017 17:27:58 +0300 Subject: [PATCH 18/25] Replaced this with self to reduce file size --- src/Textdomain.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Textdomain.js b/src/Textdomain.js index 817442d..d31d2a4 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -5,11 +5,12 @@ class Textdomain extends Component { getChildContext() { const self = this; + return { - gettext: self.gettext.bind(this), - xgettext: self.xgettext.bind(this), - ngettext: self.ngettext.bind(this), - nxgettext: self.nxgettext.bind(this), + gettext: self.gettext.bind(self), + xgettext: self.xgettext.bind(self), + ngettext: self.ngettext.bind(self), + nxgettext: self.nxgettext.bind(self), }; } @@ -35,9 +36,9 @@ class Textdomain extends Component { // characters, then we can "eval" it to calculate plural form if (typeof plural === 'string' && !plural.match(/[^n0-9 !=?:%+-/*><&|()]/i)) { /* eslint-disable no-new-func */ - const fnc = Function('n', `return ${plural}`); + const calcPlural = Function('n', `return ${plural}`); /* eslint-enable no-new-func */ - return +fnc(n); + return +calcPlural(n); } return 0; From 5dd8b3bd71c10a2c577aeb2757157e066f6e84f2 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 26 Jun 2017 17:32:43 +0300 Subject: [PATCH 19/25] Added "hoist-non-react-statics" package to dev dependencies --- package.json | 5 +++-- yarn.lock | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a40f19e..f6c3656 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-react": "^6.10.3", "faker": "^4.1.0", + "hoist-non-react-statics": "^1.2.0", "jest": "^19.0.2", "jest-enzyme": "^3.3.0", "prop-types": "^15.5.8", @@ -59,8 +60,8 @@ "webpack": "^2.4.1" }, "peerDependencies": { + "hoist-non-react-statics": "^1.2.0", "prop-types": "^15.0.0-0 || ^16.0.0-0", - "react": "^15.0.0-0 || ^16.0.0-0", - "hoist-non-react-statics": "^1.2.0" + "react": "^15.0.0-0 || ^16.0.0-0" } } diff --git a/yarn.lock b/yarn.lock index 347c90c..2208739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3485,14 +3485,14 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10: +prop-types@^15.5.10, prop-types@^15.5.7, prop-types@~15.5.7: version "15.5.10" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" dependencies: fbjs "^0.8.9" loose-envify "^1.3.1" -prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7: +prop-types@^15.5.8: version "15.5.8" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" dependencies: From f237936d31663da5b42c40200d007162137c8cc2 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 26 Jun 2017 17:58:49 +0300 Subject: [PATCH 20/25] Added changelog file --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f9de2f4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +## v0.3.0 (2017-06-26) + +**Implemented enhancements:** + +- Added nxgettext function which allows to translate plural strings with context. +- Extracted HOC into a separate component and updated it to inherit from that standalone component. \ No newline at end of file From f0f816c7011f203fea5da49da6e47c95033c752c Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 26 Jun 2017 18:00:58 +0300 Subject: [PATCH 21/25] Added nxgettext function to readme file --- README.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa28b86..384b614 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ Tiny React library for implementing gettext localization in your application. It ## Instalation -React Gettext requires **React 15.0 or later**. +React Gettext requires **React 15.0 or later**. You can add this package using following commands ``` npm install react-gettext --save +// or +yarn add react-gettext ``` ## Usage @@ -74,7 +76,8 @@ After doing it you can start using `gettext`, `ngettext` and `xgettext` function ```diff // Header.js - import React, { Component } from 'react'; -+ import React, { Component, PropTypes } from 'react'; ++ import React, { Component } from 'react'; ++ import PropTypes from 'prop-types'; export default class Header extends Component { @@ -90,7 +93,8 @@ After doing it you can start using `gettext`, `ngettext` and `xgettext` function + Header.contextTypes = { + gettext: PropTypes.func.isRequired, + ngettext: PropTypes.func.isRequired, -+ xgettext: PropTypes.func.isRequired ++ xgettext: PropTypes.func.isRequired, ++ nxgettext: PropTypes.func.isRequired, + }; ``` @@ -173,6 +177,22 @@ Example: this.context.xgettext('some text', 'context where this message is used'); ``` +### nxgettext(singular, plural, n, context) + +The function to translate plural string based on a specific context. Accepts singular and plural messages along with a number to calculate plural form against and context string. Returns translated message based on plural form if it exists, otherwise original message based on **n** value. + +- **singular**: a string to be translated when count is not plural +- **plural**: a string to be translated when count is plural +- **n**: a number to count plural form +- **context**: A context to search translation in. + +Example: + +```javascript +// somewhere in your jsx component +this.context.nxgettext('day ago', 'days ago', numberOfDays, 'Article publish date'); +``` + ## Poedit If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c` as keywords list to properly parse and extract strings from your javascript files. From cd8689f514a8168c5ba9ada6c299c52f03ade3b2 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 26 Jun 2017 18:07:43 +0300 Subject: [PATCH 22/25] Added an example of pot file --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 384b614..3a7e13d 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,24 @@ this.context.nxgettext('day ago', 'days ago', numberOfDays, 'Article publish dat ## Poedit -If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c` as keywords list to properly parse and extract strings from your javascript files. +If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c` as keywords list to properly parse and extract strings from your javascript files. Here is an example of a **POT** file which you can start with: + +``` +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=iso-8859-1\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Basepath: ./src\n" +"X-Poedit-KeywordsList: gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c\n" +"X-Poedit-SourceCharset: UTF-8\n" +``` ## Contribute From 3de4c6c2169f26a8c7bfe92e637e7777567b6040 Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Mon, 26 Jun 2017 18:10:48 +0300 Subject: [PATCH 23/25] Minor updates in readme fiel --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3a7e13d..641127a 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,13 @@ Tiny React library for implementing gettext localization in your application. It ## Instalation -React Gettext requires **React 15.0 or later**. You can add this package using following commands +React Gettext requires **React 15.0 or later**. You can add this package using following commands: ``` npm install react-gettext --save -// or +``` + +``` yarn add react-gettext ``` @@ -71,7 +73,7 @@ To make it translatable you need to update your `app.js` file to use HOC functio + export default Textdomain({...}, 'n != 1')(App); ``` -After doing it you can start using `gettext`, `ngettext` and `xgettext` functions in your descending components: +After doing it you can start using `gettext`, `ngettext`, `xgettext` and `nxgettext` functions in your descending components: ```diff // Header.js @@ -195,7 +197,9 @@ this.context.nxgettext('day ago', 'days ago', numberOfDays, 'Article publish dat ## Poedit -If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c` as keywords list to properly parse and extract strings from your javascript files. Here is an example of a **POT** file which you can start with: +If you use Poedit app to translate your messages, then you can use `gettext;ngettext:1,2;xgettext:1,2c;nxgettext:1,2,4c` as keywords list to properly parse and extract strings from your javascript files. + +Here is an example of a **POT** file which you can start with: ``` msgid "" From cc6cfc8905e42620df9152d70328e761d4c32fcf Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 27 Jun 2017 12:34:32 +0300 Subject: [PATCH 24/25] Updated readme file - replaced Textdomain with withGettext --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 641127a..71c46f9 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ To make it translatable you need to update your `app.js` file to use HOC functio ```diff // app.js import React, { Component } from 'react'; -+ import Textdomain from 'react-gettext'; ++ import withGettext from 'react-gettext'; import Header from './Header'; import Footer from './Footer'; @@ -70,7 +70,7 @@ To make it translatable you need to update your `app.js` file to use HOC functio ... } -+ export default Textdomain({...}, 'n != 1')(App); ++ export default withGettext({...}, 'n != 1')(App); ``` After doing it you can start using `gettext`, `ngettext`, `xgettext` and `nxgettext` functions in your descending components: @@ -102,7 +102,7 @@ After doing it you can start using `gettext`, `ngettext`, `xgettext` and `nxgett ## Documentation -### Textdomain(translations, pluralForms) +### withGettext(translations, pluralForms) Higher-order function which is exported by default from `react-gettext` package. It accepts two arguments and returns function to create higher-order component. @@ -119,7 +119,7 @@ const translations = { const pluralForms = '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'; // 3 plural forms for Russian, Belarusian, Bosnian, Croatian, Serbian, Ukrainian, etc. -const HOC = Textdomain(translations, pluralForms)(App); +const HOC = withGettext(translations, pluralForms)(App); ``` ```javascript @@ -134,7 +134,7 @@ function getPluralForms(n) { return n > 1 ? 1 : 0; } -const HOC = Textdomain(getTranslations, getPluralForms)(App); +const HOC = withGettext(getTranslations, getPluralForms)(App); ``` ### gettext(message) From 72452a4e4cb4343be52e0144a0d78d116257fa7f Mon Sep 17 00:00:00 2001 From: Eugene Manuilov Date: Tue, 27 Jun 2017 12:45:40 +0300 Subject: [PATCH 25/25] Updated documentation and some naming --- README.md | 22 ++++++++++++++++++++++ src/Textdomain.js | 8 ++++---- src/index.js | 4 ++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 71c46f9..3a80524 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,28 @@ function getPluralForms(n) { const HOC = withGettext(getTranslations, getPluralForms)(App); ``` +As an alternative you can pass translations and plural form as properties to higher-order-component, like this: + +```javascript +function getTranslations() { + return { + 'Some text': 'Some translated text', + ... + }; +} + +function getPluralForms(n) { + return n > 1 ? 1 : 0; +} + +const HOC = withGettext()(App); + +... + +ReactDOM.render(..., ...); +``` + + ### gettext(message) The function to translate a string. Accepts original message and returns translation if it exists, otherwise original message. diff --git a/src/Textdomain.js b/src/Textdomain.js index d31d2a4..c6c51c3 100644 --- a/src/Textdomain.js +++ b/src/Textdomain.js @@ -15,8 +15,8 @@ class Textdomain extends Component { } getTranslations() { - const { catalog } = this.props; - return typeof catalog === 'function' ? catalog() : catalog; + const { translations } = this.props; + return typeof translations === 'function' ? translations() : translations; } getPluralForm(n) { @@ -103,7 +103,7 @@ class Textdomain extends Component { } Textdomain.propTypes = { - catalog: PropTypes.oneOfType([ + translations: PropTypes.oneOfType([ PropTypes.func, PropTypes.objectOf(PropTypes.oneOfType([ PropTypes.string, @@ -118,7 +118,7 @@ Textdomain.propTypes = { }; Textdomain.defaultProps = { - catalog: {}, + translations: {}, plural: 'n != 1', children: [], }; diff --git a/src/index.js b/src/index.js index e37cf52..22eff52 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import React from 'react'; import hoistNonReactStatic from 'hoist-non-react-statics'; import Textdomain from './Textdomain'; -export default function withGettext(translations, pluralForm) { +export default function withGettext(translations = {}, pluralForm = 'n != 1') { return (WrappedComponent) => { class WithGettext extends Textdomain { @@ -13,7 +13,7 @@ export default function withGettext(translations, pluralForm) { } WithGettext.defaultProps = { - catalog: translations, + translations, plural: pluralForm, };