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.1 to 0.1.0-alpha.2

407

lib/index.es.js

@@ -6,57 +6,126 @@ import i18next from 'i18next';

const PLUGIN_NAME = 'babel-plugin-i18next-extract';
const COMMENT_DISABLE_LINE = 'i18next-extract-disable-line';
const COMMENT_DISABLE_NEXT_LINE = 'i18next-extract-disable-next-line';
const COMMENT_DISABLE_SECTION_START = 'i18next-extract-disable';
const COMMENT_DISABLE_SECTION_STOP = 'i18next-extract-enable';
const COMMENT_HINT_PREFIX = 'i18next-extract-';
const COMMENT_HINTS_KEYWORDS = {
DISABLE: {
LINE: COMMENT_HINT_PREFIX + 'disable-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'disable-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'disable',
SECTION_STOP: COMMENT_HINT_PREFIX + 'enable',
},
NAMESPACE: {
LINE: COMMENT_HINT_PREFIX + 'mark-ns-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'mark-ns-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'mark-ns-start',
SECTION_STOP: COMMENT_HINT_PREFIX + 'mark-ns-stop',
},
CONTEXT: {
LINE: COMMENT_HINT_PREFIX + 'mark-context-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'mark-context-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'mark-context-start',
SECTION_STOP: COMMENT_HINT_PREFIX + 'mark-context-stop',
},
PLURAL: {
LINE: COMMENT_HINT_PREFIX + 'mark-plural-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'mark-plural-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'mark-plural-start',
SECTION_STOP: COMMENT_HINT_PREFIX + 'mark-plural-stop',
},
};
/**
* Computes line intervals where i18n extraction should be disabled.
* @param comments Babel comments
* @returns sections on which extraction should be disabled
* Given a Babel BaseComment, try to extract a comment hint.
* @param baseComment babel comment
* @returns A comment hint without line interval information.
*/
function computeCommentDisableIntervals(comments) {
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,
};
}
}
}
return null;
}
/**
* Given an array of comment hints, compute their intervals.
* @param commentHints comment hints without line intervals information.
* @returns Comment hints with line interval information.
*/
function computeCommentHintsIntervals(commentHints) {
const result = Array();
let lastDisableLine = null;
for (const { value, loc } of comments) {
const commentWords = value.split(/\s+/);
if (commentWords.includes(COMMENT_DISABLE_LINE)) {
result.push([loc.start.line, loc.start.line]);
for (const commentHint of commentHints) {
if (commentHint.scope === 'LINE') {
result.push({
startLine: commentHint.baseComment.loc.start.line,
stopLine: commentHint.baseComment.loc.start.line,
...commentHint,
});
}
if (commentWords.includes(COMMENT_DISABLE_NEXT_LINE)) {
result.push([loc.end.line + 1, loc.end.line + 1]);
if (commentHint.scope === 'NEXT_LINE') {
result.push({
startLine: commentHint.baseComment.loc.start.line + 1,
stopLine: commentHint.baseComment.loc.start.line + 1,
...commentHint,
});
}
if (commentWords.includes(COMMENT_DISABLE_SECTION_START)) {
lastDisableLine = loc.start.line;
if (commentHint.scope === 'SECTION_START') {
result.push({
startLine: commentHint.baseComment.loc.start.line,
stopLine: Infinity,
...commentHint,
});
}
if (commentWords.includes(COMMENT_DISABLE_SECTION_STOP) &&
lastDisableLine !== null) {
result.push([lastDisableLine, loc.end.line]);
lastDisableLine = null;
if (commentHint.scope === 'SECTION_STOP') {
for (const res of result) {
if (res.type === commentHint.type &&
res.scope === 'SECTION_START' &&
res.stopLine === Infinity) {
res.stopLine = commentHint.baseComment.loc.start.line;
}
}
}
}
if (lastDisableLine !== null) {
result.push([lastDisableLine, Infinity]);
}
return result;
}
/**
* Check if a given number is within any of the given intervals.
* @param num number to check
* @param intervals array of intervals
* @returns true if num is within any of the intervals.
* Given Babel comments, extract the comment hints.
* @param baseComments Babel comments (ordered by line)
*/
function numberIsWithinIntervals(num, intervals) {
return intervals.some(([v0, v1]) => v0 <= num && num <= v1);
function parseCommentHints(baseComments) {
const result = Array();
for (const baseComment of baseComments) {
const commentHint = extractCommentHint(baseComment);
if (commentHint === null) {
continue;
}
result.push(commentHint);
}
return computeCommentHintsIntervals(result);
}
/**
* Check whether extraction is enables for a given path.
* @param path: path to check
* @param disableExtractionIntervals: line intervals where extraction is
* disabled.
* @returns true if the extraction is enabled for the given path.
* Find comment hint of a given type that applies to a Babel node path.
* @param path babel node path
* @param commentHintType Type of comment hint to look for.
* @param commentHints All the comment hints, as returned by parseCommentHints function.
*/
function extractionIsEnabledForPath(path, disableExtractionIntervals) {
return !(path.node.loc &&
numberIsWithinIntervals(path.node.loc.start.line, disableExtractionIntervals));
function getCommentHintForPath(path, commentHintType, commentHints) {
if (!path.node.loc)
return null;
const nodeLine = path.node.loc.start.line;
for (const commentHint of commentHints) {
if (commentHint.type === commentHintType &&
commentHint.startLine <= nodeLine &&
nodeLine <= commentHint.stopLine) {
return commentHint;
}
}
return null;
}

@@ -81,7 +150,67 @@

}
// AST Helpers
/**
* Given comment hints and a path, infer every I18NextOption we can from the comment hints.
* @param path path on which the comment hints should apply
* @param commentHints parsed comment hints
* @returns every parsed option that could be infered.
*/
function parseI18NextOptionsFromCommentHints(path, commentHints) {
const nsCommentHint = getCommentHintForPath(path, 'NAMESPACE', commentHints);
const contextCommentHint = getCommentHintForPath(path, 'CONTEXT', commentHints);
const pluralCommentHint = getCommentHintForPath(path, 'PLURAL', commentHints);
const res = {};
if (nsCommentHint !== null) {
res.ns = nsCommentHint.value;
}
if (contextCommentHint !== null) {
if (['', 'enable'].includes(contextCommentHint.value)) {
res.contexts = true;
}
else if (contextCommentHint.value === 'disable') {
res.contexts = false;
}
else {
try {
let val = JSON.parse(contextCommentHint.value);
if (Array.isArray(val))
res.contexts = val;
else
res.contexts = [contextCommentHint.value];
}
catch (err) {
res.contexts = [contextCommentHint.value];
}
}
}
if (pluralCommentHint !== null) {
if (pluralCommentHint.value === 'disable') {
res.hasCount = false;
}
else {
res.hasCount = true;
}
}
return res;
}
/**
* Improved version of BabelCore `referencesImport` function that also tries to detect wildcard
* imports.
*/
function referencesImport(nodePath, moduleSource, importName) {
if (nodePath.referencesImport(moduleSource, importName))
return true;
if (nodePath.isMemberExpression() || nodePath.isJSXMemberExpression()) {
const obj = nodePath.get('object');
const prop = nodePath.get('property');
if (Array.isArray(prop) ||
(!prop.isIdentifier() && !prop.isJSXIdentifier()))
return false;
return (obj.referencesImport(moduleSource, '*') && prop.node.name === importName);
}
return false;
}
/**
* Evaluates a node path if it can be evaluated with confidence.
*
* @param nodePath: node path to evaluate
* @param path: node path to evaluate
* @returns null if the node path couldn't be evaluated

@@ -189,26 +318,26 @@ */

function parseTCallOptions(path) {
let hasContext = false;
let hasCount = false;
let ns = null;
const res = {
contexts: false,
hasCount: false,
ns: null,
};
if (!path)
return { hasContext, hasCount, ns };
return res;
// Try brutal evaluation first.
const optsEvaluation = evaluateIfConfident(path);
if (optsEvaluation !== null) {
hasContext = 'context' in optsEvaluation;
hasCount = 'count' in optsEvaluation;
res.contexts = 'context' in optsEvaluation;
res.hasCount = 'count' in optsEvaluation;
const evaluatedNamespace = optsEvaluation['ns'];
ns = getFirstOrNull(evaluatedNamespace);
return { hasContext, hasCount, ns };
res.ns = getFirstOrNull(evaluatedNamespace);
}
// It didn't work. Let's try to parse object expression keys.
if (path.isObjectExpression()) {
hasContext = findKeyInObjectExpression(path, 'context') !== null;
hasCount = findKeyInObjectExpression(path, 'count') !== null;
else if (path.isObjectExpression()) {
// It didn't work. Let's try to parse object expression keys.
res.contexts = findKeyInObjectExpression(path, 'context') !== null;
res.hasCount = findKeyInObjectExpression(path, 'count') !== null;
const nsNode = findKeyInObjectExpression(path, 'ns');
const nsNodeEvaluation = evaluateIfConfident(nsNode);
ns = getFirstOrNull(nsNodeEvaluation);
return { hasContext, hasCount, ns };
res.ns = getFirstOrNull(nsNodeEvaluation);
}
throw new ExtractionError("Couldn't evaluate i18next options. Please, provide options as an object expression.");
return res;
}

@@ -219,5 +348,6 @@ /**

* @param path NodePath of the `i18next.t` call.
* @param commentHints parsed comment hints
* @throws ExtractionError when the extraction failed for the `t` call.
*/
function extractTCall(path) {
function extractTCall(path, commentHints) {
const args = path.get('arguments');

@@ -228,8 +358,11 @@ const keyEvaluation = evaluateIfConfident(args[0]);

`evaluable or skip the line using a skip comment (/* ` +
`${COMMENT_DISABLE_LINE} */ or /* ${COMMENT_DISABLE_NEXT_LINE} */).`);
`${COMMENT_HINTS_KEYWORDS.DISABLE.LINE} */ or /* ` +
`${COMMENT_HINTS_KEYWORDS.DISABLE.NEXT_LINE} */).`);
}
const parsedOptions = parseTCallOptions(args[1]);
return {
key: keyEvaluation,
parsedOptions,
parsedOptions: {
...parseTCallOptions(args[1]),
...parseI18NextOptionsFromCommentHints(path, commentHints),
},
nodePath: path,

@@ -244,12 +377,12 @@ };

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is disabled
* @param commentHints: parsed comment hints
* @param skipCheck: set to true if you know that the call expression arguments
* already is a `t` function.
*/
function extractTFunction(path, config, disableExtractionIntervals = [], skipCheck = false) {
if (!extractionIsEnabledForPath(path, disableExtractionIntervals))
function extractTFunction(path, config, commentHints = [], skipCheck = false) {
if (getCommentHintForPath(path, 'DISABLE', commentHints))
return [];
if (!skipCheck && !isSimpleTCall(path, config))
return [];
return [extractTCall(path)];
return [extractTCall(path, commentHints)];
}

@@ -265,3 +398,3 @@

const callee = path.get('callee');
return callee.referencesImport('react-i18next', 'useTranslation');
return referencesImport(callee, 'react-i18next', 'useTranslation');
}

@@ -273,10 +406,18 @@ /**

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
function extractUseTranslationHook(path, config, disableExtractionIntervals = []) {
function extractUseTranslationHook(path, config, commentHints = []) {
if (!isUseTranslationHook(path))
return [];
const namespaceArgument = path.get('arguments')[0];
const ns = getFirstOrNull(evaluateIfConfident(namespaceArgument));
let ns;
const nsCommentHint = getCommentHintForPath(path, '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 = path.get('arguments')[0];
ns = getFirstOrNull(evaluateIfConfident(namespaceArgument));
}
const parentPath = path.parentPath;

@@ -294,3 +435,3 @@ if (!parentPath.isVariableDeclarator())

...keys,
...extractTFunction(reference.parentPath, config, disableExtractionIntervals, true).map(k => ({
...extractTFunction(reference.parentPath, config, commentHints, true).map(k => ({
// Add namespace if it was not explicitely set in t() call.

@@ -316,5 +457,3 @@ ...k,

const openingElement = path.get('openingElement');
return openingElement
.get('name')
.referencesImport('react-i18next', 'Translation');
return referencesImport(openingElement.get('name'), 'react-i18next', 'Translation');
}

@@ -327,17 +466,23 @@ /**

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
function extractTranslationRenderProp(path, config, disableExtractionIntervals = []) {
function extractTranslationRenderProp(path, config, commentHints = []) {
if (!isTranslationRenderProp(path))
return [];
// Try to parse ns property
let ns = null;
const nsAttr = findJSXAttributeByName(path, 'ns');
if (nsAttr) {
let value = nsAttr.get('value');
if (value.isJSXExpressionContainer())
value = value.get('expression');
ns = getFirstOrNull(evaluateIfConfident(value));
let ns;
const nsCommentHint = getCommentHintForPath(path, 'NAMESPACE', commentHints);
if (nsCommentHint) {
// We got a comment hint, take its value as namespace.
ns = nsCommentHint.value;
}
else {
// Try to parse ns property
const nsAttr = findJSXAttributeByName(path, 'ns');
if (nsAttr) {
let value = nsAttr.get('value');
if (value.isJSXExpressionContainer())
value = value.get('expression');
ns = getFirstOrNull(evaluateIfConfident(value));
}
}
// We expect at least "<Translation>{(t) => …}</Translation>

@@ -363,3 +508,3 @@ const expressionContainer = path

...keys,
...extractTFunction(reference.parentPath, config, disableExtractionIntervals, true).map(k => ({
...extractTFunction(reference.parentPath, config, commentHints, true).map(k => ({
// Add namespace if it was not explicitely set in t() call.

@@ -385,3 +530,3 @@ ...k,

const openingElement = path.get('openingElement');
return openingElement.get('name').referencesImport('react-i18next', 'Trans');
return referencesImport(openingElement.get('name'), 'react-i18next', 'Trans');
}

@@ -391,10 +536,13 @@ /**

* @param path The node path of the JSX Element of the trans component
* @param commentHints Parsed comment hints.
* @returns The parsed i18next options
*/
function parseTransComponentOptions(path) {
let hasCount = false;
let hasContext = false;
let ns = null;
function parseTransComponentOptions(path, commentHints) {
const res = {
contexts: false,
hasCount: false,
ns: null,
};
const countAttr = findJSXAttributeByName(path, 'count');
hasCount = countAttr !== null;
res.hasCount = countAttr !== null;
const tOptionsAttr = findJSXAttributeByName(path, 'tOptions');

@@ -406,3 +554,4 @@ if (tOptionsAttr) {

if (expression.isObjectExpression()) {
hasContext = findKeyInObjectExpression(expression, 'context') !== null;
res.contexts =
findKeyInObjectExpression(expression, 'context') !== null;
}

@@ -416,8 +565,7 @@ }

value = value.get('expression');
ns = getFirstOrNull(evaluateIfConfident(value));
res.ns = getFirstOrNull(evaluateIfConfident(value));
}
return {
hasContext,
hasCount,
ns,
...res,
...parseI18NextOptionsFromCommentHints(path, commentHints),
};

@@ -436,4 +584,4 @@ }

`make the i18nKey attribute evaluable or skip the line using a skip ` +
`comment (/* ${COMMENT_DISABLE_LINE} */ or /* ` +
`${COMMENT_DISABLE_NEXT_LINE} */).`);
`comment (/* ${COMMENT_HINTS_KEYWORDS.DISABLE.LINE} */ or /* ` +
`${COMMENT_HINTS_KEYWORDS.DISABLE.NEXT_LINE} */).`);
const keyAttribute = findJSXAttributeByName(path, 'i18nKey');

@@ -462,3 +610,4 @@ if (!keyAttribute)

`component content evaluable or skip the line using a skip comment ` +
`(/* ${COMMENT_DISABLE_LINE} */ or /* ${COMMENT_DISABLE_NEXT_LINE} */).`);
`(/* ${COMMENT_HINTS_KEYWORDS.DISABLE.LINE} */ or /* ` +
`${COMMENT_HINTS_KEYWORDS.DISABLE.NEXT_LINE} */).`);
let children = path.get('children');

@@ -577,7 +726,6 @@ let result = '';

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
function extractTransComponent(path, config, disableExtractionIntervals = []) {
if (!extractionIsEnabledForPath(path, disableExtractionIntervals))
function extractTransComponent(path, config, commentHints = []) {
if (getCommentHintForPath(path, 'DISABLE', commentHints))
return [];

@@ -588,3 +736,3 @@ if (!isTransComponent(path))

parseTransComponentKeyFromChildren(path);
const parsedOptions = parseTransComponentOptions(path);
const parsedOptions = parseTransComponentOptions(path, commentHints);
return [

@@ -632,5 +780,5 @@ {

* e.g.
* ({'foo', {hasContext: false, hasCount: true}}, 'en')
* ({'foo', {contexts: false, hasCount: true}}, 'en')
* => ['foo', 'foo_plural']
* ({'bar', {hasContext: true, hasCount: true}}, 'en')
* ({'bar', {contexts: ['male', 'female'], hasCount: true}}, 'en')
* => ['foo_male', 'foo_male_plural', 'foo_female', 'foo_female_plural']

@@ -647,7 +795,10 @@ *

let keys = [translationKey];
if (parsedOptions.hasContext) {
if (parsedOptions.contexts !== false) {
// Add all context suffixes
// For instance, if key is "foo", may want
// ["foo", "foo_male", "foo_female"] depending on defaultContexts value.
keys = config.defaultContexts.map(v => {
const contexts = Array.isArray(parsedOptions.contexts)
? parsedOptions.contexts
: config.defaultContexts;
keys = contexts.map(v => {
if (v === '')

@@ -723,3 +874,4 @@ return translationKey;

defaultValue: coalesce(opts.defaultValue, ''),
useKeyAsDefaultValue: coalesce(opts.useKeyAsDefaultValue, false),
keyAsDefaultValue: coalesce(opts.keyAsDefaultValue, false),
keyAsDefaultValueForDerivedKeys: coalesce(opts.keyAsDefaultValueForDerivedKeys, true),
exporterJsonSpace: coalesce(opts.exporterJsonSpace, 2),

@@ -729,2 +881,4 @@ };

const PLUGIN_NAME = 'babel-plugin-i18next-extract';
class ExportError extends Error {

@@ -788,5 +942,8 @@ }

let defaultValue = config.defaultValue;
if (config.useKeyAsDefaultValue === true ||
(Array.isArray(config.useKeyAsDefaultValue) &&
config.useKeyAsDefaultValue.includes(locale))) {
const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true ||
(Array.isArray(config.keyAsDefaultValue) &&
config.keyAsDefaultValue.includes(locale));
const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys;
if (keyAsDefaultValueEnabled &&
(keyAsDefaultValueForDerivedKeys || !k.isDerivedKey)) {
defaultValue = k.cleanKey;

@@ -823,7 +980,7 @@ }

for (const key of keys) {
if (extractedNodes.has(key.nodePath)) {
if (extractedNodes.has(key.nodePath.node)) {
// The node was already extracted. Skip it.
continue;
}
extractedNodes.add(key.nodePath);
extractedNodes.add(key.nodePath.node);
state.I18NextExtract.extractedKeys.push(key);

@@ -850,4 +1007,4 @@ }

handleExtraction(path, state, collect => {
collect(extractUseTranslationHook(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractTFunction(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractUseTranslationHook(path, extractState.config, extractState.commentHints));
collect(extractTFunction(path, extractState.config, extractState.commentHints));
});

@@ -858,4 +1015,4 @@ },

handleExtraction(path, state, collect => {
collect(extractTranslationRenderProp(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractTransComponent(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractTranslationRenderProp(path, extractState.config, extractState.commentHints));
collect(extractTransComponent(path, extractState.config, extractState.commentHints));
});

@@ -871,3 +1028,3 @@ },

extractedKeys: [],
disableExtractionIntervals: [],
commentHints: [],
};

@@ -877,8 +1034,6 @@ },

const extractState = this.I18NextExtract;
if (!extractState.extractedKeys)
if (extractState.extractedKeys.length === 0)
return;
for (const locale of extractState.config.locales) {
// eslint-disable-next-line no-console
console.log(`Exporting locale: ${locale}.`);
const derivedKeys = this.I18NextExtract.extractedKeys.reduce((accumulator, k) => [
const derivedKeys = extractState.extractedKeys.reduce((accumulator, k) => [
...accumulator,

@@ -895,3 +1050,3 @@ ...computeDerivedKeys(k, locale, extractState.config),

if (isFile(path.container)) {
this.I18NextExtract.disableExtractionIntervals = computeCommentDisableIntervals(path.container.comments);
this.I18NextExtract.commentHints = parseCommentHints(path.container.comments);
}

@@ -898,0 +1053,0 @@ path.traverse(Visitor, state);

@@ -10,57 +10,126 @@ 'use strict';

const PLUGIN_NAME = 'babel-plugin-i18next-extract';
const COMMENT_DISABLE_LINE = 'i18next-extract-disable-line';
const COMMENT_DISABLE_NEXT_LINE = 'i18next-extract-disable-next-line';
const COMMENT_DISABLE_SECTION_START = 'i18next-extract-disable';
const COMMENT_DISABLE_SECTION_STOP = 'i18next-extract-enable';
const COMMENT_HINT_PREFIX = 'i18next-extract-';
const COMMENT_HINTS_KEYWORDS = {
DISABLE: {
LINE: COMMENT_HINT_PREFIX + 'disable-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'disable-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'disable',
SECTION_STOP: COMMENT_HINT_PREFIX + 'enable',
},
NAMESPACE: {
LINE: COMMENT_HINT_PREFIX + 'mark-ns-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'mark-ns-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'mark-ns-start',
SECTION_STOP: COMMENT_HINT_PREFIX + 'mark-ns-stop',
},
CONTEXT: {
LINE: COMMENT_HINT_PREFIX + 'mark-context-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'mark-context-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'mark-context-start',
SECTION_STOP: COMMENT_HINT_PREFIX + 'mark-context-stop',
},
PLURAL: {
LINE: COMMENT_HINT_PREFIX + 'mark-plural-line',
NEXT_LINE: COMMENT_HINT_PREFIX + 'mark-plural-next-line',
SECTION_START: COMMENT_HINT_PREFIX + 'mark-plural-start',
SECTION_STOP: COMMENT_HINT_PREFIX + 'mark-plural-stop',
},
};
/**
* Computes line intervals where i18n extraction should be disabled.
* @param comments Babel comments
* @returns sections on which extraction should be disabled
* Given a Babel BaseComment, try to extract a comment hint.
* @param baseComment babel comment
* @returns A comment hint without line interval information.
*/
function computeCommentDisableIntervals(comments) {
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,
};
}
}
}
return null;
}
/**
* Given an array of comment hints, compute their intervals.
* @param commentHints comment hints without line intervals information.
* @returns Comment hints with line interval information.
*/
function computeCommentHintsIntervals(commentHints) {
const result = Array();
let lastDisableLine = null;
for (const { value, loc } of comments) {
const commentWords = value.split(/\s+/);
if (commentWords.includes(COMMENT_DISABLE_LINE)) {
result.push([loc.start.line, loc.start.line]);
for (const commentHint of commentHints) {
if (commentHint.scope === 'LINE') {
result.push({
startLine: commentHint.baseComment.loc.start.line,
stopLine: commentHint.baseComment.loc.start.line,
...commentHint,
});
}
if (commentWords.includes(COMMENT_DISABLE_NEXT_LINE)) {
result.push([loc.end.line + 1, loc.end.line + 1]);
if (commentHint.scope === 'NEXT_LINE') {
result.push({
startLine: commentHint.baseComment.loc.start.line + 1,
stopLine: commentHint.baseComment.loc.start.line + 1,
...commentHint,
});
}
if (commentWords.includes(COMMENT_DISABLE_SECTION_START)) {
lastDisableLine = loc.start.line;
if (commentHint.scope === 'SECTION_START') {
result.push({
startLine: commentHint.baseComment.loc.start.line,
stopLine: Infinity,
...commentHint,
});
}
if (commentWords.includes(COMMENT_DISABLE_SECTION_STOP) &&
lastDisableLine !== null) {
result.push([lastDisableLine, loc.end.line]);
lastDisableLine = null;
if (commentHint.scope === 'SECTION_STOP') {
for (const res of result) {
if (res.type === commentHint.type &&
res.scope === 'SECTION_START' &&
res.stopLine === Infinity) {
res.stopLine = commentHint.baseComment.loc.start.line;
}
}
}
}
if (lastDisableLine !== null) {
result.push([lastDisableLine, Infinity]);
}
return result;
}
/**
* Check if a given number is within any of the given intervals.
* @param num number to check
* @param intervals array of intervals
* @returns true if num is within any of the intervals.
* Given Babel comments, extract the comment hints.
* @param baseComments Babel comments (ordered by line)
*/
function numberIsWithinIntervals(num, intervals) {
return intervals.some(([v0, v1]) => v0 <= num && num <= v1);
function parseCommentHints(baseComments) {
const result = Array();
for (const baseComment of baseComments) {
const commentHint = extractCommentHint(baseComment);
if (commentHint === null) {
continue;
}
result.push(commentHint);
}
return computeCommentHintsIntervals(result);
}
/**
* Check whether extraction is enables for a given path.
* @param path: path to check
* @param disableExtractionIntervals: line intervals where extraction is
* disabled.
* @returns true if the extraction is enabled for the given path.
* Find comment hint of a given type that applies to a Babel node path.
* @param path babel node path
* @param commentHintType Type of comment hint to look for.
* @param commentHints All the comment hints, as returned by parseCommentHints function.
*/
function extractionIsEnabledForPath(path, disableExtractionIntervals) {
return !(path.node.loc &&
numberIsWithinIntervals(path.node.loc.start.line, disableExtractionIntervals));
function getCommentHintForPath(path, commentHintType, commentHints) {
if (!path.node.loc)
return null;
const nodeLine = path.node.loc.start.line;
for (const commentHint of commentHints) {
if (commentHint.type === commentHintType &&
commentHint.startLine <= nodeLine &&
nodeLine <= commentHint.stopLine) {
return commentHint;
}
}
return null;
}

@@ -85,7 +154,67 @@

}
// AST Helpers
/**
* Given comment hints and a path, infer every I18NextOption we can from the comment hints.
* @param path path on which the comment hints should apply
* @param commentHints parsed comment hints
* @returns every parsed option that could be infered.
*/
function parseI18NextOptionsFromCommentHints(path, commentHints) {
const nsCommentHint = getCommentHintForPath(path, 'NAMESPACE', commentHints);
const contextCommentHint = getCommentHintForPath(path, 'CONTEXT', commentHints);
const pluralCommentHint = getCommentHintForPath(path, 'PLURAL', commentHints);
const res = {};
if (nsCommentHint !== null) {
res.ns = nsCommentHint.value;
}
if (contextCommentHint !== null) {
if (['', 'enable'].includes(contextCommentHint.value)) {
res.contexts = true;
}
else if (contextCommentHint.value === 'disable') {
res.contexts = false;
}
else {
try {
let val = JSON.parse(contextCommentHint.value);
if (Array.isArray(val))
res.contexts = val;
else
res.contexts = [contextCommentHint.value];
}
catch (err) {
res.contexts = [contextCommentHint.value];
}
}
}
if (pluralCommentHint !== null) {
if (pluralCommentHint.value === 'disable') {
res.hasCount = false;
}
else {
res.hasCount = true;
}
}
return res;
}
/**
* Improved version of BabelCore `referencesImport` function that also tries to detect wildcard
* imports.
*/
function referencesImport(nodePath, moduleSource, importName) {
if (nodePath.referencesImport(moduleSource, importName))
return true;
if (nodePath.isMemberExpression() || nodePath.isJSXMemberExpression()) {
const obj = nodePath.get('object');
const prop = nodePath.get('property');
if (Array.isArray(prop) ||
(!prop.isIdentifier() && !prop.isJSXIdentifier()))
return false;
return (obj.referencesImport(moduleSource, '*') && prop.node.name === importName);
}
return false;
}
/**
* Evaluates a node path if it can be evaluated with confidence.
*
* @param nodePath: node path to evaluate
* @param path: node path to evaluate
* @returns null if the node path couldn't be evaluated

@@ -193,26 +322,26 @@ */

function parseTCallOptions(path) {
let hasContext = false;
let hasCount = false;
let ns = null;
const res = {
contexts: false,
hasCount: false,
ns: null,
};
if (!path)
return { hasContext, hasCount, ns };
return res;
// Try brutal evaluation first.
const optsEvaluation = evaluateIfConfident(path);
if (optsEvaluation !== null) {
hasContext = 'context' in optsEvaluation;
hasCount = 'count' in optsEvaluation;
res.contexts = 'context' in optsEvaluation;
res.hasCount = 'count' in optsEvaluation;
const evaluatedNamespace = optsEvaluation['ns'];
ns = getFirstOrNull(evaluatedNamespace);
return { hasContext, hasCount, ns };
res.ns = getFirstOrNull(evaluatedNamespace);
}
// It didn't work. Let's try to parse object expression keys.
if (path.isObjectExpression()) {
hasContext = findKeyInObjectExpression(path, 'context') !== null;
hasCount = findKeyInObjectExpression(path, 'count') !== null;
else if (path.isObjectExpression()) {
// It didn't work. Let's try to parse object expression keys.
res.contexts = findKeyInObjectExpression(path, 'context') !== null;
res.hasCount = findKeyInObjectExpression(path, 'count') !== null;
const nsNode = findKeyInObjectExpression(path, 'ns');
const nsNodeEvaluation = evaluateIfConfident(nsNode);
ns = getFirstOrNull(nsNodeEvaluation);
return { hasContext, hasCount, ns };
res.ns = getFirstOrNull(nsNodeEvaluation);
}
throw new ExtractionError("Couldn't evaluate i18next options. Please, provide options as an object expression.");
return res;
}

@@ -223,5 +352,6 @@ /**

* @param path NodePath of the `i18next.t` call.
* @param commentHints parsed comment hints
* @throws ExtractionError when the extraction failed for the `t` call.
*/
function extractTCall(path) {
function extractTCall(path, commentHints) {
const args = path.get('arguments');

@@ -232,8 +362,11 @@ const keyEvaluation = evaluateIfConfident(args[0]);

`evaluable or skip the line using a skip comment (/* ` +
`${COMMENT_DISABLE_LINE} */ or /* ${COMMENT_DISABLE_NEXT_LINE} */).`);
`${COMMENT_HINTS_KEYWORDS.DISABLE.LINE} */ or /* ` +
`${COMMENT_HINTS_KEYWORDS.DISABLE.NEXT_LINE} */).`);
}
const parsedOptions = parseTCallOptions(args[1]);
return {
key: keyEvaluation,
parsedOptions,
parsedOptions: {
...parseTCallOptions(args[1]),
...parseI18NextOptionsFromCommentHints(path, commentHints),
},
nodePath: path,

@@ -248,12 +381,12 @@ };

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is disabled
* @param commentHints: parsed comment hints
* @param skipCheck: set to true if you know that the call expression arguments
* already is a `t` function.
*/
function extractTFunction(path, config, disableExtractionIntervals = [], skipCheck = false) {
if (!extractionIsEnabledForPath(path, disableExtractionIntervals))
function extractTFunction(path, config, commentHints = [], skipCheck = false) {
if (getCommentHintForPath(path, 'DISABLE', commentHints))
return [];
if (!skipCheck && !isSimpleTCall(path, config))
return [];
return [extractTCall(path)];
return [extractTCall(path, commentHints)];
}

@@ -269,3 +402,3 @@

const callee = path.get('callee');
return callee.referencesImport('react-i18next', 'useTranslation');
return referencesImport(callee, 'react-i18next', 'useTranslation');
}

@@ -277,10 +410,18 @@ /**

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
function extractUseTranslationHook(path, config, disableExtractionIntervals = []) {
function extractUseTranslationHook(path, config, commentHints = []) {
if (!isUseTranslationHook(path))
return [];
const namespaceArgument = path.get('arguments')[0];
const ns = getFirstOrNull(evaluateIfConfident(namespaceArgument));
let ns;
const nsCommentHint = getCommentHintForPath(path, '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 = path.get('arguments')[0];
ns = getFirstOrNull(evaluateIfConfident(namespaceArgument));
}
const parentPath = path.parentPath;

@@ -298,3 +439,3 @@ if (!parentPath.isVariableDeclarator())

...keys,
...extractTFunction(reference.parentPath, config, disableExtractionIntervals, true).map(k => ({
...extractTFunction(reference.parentPath, config, commentHints, true).map(k => ({
// Add namespace if it was not explicitely set in t() call.

@@ -320,5 +461,3 @@ ...k,

const openingElement = path.get('openingElement');
return openingElement
.get('name')
.referencesImport('react-i18next', 'Translation');
return referencesImport(openingElement.get('name'), 'react-i18next', 'Translation');
}

@@ -331,17 +470,23 @@ /**

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
function extractTranslationRenderProp(path, config, disableExtractionIntervals = []) {
function extractTranslationRenderProp(path, config, commentHints = []) {
if (!isTranslationRenderProp(path))
return [];
// Try to parse ns property
let ns = null;
const nsAttr = findJSXAttributeByName(path, 'ns');
if (nsAttr) {
let value = nsAttr.get('value');
if (value.isJSXExpressionContainer())
value = value.get('expression');
ns = getFirstOrNull(evaluateIfConfident(value));
let ns;
const nsCommentHint = getCommentHintForPath(path, 'NAMESPACE', commentHints);
if (nsCommentHint) {
// We got a comment hint, take its value as namespace.
ns = nsCommentHint.value;
}
else {
// Try to parse ns property
const nsAttr = findJSXAttributeByName(path, 'ns');
if (nsAttr) {
let value = nsAttr.get('value');
if (value.isJSXExpressionContainer())
value = value.get('expression');
ns = getFirstOrNull(evaluateIfConfident(value));
}
}
// We expect at least "<Translation>{(t) => …}</Translation>

@@ -367,3 +512,3 @@ const expressionContainer = path

...keys,
...extractTFunction(reference.parentPath, config, disableExtractionIntervals, true).map(k => ({
...extractTFunction(reference.parentPath, config, commentHints, true).map(k => ({
// Add namespace if it was not explicitely set in t() call.

@@ -389,3 +534,3 @@ ...k,

const openingElement = path.get('openingElement');
return openingElement.get('name').referencesImport('react-i18next', 'Trans');
return referencesImport(openingElement.get('name'), 'react-i18next', 'Trans');
}

@@ -395,10 +540,13 @@ /**

* @param path The node path of the JSX Element of the trans component
* @param commentHints Parsed comment hints.
* @returns The parsed i18next options
*/
function parseTransComponentOptions(path) {
let hasCount = false;
let hasContext = false;
let ns = null;
function parseTransComponentOptions(path, commentHints) {
const res = {
contexts: false,
hasCount: false,
ns: null,
};
const countAttr = findJSXAttributeByName(path, 'count');
hasCount = countAttr !== null;
res.hasCount = countAttr !== null;
const tOptionsAttr = findJSXAttributeByName(path, 'tOptions');

@@ -410,3 +558,4 @@ if (tOptionsAttr) {

if (expression.isObjectExpression()) {
hasContext = findKeyInObjectExpression(expression, 'context') !== null;
res.contexts =
findKeyInObjectExpression(expression, 'context') !== null;
}

@@ -420,8 +569,7 @@ }

value = value.get('expression');
ns = getFirstOrNull(evaluateIfConfident(value));
res.ns = getFirstOrNull(evaluateIfConfident(value));
}
return {
hasContext,
hasCount,
ns,
...res,
...parseI18NextOptionsFromCommentHints(path, commentHints),
};

@@ -440,4 +588,4 @@ }

`make the i18nKey attribute evaluable or skip the line using a skip ` +
`comment (/* ${COMMENT_DISABLE_LINE} */ or /* ` +
`${COMMENT_DISABLE_NEXT_LINE} */).`);
`comment (/* ${COMMENT_HINTS_KEYWORDS.DISABLE.LINE} */ or /* ` +
`${COMMENT_HINTS_KEYWORDS.DISABLE.NEXT_LINE} */).`);
const keyAttribute = findJSXAttributeByName(path, 'i18nKey');

@@ -466,3 +614,4 @@ if (!keyAttribute)

`component content evaluable or skip the line using a skip comment ` +
`(/* ${COMMENT_DISABLE_LINE} */ or /* ${COMMENT_DISABLE_NEXT_LINE} */).`);
`(/* ${COMMENT_HINTS_KEYWORDS.DISABLE.LINE} */ or /* ` +
`${COMMENT_HINTS_KEYWORDS.DISABLE.NEXT_LINE} */).`);
let children = path.get('children');

@@ -581,7 +730,6 @@ let result = '';

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
function extractTransComponent(path, config, disableExtractionIntervals = []) {
if (!extractionIsEnabledForPath(path, disableExtractionIntervals))
function extractTransComponent(path, config, commentHints = []) {
if (getCommentHintForPath(path, 'DISABLE', commentHints))
return [];

@@ -592,3 +740,3 @@ if (!isTransComponent(path))

parseTransComponentKeyFromChildren(path);
const parsedOptions = parseTransComponentOptions(path);
const parsedOptions = parseTransComponentOptions(path, commentHints);
return [

@@ -636,5 +784,5 @@ {

* e.g.
* ({'foo', {hasContext: false, hasCount: true}}, 'en')
* ({'foo', {contexts: false, hasCount: true}}, 'en')
* => ['foo', 'foo_plural']
* ({'bar', {hasContext: true, hasCount: true}}, 'en')
* ({'bar', {contexts: ['male', 'female'], hasCount: true}}, 'en')
* => ['foo_male', 'foo_male_plural', 'foo_female', 'foo_female_plural']

@@ -651,7 +799,10 @@ *

let keys = [translationKey];
if (parsedOptions.hasContext) {
if (parsedOptions.contexts !== false) {
// Add all context suffixes
// For instance, if key is "foo", may want
// ["foo", "foo_male", "foo_female"] depending on defaultContexts value.
keys = config.defaultContexts.map(v => {
const contexts = Array.isArray(parsedOptions.contexts)
? parsedOptions.contexts
: config.defaultContexts;
keys = contexts.map(v => {
if (v === '')

@@ -727,3 +878,4 @@ return translationKey;

defaultValue: coalesce(opts.defaultValue, ''),
useKeyAsDefaultValue: coalesce(opts.useKeyAsDefaultValue, false),
keyAsDefaultValue: coalesce(opts.keyAsDefaultValue, false),
keyAsDefaultValueForDerivedKeys: coalesce(opts.keyAsDefaultValueForDerivedKeys, true),
exporterJsonSpace: coalesce(opts.exporterJsonSpace, 2),

@@ -733,2 +885,4 @@ };

const PLUGIN_NAME = 'babel-plugin-i18next-extract';
class ExportError extends Error {

@@ -792,5 +946,8 @@ }

let defaultValue = config.defaultValue;
if (config.useKeyAsDefaultValue === true ||
(Array.isArray(config.useKeyAsDefaultValue) &&
config.useKeyAsDefaultValue.includes(locale))) {
const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true ||
(Array.isArray(config.keyAsDefaultValue) &&
config.keyAsDefaultValue.includes(locale));
const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys;
if (keyAsDefaultValueEnabled &&
(keyAsDefaultValueForDerivedKeys || !k.isDerivedKey)) {
defaultValue = k.cleanKey;

@@ -827,7 +984,7 @@ }

for (const key of keys) {
if (extractedNodes.has(key.nodePath)) {
if (extractedNodes.has(key.nodePath.node)) {
// The node was already extracted. Skip it.
continue;
}
extractedNodes.add(key.nodePath);
extractedNodes.add(key.nodePath.node);
state.I18NextExtract.extractedKeys.push(key);

@@ -854,4 +1011,4 @@ }

handleExtraction(path, state, collect => {
collect(extractUseTranslationHook(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractTFunction(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractUseTranslationHook(path, extractState.config, extractState.commentHints));
collect(extractTFunction(path, extractState.config, extractState.commentHints));
});

@@ -862,4 +1019,4 @@ },

handleExtraction(path, state, collect => {
collect(extractTranslationRenderProp(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractTransComponent(path, extractState.config, extractState.disableExtractionIntervals));
collect(extractTranslationRenderProp(path, extractState.config, extractState.commentHints));
collect(extractTransComponent(path, extractState.config, extractState.commentHints));
});

@@ -875,3 +1032,3 @@ },

extractedKeys: [],
disableExtractionIntervals: [],
commentHints: [],
};

@@ -881,8 +1038,6 @@ },

const extractState = this.I18NextExtract;
if (!extractState.extractedKeys)
if (extractState.extractedKeys.length === 0)
return;
for (const locale of extractState.config.locales) {
// eslint-disable-next-line no-console
console.log(`Exporting locale: ${locale}.`);
const derivedKeys = this.I18NextExtract.extractedKeys.reduce((accumulator, k) => [
const derivedKeys = extractState.extractedKeys.reduce((accumulator, k) => [
...accumulator,

@@ -899,3 +1054,3 @@ ...computeDerivedKeys(k, locale, extractState.config),

if (BabelTypes.isFile(path.container)) {
this.I18NextExtract.disableExtractionIntervals = computeCommentDisableIntervals(path.container.comments);
this.I18NextExtract.commentHints = parseCommentHints(path.container.comments);
}

@@ -902,0 +1057,0 @@ path.traverse(Visitor, state);

import * as BabelTypes from '@babel/types';
import * as BabelCore from '@babel/core';
declare type CommentHintType = 'DISABLE' | 'NAMESPACE' | 'CONTEXT' | 'PLURAL';
declare type CommentHintScope = 'LINE' | 'NEXT_LINE' | 'SECTION_START' | 'SECTION_STOP';
/**
* Computes line intervals where i18n extraction should be disabled.
* @param comments Babel comments
* @returns sections on which extraction should be disabled
* Comment Hint without line location information.
*/
export declare function computeCommentDisableIntervals(comments: BabelTypes.BaseComment[]): [number, number][];
interface BaseCommentHint {
type: CommentHintType;
scope: CommentHintScope;
value: string;
baseComment: BabelTypes.BaseComment;
}
/**
* Check whether extraction is enables for a given path.
* @param path: path to check
* @param disableExtractionIntervals: line intervals where extraction is
* disabled.
* @returns true if the extraction is enabled for the given path.
* Line intervals
*/
export declare function extractionIsEnabledForPath(path: BabelCore.NodePath, disableExtractionIntervals: [number, number][]): boolean;
interface Interval {
startLine: number;
stopLine: number;
}
/**
* Comment Hint with line intervals information.
*/
export interface CommentHint extends BaseCommentHint, Interval {
}
export declare const COMMENT_HINT_PREFIX = "i18next-extract-";
export declare const COMMENT_HINTS_KEYWORDS: {
[k in CommentHintType]: {
[s in CommentHintScope]: string;
};
};
/**
* Given Babel comments, extract the comment hints.
* @param baseComments Babel comments (ordered by line)
*/
export declare function parseCommentHints(baseComments: BabelTypes.BaseComment[]): CommentHint[];
/**
* Find comment hint of a given type that applies to a Babel node path.
* @param path babel node path
* @param commentHintType Type of comment hint to look for.
* @param commentHints All the comment hints, as returned by parseCommentHints function.
*/
export declare function getCommentHintForPath(path: BabelCore.NodePath, commentHintType: BaseCommentHint['type'], commentHints: CommentHint[]): CommentHint | null;
export {};

@@ -12,3 +12,4 @@ export interface Config {

defaultValue: string | null;
useKeyAsDefaultValue: boolean | string[];
keyAsDefaultValue: boolean | string[];
keyAsDefaultValueForDerivedKeys: boolean;
exporterJsonSpace: string | number;

@@ -15,0 +16,0 @@ }

export declare const PLUGIN_NAME = "babel-plugin-i18next-extract";
export declare const COMMENT_DISABLE_LINE = "i18next-extract-disable-line";
export declare const COMMENT_DISABLE_NEXT_LINE = "i18next-extract-disable-next-line";
export declare const COMMENT_DISABLE_SECTION_START = "i18next-extract-disable";
export declare const COMMENT_DISABLE_SECTION_STOP = "i18next-extract-enable";
import * as BabelCore from '@babel/core';
import * as BabelTypes from '@babel/types';
import { CommentHint } from '../comments';
import { ExtractedKey } from '../keys';
/**

@@ -17,5 +19,17 @@ * Error thrown in case extraction of a node failed.

/**
* Given comment hints and a path, infer every I18NextOption we can from the comment hints.
* @param path path on which the comment hints should apply
* @param commentHints parsed comment hints
* @returns every parsed option that could be infered.
*/
export declare function parseI18NextOptionsFromCommentHints(path: BabelCore.NodePath, commentHints: CommentHint[]): Partial<ExtractedKey['parsedOptions']>;
/**
* Improved version of BabelCore `referencesImport` function that also tries to detect wildcard
* imports.
*/
export declare function referencesImport(nodePath: BabelCore.NodePath, moduleSource: string, importName: string): boolean;
/**
* Evaluates a node path if it can be evaluated with confidence.
*
* @param nodePath: node path to evaluate
* @param path: node path to evaluate
* @returns null if the node path couldn't be evaluated

@@ -22,0 +36,0 @@ */

import * as BabelTypes from '@babel/types';
import * as BabelCore from '@babel/core';
import { CommentHint } from '../comments';
import { ExtractedKey } from '../keys';

@@ -11,6 +12,6 @@ import { Config } from '../config';

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is disabled
* @param commentHints: parsed comment hints
* @param skipCheck: set to true if you know that the call expression arguments
* already is a `t` function.
*/
export default function extractTFunction(path: BabelCore.NodePath<BabelTypes.CallExpression>, config: Config, disableExtractionIntervals?: [number, number][], skipCheck?: boolean): ExtractedKey[];
export default function extractTFunction(path: BabelCore.NodePath<BabelTypes.CallExpression>, config: Config, commentHints?: CommentHint[], skipCheck?: boolean): ExtractedKey[];
import * as BabelTypes from '@babel/types';
import * as BabelCore from '@babel/core';
import { CommentHint } from '../comments';
import { ExtractedKey } from '../keys';

@@ -11,5 +12,4 @@ import { Config } from '../config';

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
export default function extractTransComponent(path: BabelCore.NodePath<BabelTypes.JSXElement>, config: Config, disableExtractionIntervals?: [number, number][]): ExtractedKey[];
export default function extractTransComponent(path: BabelCore.NodePath<BabelTypes.JSXElement>, config: Config, commentHints?: CommentHint[]): ExtractedKey[];

@@ -5,2 +5,3 @@ import * as BabelTypes from '@babel/types';

import { Config } from '../config';
import { CommentHint } from '../comments';
/**

@@ -12,5 +13,4 @@ * Parse `Translation` render prop to extract all its translation keys and

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
export default function extractTranslationRenderProp(path: BabelCore.NodePath<BabelTypes.JSXElement>, config: Config, disableExtractionIntervals?: [number, number][]): ExtractedKey[];
export default function extractTranslationRenderProp(path: BabelCore.NodePath<BabelTypes.JSXElement>, config: Config, commentHints?: CommentHint[]): ExtractedKey[];

@@ -5,2 +5,3 @@ import * as BabelTypes from '@babel/types';

import { Config } from '../config';
import { CommentHint } from '../comments';
/**

@@ -11,5 +12,4 @@ * Parse `useTranslation()` hook to extract all its translation keys and

* @param config: plugin configuration
* @param disableExtractionIntervals: interval of lines where extraction is
* disabled
* @param commentHints: parsed comment hints
*/
export default function extractUseTranslationHook(path: BabelCore.NodePath<BabelTypes.CallExpression>, config: Config, disableExtractionIntervals?: [number, number][]): ExtractedKey[];
export default function extractUseTranslationHook(path: BabelCore.NodePath<BabelTypes.CallExpression>, config: Config, commentHints?: CommentHint[]): ExtractedKey[];
import * as BabelCore from '@babel/core';
import { Config } from './config';
interface I18NextParsedOptions {
hasContext: boolean;
contexts: string[] | boolean;
hasCount: boolean;

@@ -29,5 +29,5 @@ ns: string | null;

* e.g.
* ({'foo', {hasContext: false, hasCount: true}}, 'en')
* ({'foo', {contexts: false, hasCount: true}}, 'en')
* => ['foo', 'foo_plural']
* ({'bar', {hasContext: true, hasCount: true}}, 'en')
* ({'bar', {contexts: ['male', 'female'], hasCount: true}}, 'en')
* => ['foo_male', 'foo_male_plural', 'foo_female', 'foo_female_plural']

@@ -34,0 +34,0 @@ *

import * as BabelCore from '@babel/core';
import { CommentHint } from './comments';
import { ExtractedKey } from './keys';

@@ -11,3 +12,3 @@ import { Config } from './config';

extractedKeys: ExtractedKey[];
disableExtractionIntervals: [number, number][];
commentHints: CommentHint[];
config: Config;

@@ -14,0 +15,0 @@ }

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

@@ -47,3 +47,3 @@ "repository": {

"@babel/preset-typescript": "^7.3.3",
"@types/fs-extra": "^7.0.0",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^24.0.15",

@@ -50,0 +50,0 @@ "@types/jest-expect-message": "^1.0.0",

@@ -18,4 +18,2 @@ # babel-plugin-i18next-extract

Just install the plugin from npm:
```bash

@@ -31,3 +29,3 @@ yarn add --dev babel-plugin-i18next-extract

If you already use [Babel](https://babeljs.io), chances are you already have an existing babel
If you already use [Babel](https://babeljs.io), chances are you already have an babel
configuration (e.g. a `.babelrc` file). Just add declare the plugin and you're good to go:

@@ -44,6 +42,4 @@

> To work properly, the plugin must run **before** any JSX transformation step.
You can also specify additional [configuration options](#configuration) to the plugin:
You can pass additional options to the plugin by declaring it as follow:
```javascript

@@ -58,25 +54,69 @@ {

Once you are set up, you can build your app normally or run Babel through [Babel CLI](
https://babeljs.io/docs/en/babel-cli):
```bash
yarn run babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'
# or
npm run babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'
```
Extracted translations should land in the `extractedTranslations/` directory. Magic huh?
If you don't have a babel configuration yet, you can follow the [Configure Babel](
https://babeljs.io/docs/en/configuration) documentation to try setting it up.
You can then just build your app normally or run Babel through [Babel CLI](
https://babeljs.io/docs/en/babel-cli):
## Usage with create-react-app
[create-react-app](https://github.com/facebook/create-react-app) doesn't let you modify the babel
configuration. Fortunately, it's still possible to use this plugin without ejecting. First of all,
install Babel CLI:
```bash
yarn run babel -f .babelrc 'src/**/*'
yarn add --dev @babel/cli
# or
npm run babel -f .babelrc 'src/**/*'
npm add --save-dev @babel/cli
```
You should then be able to see the extracted translations in the `extractedTranslations/`
directory. Magic huh? Next step is to check out all the available [configuration options
](#configuration).
Create a minimal `.babelrc` that uses the `react-app` babel preset (DO NOT install it, it's already
shipped with CRA):
## Usage with create-react-app
```javascript
{
"presets": ["react-app"],
"plugins": ["i18next-extract"]
}
```
TODO: It should be enough to use babel-preset-react-app and declare the plugin in babelrc,
but I have to check this out.
You should then be able to extract your translations using the CLI:
```bash
# NODE_ENV must be specified for react-app preset to work properly
NODE_ENV=development yarn run babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'
```
To simplify the extraction, you can add a script to your `package.json`:
```javascript
"scripts": {
[…]
"i18n-extract": "NODE_ENV=development babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'",
[…]
}
```
And then just run:
```bash
yarn run i18n-extract
# or
npm run i18n-extract
```
## Features

@@ -90,3 +130,3 @@

- [x] Automatic detection from `react-i18next` properties.
- [ ] (todo) Manual detection from comment hints.
- [x] Manual detection from [comment hints](#comment-hints).
- [x] Contexts support:

@@ -96,3 +136,3 @@ - [x] Naïve implementation with default contexts.

- [x] Automatic detection from `react-i18next` properties.
- [ ] (todo) Manual detection from comment hints.
- [x] Manual detection from [comment hints](#comment-hints).
- [x] [react-i18next](https://react.i18next.com/) support:

@@ -108,4 +148,4 @@ - [x] `Trans` component support (with plural forms, contexts and namespaces).

- [x] Depending on the `ns` attribute in the `Trans` component.
- [x] Explicitely disable extraction on a specific file sections or lines using comment hints.
- [x] … and more?
- [x] Explicitely disable extraction on a specific file sections or lines using [comment hints](#comment-hints).
- [ ] [… and more?](./CONTRIBUTING.md)

@@ -126,3 +166,4 @@ ## Configuration

| defaultValue | `string` or `null` | Default value for extracted keys. | `''` (empty string) |
| useKeyAsDefaultValue | `boolean` or `string[]` | If true, use the extracted key as defaultValue (ignoring `defaultValue` option). You can also specify an array of locales to apply this behavior only to a specific set locales (e.g. if you keys are in plain english, you may want to set this option to `['en']`). | `false` |
| keyAsDefaultValue | `boolean` or `string[]` | If true, use the extracted key as defaultValue (ignoring `defaultValue` option). You can also specify an array of locales to apply this behavior only to a specific set locales (e.g. if you keys are in plain english, you may want to set this option to `['en']`). | `false` |
| keyAsDefaultValueForDerivedKeys | `boolean` | If false and `keyAsDefaultValue` is enabled, don't use derived keys (plural forms or contexts) as default value. `defaultValue` will be used instead. | `true` |
| exporterJsonSpace | `number` | Number of indentation space to use in extracted JSON files. | 2 |

@@ -132,2 +173,4 @@

### Disable extraction on a specific section
If the plugin extracts a key you want to skip or erroneously tries to parse a function that doesn't

@@ -150,8 +193,55 @@ belong to i18next, you can use a comment hint to disable the extraction:

Notice you can put a `// i18next-extract-disable` comment at the top of the file in order to
disable extraction on the entire file.
You can put a `// i18next-extract-disable` comment at the top of the file in order to disable
extraction on the entire file.
Comment hints may also be used in the future to explicitly mark keys as having plural forms or
contexts (and specify which ones), or to specify a namespace. Stay tuned.
### Explicitly specify contexts for a key
This is very useful if you want to use different contexts than the default `male` and `female`
for a given key:
```javascript
// i18next-extract-mark-context-next-line ["dog", "cat"]
i18next.t("this key will have dog and cat context", {context: dogOrCat})
// i18next-extract-mark-context-next-line
i18next.t("this key will have default context, although no context is specified")
// i18next-extract-mark-context-next-line disable
i18next.t("this key wont have a context, although a context is specified", {context})
i18next.t("can be used on line") // i18next-extract-mark-context-line
// i18next-extract-mark-context-start
i18next.t("or on sections")
// i18next-extract-mark-context-stop
const transComponent = (
// i18next-extract-mark-context-next-line
<Trans>it also works on Trans components</Trans>
)
```
### Explicitly use a namespace for a key
```javascript
// i18next-extract-mark-ns-next-line forced-ns
i18next.t("this key will be in forced-ns namespace")
i18next.t("this one also", {ns: 'this-ns-wont-be-used'}) // i18next-extract-mark-ns-line forced-ns
// i18next-extract-mark-ns-start forced-ns
i18next.t("and still this one")
// i18next-extract-mark-ns-stop forced-ns
```
### Explicitly enable/disable a plural form for a key
```javascript
// i18next-extract-mark-plural-next-line
i18next.t("this key will be in forced in plural form")
// i18next-extract-mark-plural-next-line disable
i18next.t("this key wont have plural form", {count})
```
## Gotchas

@@ -158,0 +248,0 @@

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