diff --git a/README.md b/README.md index 00a4cc5..3561796 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,7 @@ The default message descriptors for the app's default language will be extracted { "plugins": [ ["react-intl", { - "messagesDir": "./build/messages/", - "enforceDescriptions": true + "messagesDir": "./build/messages/" }] ] } @@ -35,7 +34,9 @@ The default message descriptors for the app's default language will be extracted - **`messagesDir`**: The target location where the plugin will output a `.json` file corresponding to each component from which React Intl messages were extracted. If not provided, the extracted message descriptors will only be accessible via Babel's API. -- **`enforceDescriptions`**: Whether or not message declarations _must_ contain a `description` to provide context to translators. Defaults to: `false`. +- **`enforceDescriptions`**: Whether message declarations _must_ contain a `description` to provide context to translators. Defaults to: `false`. + +- **`extractSourceLocation`**: Whether the metadata about the location of the message in the source file should be extracted. If `true`, then `file`, `start`, and `end` fields will exist for each extracted message descriptors. Defaults to `false`. - **`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. diff --git a/scripts/build-fixtures.js b/scripts/build-fixtures.js index 6fcf2ec..c9c9417 100644 --- a/scripts/build-fixtures.js +++ b/scripts/build-fixtures.js @@ -7,6 +7,9 @@ const baseDir = p.resolve(`${__dirname}/../test/fixtures`); const fixtures = [ 'defineMessages', + ['extractSourceLocation', { + extractSourceLocation: true, + }], 'FormattedHTMLMessage', 'FormattedMessage', ['moduleSourceName', { diff --git a/src/index.js b/src/index.js index 652a3c7..0a32f89 100644 --- a/src/index.js +++ b/src/index.js @@ -107,7 +107,7 @@ export default function () { } function storeMessage({id, description, defaultMessage}, path, state) { - const {opts, reactIntl} = state; + const {file, opts, reactIntl} = state; if (!(id && defaultMessage)) { throw path.buildCodeFrameError( @@ -134,7 +134,15 @@ export default function () { ); } - reactIntl.messages.set(id, {id, description, defaultMessage}); + let loc; + if (opts.extractSourceLocation) { + loc = { + file: p.relative(process.cwd(), file.opts.filename), + ...path.node.loc, + }; + } + + reactIntl.messages.set(id, {id, description, defaultMessage, ...loc}); } function referencesImport(path, mod, importedNames) { @@ -257,7 +265,7 @@ export default function () { // Evaluate the Message Descriptor values, then store it. descriptor = evaluateMessageDescriptor(descriptor); - storeMessage(descriptor, path, state); + storeMessage(descriptor, messageObj, state); } if (referencesImport(callee, moduleSourceName, FUNCTION_NAMES)) { diff --git a/test/fixtures/extractSourceLocation/actual.js b/test/fixtures/extractSourceLocation/actual.js new file mode 100644 index 0000000..860368c --- /dev/null +++ b/test/fixtures/extractSourceLocation/actual.js @@ -0,0 +1,13 @@ +import React, {Component} from 'react'; +import {FormattedMessage} from 'react-intl'; + +export default class Foo extends Component { + render() { + return ( + + ); + } +} diff --git a/test/fixtures/extractSourceLocation/expected.js b/test/fixtures/extractSourceLocation/expected.js new file mode 100644 index 0000000..294f3d2 --- /dev/null +++ b/test/fixtures/extractSourceLocation/expected.js @@ -0,0 +1,45 @@ +'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', + defaultMessage: 'Hello World!' + }); + } + }]); + + return Foo; +}(_react.Component); + +exports.default = Foo; diff --git a/test/fixtures/extractSourceLocation/expected.json b/test/fixtures/extractSourceLocation/expected.json new file mode 100644 index 0000000..1dcd203 --- /dev/null +++ b/test/fixtures/extractSourceLocation/expected.json @@ -0,0 +1,15 @@ +[ + { + "id": "foo.bar.baz", + "defaultMessage": "Hello World!", + "file": "test/fixtures/extractSourceLocation/actual.js", + "start": { + "line": 7, + "column": 12 + }, + "end": { + "line": 10, + "column": 14 + } + } +] diff --git a/test/index.js b/test/index.js index 0fca692..46c4312 100644 --- a/test/index.js +++ b/test/index.js @@ -12,6 +12,7 @@ const skipTests = [ '.babelrc', '.DS_Store', 'enforceDescriptions', + 'extractSourceLocation', 'moduleSourceName', 'icuSyntax', ]; @@ -49,7 +50,9 @@ describe('options', () => { const fixtureDir = path.join(fixturesDir, 'enforceDescriptions'); try { - transform(path.join(fixtureDir, 'actual.js')); + transform(path.join(fixtureDir, 'actual.js'), { + enforceDescriptions: true, + }); assert(false); } catch (e) { assert(e); @@ -83,6 +86,30 @@ describe('options', () => { console.error(e); assert(false); } + + // 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)); + }); + + it('respects extractSourceLocation', () => { + const fixtureDir = path.join(fixturesDir, 'extractSourceLocation'); + + try { + transform(path.join(fixtureDir, 'actual.js'), { + extractSourceLocation: true, + }); + assert(true); + } catch (e) { + console.error(e); + assert(false); + } + + // 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)); }); }); @@ -99,13 +126,11 @@ describe('errors', () => { assert(/Expected .* but "\." found/.test(e.message)); } }); - }); const BASE_OPTIONS = { messagesDir: baseDir, - enforceDescriptions: true, }; function transform(filePath, options = {}) {