diff --git a/__snapshots__/test.js.snap b/__snapshots__/test.js.snap
index 14fca07..fc1e44c 100644
--- a/__snapshots__/test.js.snap
+++ b/__snapshots__/test.js.snap
@@ -200,6 +200,20 @@ exports[`Test Fixtures plugin-config 1`] = `
exports[`Test Fixtures plugin-config 2`] = `Map {}`;
+exports[`Test Fixtures react-intl 1`] = `
+"[1/4] 🔍 Finding JS and HBS files...
+[2/4] 🔍 Searching for translations keys in JS and HBS files...
+[3/4] ⚙️ Checking for unused translations...
+[4/4] ⚙️ Checking for missing translations...
+
+ 👏 No unused translations were found!
+
+ 👏 No missing translations were found!
+"
+`;
+
+exports[`Test Fixtures react-intl 2`] = `Map {}`;
+
exports[`Test Fixtures remove-unused-translations 1`] = `
"[1/4] 🔍 Finding JS and HBS files...
[2/4] 🔍 Searching for translations keys in JS and HBS files...
diff --git a/fixtures/react-intl/app/react/components/my-localized-component.jsx b/fixtures/react-intl/app/react/components/my-localized-component.jsx
new file mode 100644
index 0000000..37b21fc
--- /dev/null
+++ b/fixtures/react-intl/app/react/components/my-localized-component.jsx
@@ -0,0 +1,18 @@
+import { FormattedMessage, useIntl } from 'react-intl';
+
+export function MyLocalizedComponent() {
+ // legacy API
+ const { t } = useIntl();
+
+ // new API
+ const intl = useIntl();
+
+ return (
+
+ {t('hook-translation')}
+ {intl.formatMessage({ id: 'hook-translation-new' })}
+ {intl.formatMessage({ id: true ? 'hook-alternate' : 'hook-consequent'})}
+
+
+ );
+}
diff --git a/fixtures/react-intl/app/react/components/my-ts-localized-component.tsx b/fixtures/react-intl/app/react/components/my-ts-localized-component.tsx
new file mode 100644
index 0000000..43e1297
--- /dev/null
+++ b/fixtures/react-intl/app/react/components/my-ts-localized-component.tsx
@@ -0,0 +1,9 @@
+import { FormattedMessage } from 'react-intl';
+
+export function MyLocalizedComponent(): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/fixtures/react-intl/translations/de.json b/fixtures/react-intl/translations/de.json
new file mode 100644
index 0000000..eddb982
--- /dev/null
+++ b/fixtures/react-intl/translations/de.json
@@ -0,0 +1,8 @@
+{
+ "jsx-translation": "JSX but in german!",
+ "hook-translation": "Hook but in german!",
+ "hook-translation-new": "new hook but in german!",
+ "hook-alternate": "Ich bin in a ternary!",
+ "hook-consequent": "Ich also bin in a ternary!",
+ "tsx-translation": "TSX but in german!"
+}
diff --git a/fixtures/react-intl/translations/en.json b/fixtures/react-intl/translations/en.json
new file mode 100644
index 0000000..3435385
--- /dev/null
+++ b/fixtures/react-intl/translations/en.json
@@ -0,0 +1,8 @@
+{
+ "jsx-translation": "JSX!",
+ "hook-translation": "Hook!",
+ "hook-translation-new": "new hook!",
+ "hook-alternate": "I'm in a ternary!",
+ "hook-consequent": "I'm also in a ternary!",
+ "tsx-translation": "TSX!"
+}
diff --git a/index.js b/index.js
index 5003f9e..f5a445f 100755
--- a/index.js
+++ b/index.js
@@ -239,6 +239,8 @@ async function analyzeFile(cwd, file, options) {
if ('.gjs' === extension || (includeGtsExtension && '.gts' === extension)) {
return analyzeGJSFile(content, options);
+ } else if (['.tsx', '.jsx'].includes(extension)) {
+ return analyzeJsxFile(content, options.userPlugins);
} else if (['.js', ...options.userExtensions].includes(extension)) {
return analyzeJsFile(content, options.userPlugins);
} else if (extension === '.hbs') {
@@ -298,6 +300,98 @@ async function analyzeGJSFile(gjsGtsContent, options) {
return new Set([...keysFromJs, ...keysFromHbs]);
}
+async function analyzeJsxFile(content, userPlugins) {
+ let translationKeys = new Set();
+
+ let ast = BabelParser.parse(content, {
+ sourceType: 'module',
+ plugins: ['jsx', 'typescript', 'dynamicImport', ...userPlugins],
+ });
+
+ // store ids passed to the component in the translationKeys set
+ traverse(ast, {
+ JSXOpeningElement({ node }) {
+ if (node.name.type === 'JSXIdentifier' && node.name.name === 'FormattedMessage') {
+ for (let attribute of node.attributes) {
+ if (
+ attribute.type === 'JSXAttribute' &&
+ attribute.name.name === 'id' &&
+ attribute.value
+ ) {
+ if (attribute.value.type === 'StringLiteral') {
+ translationKeys.add(attribute.value.value);
+ } else if (attribute.value.type === 'JSXExpressionContainer') {
+ if (attribute.value.expression.type === 'ConditionalExpression') {
+ if (attribute.value.expression.alternate.type === 'StringLiteral') {
+ translationKeys.add(attribute.value.expression.alternate.value);
+ }
+ if (attribute.value.expression.consequent.type === 'StringLiteral') {
+ translationKeys.add(attribute.value.expression.consequent.value);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ // store ids passed to the t function in the translationKeys set
+ CallExpression({ node }) {
+ let { callee } = node;
+ if (node.arguments.length === 0) return;
+
+ // handle t('foo') case
+ if (callee.type === 'Identifier' && callee.name === 't') {
+ let firstParam = node.arguments[0];
+ if (firstParam.type === 'StringLiteral') {
+ translationKeys.add(firstParam.value);
+ } else if (firstParam.type === 'ConditionalExpression') {
+ if (firstParam.alternate.type === 'StringLiteral') {
+ translationKeys.add(firstParam.alternate.value);
+ }
+ if (firstParam.consequent.type === 'StringLiteral') {
+ translationKeys.add(firstParam.consequent.value);
+ }
+ }
+ }
+ // handle intl.formatMessage({ id: 'foo' }) case
+ else if (
+ callee.type === 'MemberExpression' &&
+ callee.object.type === 'Identifier' &&
+ callee.object.name === 'intl' &&
+ callee.property.type === 'Identifier' &&
+ callee.property.name === 'formatMessage'
+ ) {
+ let firstParam = node.arguments[0];
+ if (firstParam.type === 'ObjectExpression') {
+ for (let property of firstParam.properties) {
+ if (
+ property.type === 'ObjectProperty' &&
+ property.key.type === 'Identifier' &&
+ property.key.name === 'id'
+ ) {
+ // if it's a string literal, add it to the translationKeys set
+ if (property.value.type === 'StringLiteral') {
+ translationKeys.add(property.value.value);
+ }
+ // else, if it's a ternary operator, add the consequent and alternate to the translationKeys set
+ else if (property.value.type === 'ConditionalExpression') {
+ if (property.value.alternate.type === 'StringLiteral') {
+ translationKeys.add(property.value.alternate.value);
+ }
+ if (property.value.consequent.type === 'StringLiteral') {
+ translationKeys.add(property.value.consequent.value);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ });
+
+ return translationKeys;
+}
+
async function analyzeJsFile(content, userPlugins) {
let translationKeys = new Set();
diff --git a/test.js b/test.js
index 6a3d6aa..ae46abe 100644
--- a/test.js
+++ b/test.js
@@ -43,6 +43,9 @@ describe('Test Fixtures', () => {
'custom-t-helpers': {
helpers: ['t-error'],
},
+ 'react-intl': {
+ extensions: ['.jsx', '.tsx'],
+ }
};
beforeEach(() => {