Skip to content
This repository was archived by the owner on Jun 8, 2019. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ If a message descriptor has a `description`, it'll be removed from the source af

- **`moduleSourceName`**: The ES6 module source name of the React Intl package. Defaults to: `"react-intl"`, but can be changed to another name/path to React Intl.

- **`fields`**: For specifying additional metadata fields and whether they're required. Takes an object with field names as key names, which specify whether they're required. `fields: { metadata: { required: true }, otherdata: {required: false} }`

### Via CLI

```sh
Expand Down
3 changes: 3 additions & 0 deletions scripts/build-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const fixtures = [
['moduleSourceName', {
moduleSourceName: 'react-i18n',
}],
['extractFieldConfig', {
fields: { metadata: { required: true}}
}],
];

fixtures.forEach((fixture) => {
Expand Down
42 changes: 27 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const FUNCTION_NAMES = [
'defineMessages',
];

const DESCRIPTOR_PROPS = new Set(['id', 'description', 'defaultMessage']);
const DESCRIPTOR_PROPS = {
id: {required: true},
description: {required:false},
defaultMessage: {required:true},
};

const EXTRACTED_TAG = Symbol('ReactIntlExtracted');

Expand Down Expand Up @@ -82,11 +86,11 @@ export default function ({types: t}) {
}
}

function createMessageDescriptor(propPaths) {
function createMessageDescriptor(propPaths, fields) {
return propPaths.reduce((hash, [keyPath, valuePath]) => {
let key = getMessageDescriptorKey(keyPath);

if (DESCRIPTOR_PROPS.has(key)) {
if (Object.keys(fields).indexOf(key) > -1) {
hash[key] = valuePath;
}

Expand All @@ -108,12 +112,20 @@ export default function ({types: t}) {
return descriptor;
}

function storeMessage({id, description, defaultMessage}, path, state) {
function storeMessage(messageDescriptor, path, state) {
const {id, description, defaultMessage} = messageDescriptor;
const {file, opts, reactIntl} = state;

if (!(id && defaultMessage)) {
const missing_required = Object.keys(opts.fields)
.filter( (key) => opts.fields[key].required)
.reduce( (arr,key) => {
if(!messageDescriptor[key]){ arr.push( key ); }
return arr;
}, []);

if(missing_required.length){
throw path.buildCodeFrameError(
'[React Intl] Message Descriptors require an `id` and `defaultMessage`.'
'[React Intl] Message must have the following fields:' + missing_required.join(', ')
);
}

Expand All @@ -130,12 +142,6 @@ export default function ({types: t}) {
}
}

if (opts.enforceDescriptions && !description) {
throw path.buildCodeFrameError(
'[React Intl] Message must have a `description`.'
);
}

let loc;
if (opts.extractSourceLocation) {
loc = {
Expand All @@ -144,7 +150,7 @@ export default function ({types: t}) {
};
}

reactIntl.messages.set(id, {id, description, defaultMessage, ...loc});
reactIntl.messages.set(id, {...messageDescriptor, ...loc});
}

function referencesImport(path, mod, importedNames) {
Expand All @@ -167,6 +173,10 @@ export default function ({types: t}) {
visitor: {
Program: {
enter(path, state) {
state.opts.fields = state.opts.fields || {};
const fields = state.opts.fields;
Object.assign(fields,DESCRIPTOR_PROPS);
fields.description.required = !!state.opts.enforceDescriptions;
state.reactIntl = {
messages: new Map(),
};
Expand Down Expand Up @@ -228,7 +238,8 @@ export default function ({types: t}) {
attributes.map((attr) => [
attr.get('name'),
attr.get('value'),
])
]),
opts.fields
);

// In order for a default message to be extracted when
Expand Down Expand Up @@ -290,7 +301,8 @@ export default function ({types: t}) {
properties.map((prop) => [
prop.get('key'),
prop.get('value'),
])
]),
state.opts.fields
);

// Evaluate the Message Descriptor values, then store it.
Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/extractFieldConfig/actual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, {Component} from 'react';
import {FormattedMessage} from 'react-intl';

export default class Foo extends Component {
render() {
return (
<FormattedMessage
id='foo.bar.baz'
metadata='metadata content'
defaultMessage='Hello World!'
/>
);
}
}
46 changes: 46 additions & 0 deletions test/fixtures/extractFieldConfig/expected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _reactIntl = require('react-intl');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Foo = function (_Component) {
_inherits(Foo, _Component);

function Foo() {
_classCallCheck(this, Foo);

return _possibleConstructorReturn(this, (Foo.__proto__ || Object.getPrototypeOf(Foo)).apply(this, arguments));
}

_createClass(Foo, [{
key: 'render',
value: function render() {
return _react2.default.createElement(_reactIntl.FormattedMessage, {
id: 'foo.bar.baz',
metadata: 'metadata content',
defaultMessage: 'Hello World!'
});
}
}]);

return Foo;
}(_react.Component);

exports.default = Foo;
7 changes: 7 additions & 0 deletions test/fixtures/extractFieldConfig/expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"id": "foo.bar.baz",
"metadata": "metadata content",
"defaultMessage": "Hello World!"
}
]
56 changes: 46 additions & 10 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const skipTests = [
'moduleSourceName',
'icuSyntax',
'removeDescriptions',
'extractFieldConfig',
];

const fixturesDir = path.join(__dirname, 'fixtures');
Expand All @@ -39,9 +40,9 @@ describe('emit asserts for: ', () => {
assert.equal(trim(actual), trim(expected));

// Check message output
const expectedMessages = fs.readFileSync(path.join(fixtureDir, 'expected.json'));
const actualMessages = fs.readFileSync(path.join(fixtureDir, 'actual.json'));
assert.equal(trim(actualMessages), trim(expectedMessages));
const expectedMessages = require(path.join(fixtureDir, 'expected.json'));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed this test to rely on deepEqual instead of text string content. different object key order was breaking tests

const actualMessages = require(path.join(fixtureDir, 'actual.json'));
assert.deepEqual(actualMessages, expectedMessages);
});
});
});
Expand All @@ -57,7 +58,8 @@ describe('options', () => {
assert(false);
} catch (e) {
assert(e);
assert(/Message must have a `description`/.test(e.message));
assert(/Message must have the following fields/.test(e.message));
assert(/description/.test(e.message));
}
});

Expand Down Expand Up @@ -105,9 +107,9 @@ describe('options', () => {
}

// Check message output
const expectedMessages = fs.readFileSync(path.join(fixtureDir, 'expected.json'));
const actualMessages = fs.readFileSync(path.join(fixtureDir, 'actual.json'));
assert.equal(trim(actualMessages), trim(expectedMessages));
const expectedMessages = require(path.join(fixtureDir, 'expected.json'));
const actualMessages = require(path.join(fixtureDir, 'actual.json'));
assert.deepEqual(actualMessages, expectedMessages);
});

it('respects extractSourceLocation', () => {
Expand All @@ -124,9 +126,43 @@ describe('options', () => {
}

// Check message output
const expectedMessages = fs.readFileSync(path.join(fixtureDir, 'expected.json'));
const actualMessages = fs.readFileSync(path.join(fixtureDir, 'actual.json'));
assert.equal(trim(actualMessages), trim(expectedMessages));
const expectedMessages = require(path.join(fixtureDir, 'expected.json'));
const actualMessages = require(path.join(fixtureDir, 'actual.json'));
assert.deepEqual(actualMessages, expectedMessages);
});

it('respects field config, extracts data', () => {
const fixtureDir = path.join(fixturesDir, 'extractFieldConfig');

try {
transform(path.join(fixtureDir, 'actual.js'), {
fields: { metadata: { required: true}},
});
assert(true);
} catch (e) {
console.error(e);
assert(false);
}

// Check message output
const expectedMessages = require(path.join(fixtureDir, 'expected.json'));
const actualMessages = require(path.join(fixtureDir, 'actual.json'));
assert.deepEqual(actualMessages, expectedMessages);
});

it('enforces field config required setting', () => {
const fixtureDir = path.join(fixturesDir, 'extractSourceLocation');

try {
transform(path.join(fixtureDir, 'actual.js'), {
fields: { metadata: { required: true}},
});
assert(false);
} catch (e) {
assert(e);
assert(/Message must have the following fields/.test(e.message));
assert(/metadata/.test(e.message));
}
});
});

Expand Down