Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

babel-plugin-i18next-extract

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

babel-plugin-i18next-extract - npm Package Compare versions

Comparing version 0.1.0-alpha.2 to 0.1.0-alpha.3

lib/src/extractors/withTranslationHOC.d.ts

283

lib/index.es.js

@@ -34,23 +34,24 @@ import i18next from 'i18next';

/**
* Given a Babel BaseComment, try to extract a comment hint.
* Given a Babel BaseComment, extract base comment hints.
* @param baseComment babel comment
* @returns A comment hint without line interval information.
* @yields Comment hint without line interval information.
*/
function extractCommentHint(baseComment) {
const trimmedValue = baseComment.value.trim();
const keyword = trimmedValue.split(/\s+/)[0];
const value = trimmedValue.split(/\s+(.+)/)[1] || '';
for (let [commentHintType, commentHintKeywords] of Object.entries(COMMENT_HINTS_KEYWORDS)) {
for (let [commentHintScope, commentHintKeyword] of Object.entries(commentHintKeywords)) {
if (keyword === commentHintKeyword) {
return {
type: commentHintType,
scope: commentHintScope,
value,
baseComment: baseComment,
};
function* extractCommentHintFromBaseComment(baseComment) {
for (const line of baseComment.value.split(/\r?\n/)) {
const trimmedValue = line.trim();
const keyword = trimmedValue.split(/\s+/)[0];
const value = trimmedValue.split(/\s+(.+)/)[1] || '';
for (let [commentHintType, commentHintKeywords] of Object.entries(COMMENT_HINTS_KEYWORDS)) {
for (let [commentHintScope, commentHintKeyword] of Object.entries(commentHintKeywords)) {
if (keyword === commentHintKeyword) {
yield {
type: commentHintType,
scope: commentHintScope,
value,
baseComment: baseComment,
};
}
}
}
}
return null;
}

@@ -74,4 +75,4 @@ /**

result.push({
startLine: commentHint.baseComment.loc.start.line + 1,
stopLine: commentHint.baseComment.loc.start.line + 1,
startLine: commentHint.baseComment.loc.end.line + 1,
stopLine: commentHint.baseComment.loc.end.line + 1,
...commentHint,

@@ -104,11 +105,7 @@ });

function parseCommentHints(baseComments) {
const result = Array();
const baseCommentHints = Array();
for (const baseComment of baseComments) {
const commentHint = extractCommentHint(baseComment);
if (commentHint === null) {
continue;
}
result.push(commentHint);
baseCommentHints.push(...extractCommentHintFromBaseComment(baseComment));
}
return computeCommentHintsIntervals(result);
return computeCommentHintsIntervals(baseCommentHints);
}

@@ -940,2 +937,216 @@ /**

/**
* Find whether a given function or class is wrapped with "withTranslation" HOC
* somewhere.
* @param path Function or class declaration node path.
* @returns "withTranslation()()" call expression if found. Else null.
*/
function findWithTranslationHOCCallExpression(path) {
let functionIdentifier = path.get('id');
if (!Array.isArray(functionIdentifier) &&
!functionIdentifier.isIdentifier() &&
path.parentPath.isVariableDeclarator()) {
// It doesn't look like "function MyComponent(…)"
// but like be "const MyComponent = (…) => …" or "const MyComponent = function(…) { … }"
functionIdentifier = path.parentPath.get('id');
}
if (Array.isArray(functionIdentifier) || !functionIdentifier.isIdentifier())
return null;
// Try to find a withTranslation() call in parent scope
for (const refPath of path.parentPath.scope.bindings[functionIdentifier.node.name].referencePaths) {
const callExpr = refPath.findParent(parentPath => {
if (!parentPath.isCallExpression())
return false;
const callee = parentPath.get('callee');
return (callee.isCallExpression() &&
referencesImport(callee.get('callee'), 'react-i18next', 'withTranslation'));
});
if (callExpr !== null) {
return callExpr.get('callee');
}
}
return null;
}
/**
* Try to find "t" in an object spread. Useful when looking for the "t" key
* in a spread object. e.g. const {t} = props;
*
* @param path object pattern
* @returns t identifier or null of it was not found in the object pattern.
*/
function findTFunctionIdentifierInObjectPattern(path) {
const props = path.get('properties');
for (const prop of props) {
if (prop.isObjectProperty()) {
const key = prop.get('key');
if (!Array.isArray(key) && key.isIdentifier() && key.node.name === 't') {
return key;
}
}
}
return null;
}
/**
* Find T function calls from a props assignment. Prop assignment can occur
* in function parameters (i.e. "function Component(props)" or
* "function Component({t})") or in a variable declarator (i.e.
* "const props = …" or "const {t} = props").
*
* @param propsId identifier for the prop assignment. e.g. "props" or "{t}"
* @returns Call expressions to t function.
*/
function findTFunctionCallsFromPropsAssignment(propsId) {
const tReferences = Array();
if (propsId.isObjectPattern()) {
// got "function MyComponent({t, other, props})"
// or "const {t, other, props} = this.props"
// we want to find references to "t"
const tFunctionIdentifier = findTFunctionIdentifierInObjectPattern(propsId);
if (tFunctionIdentifier === null)
return [];
const tBinding = tFunctionIdentifier.scope.bindings[tFunctionIdentifier.node.name];
if (!tBinding)
return [];
tReferences.push(...tBinding.referencePaths);
}
else if (propsId.isIdentifier()) {
// got "function MyComponent(props)"
// or "const props = this.props"
// we want to find references to props.t
const references = propsId.scope.bindings[propsId.node.name].referencePaths;
for (const reference of references) {
if (reference.parentPath.isMemberExpression()) {
const prop = reference.parentPath.get('property');
if (!Array.isArray(prop) && prop.node.name === 't') {
tReferences.push(reference.parentPath);
}
}
}
}
// We have candidates. Let's see if t references are actual calls to the t
// function
const tCalls = Array();
for (const tCall of tReferences) {
if (tCall.parentPath.isCallExpression()) {
tCalls.push(tCall.parentPath);
}
}
return tCalls;
}
/**
* Find all t function calls in a class component.
* @param path node path to the class component.
*/
function findTFunctionCallsInClassComponent(path) {
const result = Array();
const thisVisitor = {
ThisExpression(path) {
if (!path.parentPath.isMemberExpression())
return;
const propProperty = path.parentPath.get('property');
if (Array.isArray(propProperty) || !propProperty.isIdentifier())
return;
if (propProperty.node.name !== 'props')
return;
// Ok, this is interesting, we have something with "this.props"
if (path.parentPath.parentPath.isMemberExpression()) {
// We have something in the form "this.props.xxxx".
const tIdentifier = path.parentPath.parentPath.get('property');
if (Array.isArray(tIdentifier) || !tIdentifier.isIdentifier())
return;
if (tIdentifier.node.name !== 't')
return;
// We have something in the form "this.props.t". Let's see if it's an
// actual function call or an assignment.
const tExpression = path.parentPath.parentPath.parentPath;
if (tExpression.isCallExpression()) {
// Simple case. Direct call to "this.props.t()"
result.push(tExpression);
}
else if (tExpression.isVariableDeclarator()) {
// Hard case. const t = this.props.t;
// Let's loop through all references to t.
const id = tExpression.get('id');
if (!id.isIdentifier())
return;
for (const reference of id.scope.bindings[id.node.name]
.referencePaths) {
if (reference.parentPath.isCallExpression()) {
result.push(reference.parentPath);
}
}
}
}
else if (path.parentPath.parentPath.isVariableDeclarator()) {
// We have something in the form "const props = this.props"
// Or "const {t} = this.props"
const id = path.parentPath.parentPath.get('id');
result.push(...findTFunctionCallsFromPropsAssignment(id));
}
},
};
path.traverse(thisVisitor);
return result;
}
/**
* Find t function calls in a function component.
* @param path node path to the function component.
*/
function findTFunctionCallsInFunctionComponent(path) {
const propsParam = path.get('params')[0];
if (propsParam === undefined)
return [];
return findTFunctionCallsFromPropsAssignment(propsParam);
}
/**
* Parse function or class declaration (likely components) to find whether
* they are wrapped with "withTranslation()" HOC, and if so, extract all the
* translations that come from the "t" function injected in the component
* properties.
*
* @param path node path to the component
* @param config plugin configuration
* @param commentHints parsed comment hints
*/
function extractWithTranslationHOC(path, config, commentHints = []) {
// Detect if this component is wrapped with withTranslation() somewhere
const withTranslationCallExpression = findWithTranslationHOCCallExpression(path);
if (withTranslationCallExpression === null)
return [];
let tCalls;
if (path.isClassDeclaration()) {
tCalls = findTFunctionCallsInClassComponent(path);
}
else {
tCalls = findTFunctionCallsInFunctionComponent(path);
}
// Extract namespace
let ns;
const nsCommentHint = getCommentHintForPath(withTranslationCallExpression, 'NAMESPACE', commentHints);
if (nsCommentHint) {
// We got a comment hint, take its value as namespace.
ns = nsCommentHint.value;
}
else {
// Otherwise, try to get namespace from arguments.
const namespaceArgument = withTranslationCallExpression.get('arguments')[0];
ns = getFirstOrNull(evaluateIfConfident(namespaceArgument));
}
let keys = Array();
for (const tCall of tCalls) {
keys = [
...keys,
...extractTFunction(tCall, config, commentHints, true).map(k => ({
// Add namespace if it was not explicitely set in t() call.
...k,
parsedOptions: {
...k.parsedOptions,
ns: k.parsedOptions.ns || ns,
},
})),
];
}
return keys;
}
// We have to store which nodes were extracted because the plugin might be called multiple times

@@ -971,10 +1182,8 @@ // by Babel and the state would be lost across calls.

catch (err) {
if (err instanceof ExtractionError) {
// eslint-disable-next-line no-console
console.warn(`${PLUGIN_NAME}: Extraction error in ${filename} at line ` +
`${lineNumber}. ${err.message}`);
}
else {
if (!(err instanceof ExtractionError)) {
throw err;
}
// eslint-disable-next-line no-console
console.warn(`${PLUGIN_NAME}: Extraction error in ${filename} at line ` +
`${lineNumber}. ${err.message}`);
}

@@ -997,2 +1206,14 @@ }

},
ClassDeclaration(path, state) {
const extractState = this.I18NextExtract;
handleExtraction(path, state, collect => {
collect(extractWithTranslationHOC(path, extractState.config, extractState.commentHints));
});
},
Function(path, state) {
const extractState = this.I18NextExtract;
handleExtraction(path, state, collect => {
collect(extractWithTranslationHOC(path, extractState.config, extractState.commentHints));
});
},
};

@@ -999,0 +1220,0 @@ function plugin (api) {

@@ -38,23 +38,24 @@ 'use strict';

/**
* Given a Babel BaseComment, try to extract a comment hint.
* Given a Babel BaseComment, extract base comment hints.
* @param baseComment babel comment
* @returns A comment hint without line interval information.
* @yields Comment hint without line interval information.
*/
function extractCommentHint(baseComment) {
const trimmedValue = baseComment.value.trim();
const keyword = trimmedValue.split(/\s+/)[0];
const value = trimmedValue.split(/\s+(.+)/)[1] || '';
for (let [commentHintType, commentHintKeywords] of Object.entries(COMMENT_HINTS_KEYWORDS)) {
for (let [commentHintScope, commentHintKeyword] of Object.entries(commentHintKeywords)) {
if (keyword === commentHintKeyword) {
return {
type: commentHintType,
scope: commentHintScope,
value,
baseComment: baseComment,
};
function* extractCommentHintFromBaseComment(baseComment) {
for (const line of baseComment.value.split(/\r?\n/)) {
const trimmedValue = line.trim();
const keyword = trimmedValue.split(/\s+/)[0];
const value = trimmedValue.split(/\s+(.+)/)[1] || '';
for (let [commentHintType, commentHintKeywords] of Object.entries(COMMENT_HINTS_KEYWORDS)) {
for (let [commentHintScope, commentHintKeyword] of Object.entries(commentHintKeywords)) {
if (keyword === commentHintKeyword) {
yield {
type: commentHintType,
scope: commentHintScope,
value,
baseComment: baseComment,
};
}
}
}
}
return null;
}

@@ -78,4 +79,4 @@ /**

result.push({
startLine: commentHint.baseComment.loc.start.line + 1,
stopLine: commentHint.baseComment.loc.start.line + 1,
startLine: commentHint.baseComment.loc.end.line + 1,
stopLine: commentHint.baseComment.loc.end.line + 1,
...commentHint,

@@ -108,11 +109,7 @@ });

function parseCommentHints(baseComments) {
const result = Array();
const baseCommentHints = Array();
for (const baseComment of baseComments) {
const commentHint = extractCommentHint(baseComment);
if (commentHint === null) {
continue;
}
result.push(commentHint);
baseCommentHints.push(...extractCommentHintFromBaseComment(baseComment));
}
return computeCommentHintsIntervals(result);
return computeCommentHintsIntervals(baseCommentHints);
}

@@ -944,2 +941,216 @@ /**

/**
* Find whether a given function or class is wrapped with "withTranslation" HOC
* somewhere.
* @param path Function or class declaration node path.
* @returns "withTranslation()()" call expression if found. Else null.
*/
function findWithTranslationHOCCallExpression(path) {
let functionIdentifier = path.get('id');
if (!Array.isArray(functionIdentifier) &&
!functionIdentifier.isIdentifier() &&
path.parentPath.isVariableDeclarator()) {
// It doesn't look like "function MyComponent(…)"
// but like be "const MyComponent = (…) => …" or "const MyComponent = function(…) { … }"
functionIdentifier = path.parentPath.get('id');
}
if (Array.isArray(functionIdentifier) || !functionIdentifier.isIdentifier())
return null;
// Try to find a withTranslation() call in parent scope
for (const refPath of path.parentPath.scope.bindings[functionIdentifier.node.name].referencePaths) {
const callExpr = refPath.findParent(parentPath => {
if (!parentPath.isCallExpression())
return false;
const callee = parentPath.get('callee');
return (callee.isCallExpression() &&
referencesImport(callee.get('callee'), 'react-i18next', 'withTranslation'));
});
if (callExpr !== null) {
return callExpr.get('callee');
}
}
return null;
}
/**
* Try to find "t" in an object spread. Useful when looking for the "t" key
* in a spread object. e.g. const {t} = props;
*
* @param path object pattern
* @returns t identifier or null of it was not found in the object pattern.
*/
function findTFunctionIdentifierInObjectPattern(path) {
const props = path.get('properties');
for (const prop of props) {
if (prop.isObjectProperty()) {
const key = prop.get('key');
if (!Array.isArray(key) && key.isIdentifier() && key.node.name === 't') {
return key;
}
}
}
return null;
}
/**
* Find T function calls from a props assignment. Prop assignment can occur
* in function parameters (i.e. "function Component(props)" or
* "function Component({t})") or in a variable declarator (i.e.
* "const props = …" or "const {t} = props").
*
* @param propsId identifier for the prop assignment. e.g. "props" or "{t}"
* @returns Call expressions to t function.
*/
function findTFunctionCallsFromPropsAssignment(propsId) {
const tReferences = Array();
if (propsId.isObjectPattern()) {
// got "function MyComponent({t, other, props})"
// or "const {t, other, props} = this.props"
// we want to find references to "t"
const tFunctionIdentifier = findTFunctionIdentifierInObjectPattern(propsId);
if (tFunctionIdentifier === null)
return [];
const tBinding = tFunctionIdentifier.scope.bindings[tFunctionIdentifier.node.name];
if (!tBinding)
return [];
tReferences.push(...tBinding.referencePaths);
}
else if (propsId.isIdentifier()) {
// got "function MyComponent(props)"
// or "const props = this.props"
// we want to find references to props.t
const references = propsId.scope.bindings[propsId.node.name].referencePaths;
for (const reference of references) {
if (reference.parentPath.isMemberExpression()) {
const prop = reference.parentPath.get('property');
if (!Array.isArray(prop) && prop.node.name === 't') {
tReferences.push(reference.parentPath);
}
}
}
}
// We have candidates. Let's see if t references are actual calls to the t
// function
const tCalls = Array();
for (const tCall of tReferences) {
if (tCall.parentPath.isCallExpression()) {
tCalls.push(tCall.parentPath);
}
}
return tCalls;
}
/**
* Find all t function calls in a class component.
* @param path node path to the class component.
*/
function findTFunctionCallsInClassComponent(path) {
const result = Array();
const thisVisitor = {
ThisExpression(path) {
if (!path.parentPath.isMemberExpression())
return;
const propProperty = path.parentPath.get('property');
if (Array.isArray(propProperty) || !propProperty.isIdentifier())
return;
if (propProperty.node.name !== 'props')
return;
// Ok, this is interesting, we have something with "this.props"
if (path.parentPath.parentPath.isMemberExpression()) {
// We have something in the form "this.props.xxxx".
const tIdentifier = path.parentPath.parentPath.get('property');
if (Array.isArray(tIdentifier) || !tIdentifier.isIdentifier())
return;
if (tIdentifier.node.name !== 't')
return;
// We have something in the form "this.props.t". Let's see if it's an
// actual function call or an assignment.
const tExpression = path.parentPath.parentPath.parentPath;
if (tExpression.isCallExpression()) {
// Simple case. Direct call to "this.props.t()"
result.push(tExpression);
}
else if (tExpression.isVariableDeclarator()) {
// Hard case. const t = this.props.t;
// Let's loop through all references to t.
const id = tExpression.get('id');
if (!id.isIdentifier())
return;
for (const reference of id.scope.bindings[id.node.name]
.referencePaths) {
if (reference.parentPath.isCallExpression()) {
result.push(reference.parentPath);
}
}
}
}
else if (path.parentPath.parentPath.isVariableDeclarator()) {
// We have something in the form "const props = this.props"
// Or "const {t} = this.props"
const id = path.parentPath.parentPath.get('id');
result.push(...findTFunctionCallsFromPropsAssignment(id));
}
},
};
path.traverse(thisVisitor);
return result;
}
/**
* Find t function calls in a function component.
* @param path node path to the function component.
*/
function findTFunctionCallsInFunctionComponent(path) {
const propsParam = path.get('params')[0];
if (propsParam === undefined)
return [];
return findTFunctionCallsFromPropsAssignment(propsParam);
}
/**
* Parse function or class declaration (likely components) to find whether
* they are wrapped with "withTranslation()" HOC, and if so, extract all the
* translations that come from the "t" function injected in the component
* properties.
*
* @param path node path to the component
* @param config plugin configuration
* @param commentHints parsed comment hints
*/
function extractWithTranslationHOC(path, config, commentHints = []) {
// Detect if this component is wrapped with withTranslation() somewhere
const withTranslationCallExpression = findWithTranslationHOCCallExpression(path);
if (withTranslationCallExpression === null)
return [];
let tCalls;
if (path.isClassDeclaration()) {
tCalls = findTFunctionCallsInClassComponent(path);
}
else {
tCalls = findTFunctionCallsInFunctionComponent(path);
}
// Extract namespace
let ns;
const nsCommentHint = getCommentHintForPath(withTranslationCallExpression, 'NAMESPACE', commentHints);
if (nsCommentHint) {
// We got a comment hint, take its value as namespace.
ns = nsCommentHint.value;
}
else {
// Otherwise, try to get namespace from arguments.
const namespaceArgument = withTranslationCallExpression.get('arguments')[0];
ns = getFirstOrNull(evaluateIfConfident(namespaceArgument));
}
let keys = Array();
for (const tCall of tCalls) {
keys = [
...keys,
...extractTFunction(tCall, config, commentHints, true).map(k => ({
// Add namespace if it was not explicitely set in t() call.
...k,
parsedOptions: {
...k.parsedOptions,
ns: k.parsedOptions.ns || ns,
},
})),
];
}
return keys;
}
// We have to store which nodes were extracted because the plugin might be called multiple times

@@ -975,10 +1186,8 @@ // by Babel and the state would be lost across calls.

catch (err) {
if (err instanceof ExtractionError) {
// eslint-disable-next-line no-console
console.warn(`${PLUGIN_NAME}: Extraction error in ${filename} at line ` +
`${lineNumber}. ${err.message}`);
}
else {
if (!(err instanceof ExtractionError)) {
throw err;
}
// eslint-disable-next-line no-console
console.warn(`${PLUGIN_NAME}: Extraction error in ${filename} at line ` +
`${lineNumber}. ${err.message}`);
}

@@ -1001,2 +1210,14 @@ }

},
ClassDeclaration(path, state) {
const extractState = this.I18NextExtract;
handleExtraction(path, state, collect => {
collect(extractWithTranslationHOC(path, extractState.config, extractState.commentHints));
});
},
Function(path, state) {
const extractState = this.I18NextExtract;
handleExtraction(path, state, collect => {
collect(extractWithTranslationHOC(path, extractState.config, extractState.commentHints));
});
},
};

@@ -1003,0 +1224,0 @@ function plugin (api) {

2

package.json
{
"name": "babel-plugin-i18next-extract",
"version": "0.1.0-alpha.2",
"version": "0.1.0-alpha.3",
"description": "Statically extract translation keys from i18next application.",

@@ -5,0 +5,0 @@ "repository": {

@@ -136,3 +136,3 @@ # babel-plugin-i18next-extract

- [x] `Translation` render prop support (with plural forms, contexts and namespaces).
- [ ] (todo) Fuzzy namespace inference from `withTranslation` HoC.
- [x] Namespace inference from `withTranslation` HOC.
- [x] Namespace inference:

@@ -149,4 +149,4 @@ - [x] Depending on the key value.

| Option | Type | Description | Default |
|-----------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------|
| locales | `string[]` | All the locales your project supports. babel-plugin-i18next-extract will generate a JSON file for each locale. | `['en']` |
|-|-|-|-|
| locales | `string[]` | Locales your project supports. | `['en']` |
| defaultNS | `string` | The default namespace that your translation use. | `'translation'` |

@@ -153,0 +153,0 @@ | pluralSeparator | `string` | String you want to use to split plural from keys. See [i18next Configuration options](https://www.i18next.com/overview/configuration-options#misc) | `'_'` |

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc