Skip to content

Commit

Permalink
testing framework, split Translate into two components
Browse files Browse the repository at this point in the history
  • Loading branch information
jenseng committed Feb 15, 2015
1 parent 395d824 commit d2d92d2
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 53 deletions.
1 change: 1 addition & 0 deletions .jshintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
3 changes: 3 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"esnext": true
}
99 changes: 99 additions & 0 deletions ComponentInterpolator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Given:
<Translate>Ohai {this.props.user}, click <Link>here</Link> right <b>now <i>please</i></b>!</Translate>
Pre-process it into:
<ComponentInterpolator string={I18n.t("Ohai, %{user}, click *here* right ***now **please** ***", {user: this.props.user})">
<Link />
<b><i /></b>
</ComponentInterpolator>
*/

var React = require('react');
var cloneWithProps = require('react/lib/cloneWithProps');
var invariant = require('react/lib/invariant');
var { string } = React.PropTypes;

var OWN_PROPS = ['defaultValue', 'translateKey', 'children'];

var ComponentInterpolator = React.createClass({
propTypes: {
string: string.isRequired
},

componentWillMount() {
var textCount = this.textCount();
var componentCount = this.componentCount();
invariant(
textCount === 0,
'<ComponentInterpolator> cannot have any text children'
);
},

textCount(node) {
node = node || this;
count = 0;
React.Children.forEach(node.props.children, function(child) {
count += typeof child === 'string' ? 1 : this.textCount(child);
});
return count;
},

componentCount(node) {
node = node || this;
count = 0;
React.Children.forEach(node.props.children, function(child) {
count += typeof child === 'string' ? 0 : 1 + this.componentCount(child);
});
return count;
},

inferChildren(string, children) {
var tokens = (string || '').split(/(\*+)/);
return this.interpolateChildren(tokens, children);
},

interpolateChildren(tokens, children, eof) {
var token, child, newChildren = [];
while (tokens.length) {
token = tokens.shift();
if (token === eof) break;
if (token.match(/\*/)) {
child = children.shift();
child = cloneWithProps(child, {
key: child.key,
children: this.interpolateChildren(tokens, child.children, token)
});
}
else {
child = token;
}
newChildren.push(child);
}
return newChildren;
},

extraProps() {
var props = {};
for (var key in this.props) {
if (OWN_PROPS.indexOf(key) === -1)
props[key] = this.props[key];
}
return props;
},

render() {
var options = this.extraProps();
var translateKey = this.props.translateKey;
var defaultValue = this.props.defaultValue || this.props.children;
options.defaultValue = defaultValue;

var children = this.inferChildren(this.props.string, this.props.children);
return React.createElement('span', {}, children);
}
});

module.exports = ComponentInterpolator;

59 changes: 8 additions & 51 deletions Translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ Given:
Pre-process it into:
<Translate translateKey="..." defaultValue="Ohai, %{user}, click *here* right ***now **please** ***" user={this.props.user}>
<ComponentInterpolator string={I18n.t("Ohai, %{user}, click *here* right ***now **please** ***", {user: this.props.user})">
<Link />
<b><i /></b>
</Translate>
</ComponentInterpolator>
*/

var React = require('react');
var cloneWithProps = require('react/lib/cloneWithProps');
var I18n = require('i18n');
var invariant = require('react/lib/invariant');
var { string } = React.PropTypes;

OWN_PROPS = ['defaultValue', 'translateKey', 'children'];
var OWN_PROPS = ['defaultValue', 'translateKey', 'children'];

var Translate = React.createClass({
propTypes: {
Expand All @@ -29,14 +29,14 @@ var Translate = React.createClass({
var componentCount = this.componentCount();
invariant(
textCount <= 1,
'<Translate> can only have a one text child when not using pre-processing'
'<Translate> can only have one text child when not using pre-processing'
);
invariant(
componentCount === 0 || textCount === 0,
'<Translate> cannot have both text and component children when not using pre-processing'
componentCount === 0,
'<Translate> cannot have any component children when not using pre-processing'
);
invariant(
textCount > 0 || this.props.defaultValue || this.props.translateKey,
textCount === 1 || this.props.defaultValue || this.props.translateKey,
'<Translate> needs at least a translateKey, defaultValue, or text child'
);
},
Expand All @@ -50,49 +50,6 @@ var Translate = React.createClass({
return count;
},

componentCount() {
node = node || this;
count = 0;
React.Children.forEach(node.props.children, function(child) {
count += typeof child === 'string' ? 0 : 1 + this.componentCount(child);
});
return count;
},

inferChildren(string, children) {
var tokens = string.split(/(\*+)/);
return this.interpolateChildren(tokens, children, result);
},

interpolateChildren(tokens, children, eof) {
var token, child, newChildren = [];
while (tokens.length) {
token = tokens.shift();
break if token === eof;
if (token.match(/\*/)) {
child = children.shift();
child = cloneWithProps(child, {
key: child.key,
children: this.interpolateChildren(tokens, child.children, token)
});
}
else {
child = token;
}
newChildren.push(child);
}
return newChildren;
},

extraProps() {
var props = {};
for (var key in this.props) {
if (OWN_PROPS.indexOf(key) === -1)
props[key] = this.props[key];
}
return props;
},

render() {
var options = this.extraProps();
var translateKey = this.props.translateKey;
Expand Down
15 changes: 15 additions & 0 deletions __tests__/ComponentInterpolator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var subjector = require('../test_utils/subjector');
var Subject = subjector(__dirname + '/../ComponentInterpolator');

describe('ComponentInterpolator', function() {
it('renders', function() {
var subject = Subject({string: 'Hello World'});
expect(subject.isMounted()).toEqual(true);
expect(subject.getDOMNode().textContent).toEqual('Hello World');
});

it('escapes html in the string', function() {
var subject = Subject({string: 'My favorite tag is <script />'});
expect(subject.getDOMNode().textContent).toEqual('My favorite tag is <script />');
});
});
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@
"description": "i18n made simple",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node_modules/.bin/jsxhint . && node_modules/.bin/jest"
},
"author": "Jon Jensen",
"license": "ISC",
"devDependencies": {
"react": "^0.12.2"
"jest-cli": "^0.2.2",
"jsxhint": "^0.10.0",
"react": "0.12.x",
"react-tools": "^0.12.2"
},
"peerDependencies": {
"react": "0.12.x"
},
"jest": {
"scriptPreprocessor": "test_utils/preprocessor.js",
"setupTestFrameworkScriptFile": "test_utils/jasmine.js",
"unmockedModulePathPatterns": [
"node_modules",
"test_utils"
]
}
}
1 change: 1 addition & 0 deletions test_utils/jasmine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
jasmine.VERBOSE = true;
6 changes: 6 additions & 0 deletions test_utils/preprocessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var ReactTools = require('react-tools');
module.exports = {
process: function(src) {
return ReactTools.transform(src, {harmony: true});
}
};
14 changes: 14 additions & 0 deletions test_utils/subjector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = function(path) {
jest.dontMock(path);

var React = require('react/addons');
var { addons: { TestUtils } } = React;
var Component = require(path);

return function(props) {
return TestUtils.renderIntoDocument(
React.createElement(Component, props)
);
};
};

0 comments on commit d2d92d2

Please sign in to comment.