@@ -317,6 +622,8 @@ export default function MyComponent() {
await fs.writeFile(
plugin1File3,
`
+import {translate} from '@docusaurus/Translate';
+
export default function MyComponent() {
return (
@@ -334,6 +641,8 @@ export default function MyComponent() {
await fs.writeFile(
plugin2File,
`
+import Translate, {translate} from '@docusaurus/Translate';
+
type Props = {hey: string};
export default function MyComponent(props: Props) {
diff --git a/packages/docusaurus/src/server/translations/translationsExtractor.ts b/packages/docusaurus/src/server/translations/translationsExtractor.ts
index ba55eb82f3..c1f4f81d6a 100644
--- a/packages/docusaurus/src/server/translations/translationsExtractor.ts
+++ b/packages/docusaurus/src/server/translations/translationsExtractor.ts
@@ -183,157 +183,191 @@ function extractSourceCodeAstTranslations(
sourceCodeFilePath: string,
): SourceCodeFileTranslations {
function sourceWarningPart(node: Node) {
- return `File: ${sourceCodeFilePath} at ${
- node.loc?.start.line
- } line\nFull code: ${generate(node).code}`;
+ return `File: ${sourceCodeFilePath} at line ${node.loc?.start.line}
+Full code: ${generate(node).code}`;
}
const translations: Record = {};
const warnings: string[] = [];
+ let translateComponentName: string | undefined;
+ let translateFunctionName: string | undefined;
- // TODO we should check the presence of the correct @docusaurus imports here!
-
+ // First pass: find import declarations of Translate / translate.
+ // If not found, don't process the rest to avoid false positives
traverse(ast, {
- JSXElement(path) {
+ ImportDeclaration(path) {
if (
- !path
- .get('openingElement')
- .get('name')
- .isJSXIdentifier({name: 'Translate'})
+ path.node.importKind === 'type' ||
+ path.get('source').node.value !== '@docusaurus/Translate'
) {
return;
}
- function evaluateJSXProp(propName: string): string | undefined {
- const attributePath = path
- .get('openingElement.attributes')
- .find(
- (attr) =>
- attr.isJSXAttribute() &&
- (attr as NodePath)
- .get('name')
- .isJSXIdentifier({name: propName}),
- );
+ const importSpecifiers = path.get('specifiers');
+ const defaultImport = importSpecifiers.find(
+ (specifier): specifier is NodePath =>
+ specifier.node.type === 'ImportDefaultSpecifier',
+ );
+ const callbackImport = importSpecifiers.find(
+ (specifier): specifier is NodePath =>
+ specifier.node.type === 'ImportSpecifier' &&
+ ((
+ (specifier as NodePath).get('imported')
+ .node as t.Identifier
+ ).name === 'translate' ||
+ (
+ (specifier as NodePath).get('imported')
+ .node as t.StringLiteral
+ ).value === 'translate'),
+ );
- if (attributePath) {
- const attributeValue = attributePath.get('value') as NodePath;
-
- const attributeValueEvaluated =
- attributeValue.isJSXExpressionContainer()
- ? (attributeValue.get('expression') as NodePath).evaluate()
- : attributeValue.evaluate();
-
- if (
- attributeValueEvaluated.confident &&
- typeof attributeValueEvaluated.value === 'string'
- ) {
- return attributeValueEvaluated.value;
- } else {
- warnings.push(
- ` prop=${propName} should be a statically evaluable object.\nExample: Message\nDynamically constructed values are not allowed, because they prevent translations to be extracted.\n${sourceWarningPart(
- path.node,
- )}`,
- );
- }
- }
-
- return undefined;
- }
-
- const id = evaluateJSXProp('id');
- const description = evaluateJSXProp('description');
- let message;
- const childrenPath = path.get('children');
-
- // Handle empty content
- if (!childrenPath.length) {
- if (!id) {
- warnings.push(`
- without children must have id prop.\nExample: \n${sourceWarningPart(
- path.node,
- )}
- `);
- } else {
- translations[id] = {
- message: message ?? id,
- ...(description && {description}),
- };
- }
-
- return;
- }
-
- // Handle single non-empty content
- const singleChildren = childrenPath
- // Remove empty/useless text nodes that might be around our translation!
- // Makes the translation system more reliable to JSX formatting issues
- .filter(
- (children) =>
- !(
- children.isJSXText() &&
- children.node.value.replace('\n', '').trim() === ''
- ),
- )
- .pop();
- const isJSXText = singleChildren && singleChildren.isJSXText();
- const isJSXExpressionContainer =
- singleChildren &&
- singleChildren.isJSXExpressionContainer() &&
- (singleChildren.get('expression') as NodePath).evaluate().confident;
-
- if (isJSXText || isJSXExpressionContainer) {
- message = isJSXText
- ? singleChildren.node.value.trim().replace(/\s+/g, ' ')
- : (singleChildren.get('expression') as NodePath).evaluate().value;
-
- translations[id ?? message] = {
- message,
- ...(description && {description}),
- };
- } else {
- warnings.push(
- `Translate content could not be extracted. It has to be a static string and use optional but static props, like text.\n${sourceWarningPart(
- path.node,
- )}`,
- );
- }
+ translateComponentName = defaultImport?.get('local').node.name;
+ translateFunctionName = callbackImport?.get('local').node.name;
},
+ });
- CallExpression(path) {
- if (!path.get('callee').isIdentifier({name: 'translate'})) {
- return;
- }
-
- const args = path.get('arguments');
- if (args.length === 1 || args.length === 2) {
- const firstArgPath = args[0];
-
- // evaluation allows translate("x" + "y"); to be considered as translate("xy");
- const firstArgEvaluated = firstArgPath.evaluate();
-
+ traverse(ast, {
+ ...(translateComponentName && {
+ JSXElement(path) {
if (
- firstArgEvaluated.confident &&
- typeof firstArgEvaluated.value === 'object'
+ !path
+ .get('openingElement')
+ .get('name')
+ .isJSXIdentifier({name: translateComponentName})
) {
- const {message, id, description} = firstArgEvaluated.value;
+ return;
+ }
+ function evaluateJSXProp(propName: string): string | undefined {
+ const attributePath = path
+ .get('openingElement.attributes')
+ .find(
+ (attr) =>
+ attr.isJSXAttribute() &&
+ (attr as NodePath)
+ .get('name')
+ .isJSXIdentifier({name: propName}),
+ );
+
+ if (attributePath) {
+ const attributeValue = attributePath.get('value') as NodePath;
+
+ const attributeValueEvaluated =
+ attributeValue.isJSXExpressionContainer()
+ ? (attributeValue.get('expression') as NodePath).evaluate()
+ : attributeValue.evaluate();
+
+ if (
+ attributeValueEvaluated.confident &&
+ typeof attributeValueEvaluated.value === 'string'
+ ) {
+ return attributeValueEvaluated.value;
+ } else {
+ warnings.push(
+ ` prop=${propName} should be a statically evaluable object.
+Example: Message
+Dynamically constructed values are not allowed, because they prevent translations to be extracted.
+${sourceWarningPart(path.node)}`,
+ );
+ }
+ }
+
+ return undefined;
+ }
+
+ const id = evaluateJSXProp('id');
+ const description = evaluateJSXProp('description');
+ let message;
+ const childrenPath = path.get('children');
+
+ // Handle empty content
+ if (!childrenPath.length) {
+ if (!id) {
+ warnings.push(` without children must have id prop.
+Example:
+${sourceWarningPart(path.node)}`);
+ } else {
+ translations[id] = {
+ message: message ?? id,
+ ...(description && {description}),
+ };
+ }
+
+ return;
+ }
+
+ // Handle single non-empty content
+ const singleChildren = childrenPath
+ // Remove empty/useless text nodes that might be around our translation!
+ // Makes the translation system more reliable to JSX formatting issues
+ .filter(
+ (children) =>
+ !(
+ children.isJSXText() &&
+ children.node.value.replace('\n', '').trim() === ''
+ ),
+ )
+ .pop();
+ const isJSXText = singleChildren && singleChildren.isJSXText();
+ const isJSXExpressionContainer =
+ singleChildren &&
+ singleChildren.isJSXExpressionContainer() &&
+ (singleChildren.get('expression') as NodePath).evaluate().confident;
+
+ if (isJSXText || isJSXExpressionContainer) {
+ message = isJSXText
+ ? singleChildren.node.value.trim().replace(/\s+/g, ' ')
+ : (singleChildren.get('expression') as NodePath).evaluate().value;
+
translations[id ?? message] = {
- message: message ?? id,
+ message,
...(description && {description}),
};
} else {
warnings.push(
- `translate() first arg should be a statically evaluable object.\nExample: translate({message: "text",id: "optional.id",description: "optional description"}\nDynamically constructed values are not allowed, because they prevent translations to be extracted.\n${sourceWarningPart(
- path.node,
- )}`,
+ `Translate content could not be extracted. It has to be a static string and use optional but static props, like text.
+${sourceWarningPart(path.node)}`,
);
}
- } else {
- warnings.push(
- `translate() function only takes 1 or 2 args\n${sourceWarningPart(
- path.node,
- )}`,
- );
- }
- },
+ },
+ }),
+
+ ...(translateFunctionName && {
+ CallExpression(path) {
+ if (!path.get('callee').isIdentifier({name: translateFunctionName})) {
+ return;
+ }
+
+ const args = path.get('arguments');
+ if (args.length === 1 || args.length === 2) {
+ const firstArgPath = args[0];
+
+ // evaluation allows translate("x" + "y"); to be considered as translate("xy");
+ const firstArgEvaluated = firstArgPath.evaluate();
+
+ if (
+ firstArgEvaluated.confident &&
+ typeof firstArgEvaluated.value === 'object'
+ ) {
+ const {message, id, description} = firstArgEvaluated.value;
+ translations[id ?? message] = {
+ message: message ?? id,
+ ...(description && {description}),
+ };
+ } else {
+ warnings.push(
+ `translate() first arg should be a statically evaluable object.
+Example: translate({message: "text",id: "optional.id",description: "optional description"}
+Dynamically constructed values are not allowed, because they prevent translations to be extracted.
+${sourceWarningPart(path.node)}`,
+ );
+ }
+ } else {
+ warnings.push(
+ `translate() function only takes 1 or 2 args
+${sourceWarningPart(path.node)}`,
+ );
+ }
+ },
+ }),
});
return {sourceCodeFilePath, translations, warnings};