mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-07 14:17:16 +02:00
feat(core): check imported API name when extracting translations (#6405)
This commit is contained in:
parent
2cace21083
commit
71b6ae2fbf
2 changed files with 474 additions and 131 deletions
|
@ -88,6 +88,8 @@ const unrelated = 42;
|
||||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
extension: 'js',
|
extension: 'js',
|
||||||
content: `
|
content: `
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
export default function MyComponent() {
|
export default function MyComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -119,6 +121,8 @@ export default function MyComponent() {
|
||||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
extension: 'js',
|
extension: 'js',
|
||||||
content: `
|
content: `
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
|
||||||
export default function MyComponent() {
|
export default function MyComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -126,7 +130,7 @@ export default function MyComponent() {
|
||||||
code message
|
code message
|
||||||
</Translate>
|
</Translate>
|
||||||
|
|
||||||
<Translate id="codeId1" />
|
<Translate id="codeId1" description="description 2" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -142,7 +146,7 @@ export default function MyComponent() {
|
||||||
sourceCodeFilePath,
|
sourceCodeFilePath,
|
||||||
translations: {
|
translations: {
|
||||||
codeId: {message: 'code message', description: 'code description'},
|
codeId: {message: 'code message', description: 'code description'},
|
||||||
codeId1: {message: 'codeId1'},
|
codeId1: {message: 'codeId1', description: 'description 2'},
|
||||||
},
|
},
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
|
@ -152,6 +156,8 @@ export default function MyComponent() {
|
||||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
extension: 'js',
|
extension: 'js',
|
||||||
content: `
|
content: `
|
||||||
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
const prefix = "prefix ";
|
const prefix = "prefix ";
|
||||||
|
|
||||||
export default function MyComponent() {
|
export default function MyComponent() {
|
||||||
|
@ -211,6 +217,8 @@ export default function MyComponent() {
|
||||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
extension: 'tsx',
|
extension: 'tsx',
|
||||||
content: `
|
content: `
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
type ComponentProps<T> = {toto: string}
|
type ComponentProps<T> = {toto: string}
|
||||||
|
|
||||||
export default function MyComponent<T>(props: ComponentProps<T>) {
|
export default function MyComponent<T>(props: ComponentProps<T>) {
|
||||||
|
@ -236,6 +244,297 @@ export default function MyComponent<T>(props: ComponentProps<T>) {
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('do not extract from functions that is not docusaurus provided', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import translate from 'a-lib';
|
||||||
|
|
||||||
|
export default function somethingElse() {
|
||||||
|
const a = translate('foo');
|
||||||
|
return <Translate>bar</Translate>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {},
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('do not extract from functions that is internal', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
function translate() {
|
||||||
|
return 'foo'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function somethingElse() {
|
||||||
|
const a = translate('foo');
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {},
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize aliased imports', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import Foo, {translate as bar} from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Foo id="codeId" description={"code description"}>
|
||||||
|
code message
|
||||||
|
</Foo>
|
||||||
|
|
||||||
|
<Translate id="codeId1" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
|
||||||
|
|
||||||
|
<input text={bar({id: 'codeId1'})}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {
|
||||||
|
codeId: {
|
||||||
|
description: 'code description',
|
||||||
|
message: 'code message',
|
||||||
|
},
|
||||||
|
codeId1: {
|
||||||
|
message: 'codeId1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize aliased imports as string literal', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import {'translate' as bar} from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
|
||||||
|
|
||||||
|
<input text={bar({id: 'codeId1'})}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {
|
||||||
|
codeId1: {
|
||||||
|
message: 'codeId1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warn about id if no children', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<Translate description="foo" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {},
|
||||||
|
warnings: [
|
||||||
|
`<Translate> without children must have id prop.
|
||||||
|
Example: <Translate id="my-id" />
|
||||||
|
File: ${sourceCodeFilePath} at line 6
|
||||||
|
Full code: <Translate description="foo" />`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warn about dynamic id', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<Translate id={index}>foo</Translate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {
|
||||||
|
foo: {
|
||||||
|
message: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warnings: [
|
||||||
|
`<Translate> prop=id should be a statically evaluable object.
|
||||||
|
Example: <Translate id="optional id" description="optional description">Message</Translate>
|
||||||
|
Dynamically constructed values are not allowed, because they prevent translations to be extracted.
|
||||||
|
File: ${sourceCodeFilePath} at line 6
|
||||||
|
Full code: <Translate id={index}>foo</Translate>`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warn about dynamic children', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<Translate id='foo'><a>hhh</a></Translate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {},
|
||||||
|
warnings: [
|
||||||
|
`Translate content could not be extracted. It has to be a static string and use optional but static props, like <Translate id="my-id" description="my-description">text</Translate>.
|
||||||
|
File: ${sourceCodeFilePath} at line 6
|
||||||
|
Full code: <Translate id='foo'><a>hhh</a></Translate>`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warn about dynamic translate argument', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
translate(foo);
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {},
|
||||||
|
warnings: [
|
||||||
|
`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.
|
||||||
|
File: ${sourceCodeFilePath} at line 4
|
||||||
|
Full code: translate(foo)`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warn about too many arguments', async () => {
|
||||||
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
|
extension: 'js',
|
||||||
|
content: `
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
|
translate({message: 'a'}, {a: 1}, 2);
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||||
|
sourceCodeFilePath,
|
||||||
|
TestBabelOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceCodeFileTranslations).toEqual({
|
||||||
|
sourceCodeFilePath,
|
||||||
|
translations: {},
|
||||||
|
warnings: [
|
||||||
|
`translate() function only takes 1 or 2 args
|
||||||
|
File: ${sourceCodeFilePath} at line 4
|
||||||
|
Full code: translate({
|
||||||
|
message: 'a'
|
||||||
|
}, {
|
||||||
|
a: 1
|
||||||
|
}, 2)`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('extractSiteSourceCodeTranslations', () => {
|
describe('extractSiteSourceCodeTranslations', () => {
|
||||||
|
@ -251,6 +550,8 @@ describe('extractSiteSourceCodeTranslations', () => {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
siteComponentFile1,
|
siteComponentFile1,
|
||||||
`
|
`
|
||||||
|
import Translate from '@docusaurus/Translate';
|
||||||
|
|
||||||
export default function MySiteComponent1() {
|
export default function MySiteComponent1() {
|
||||||
return (
|
return (
|
||||||
<Translate
|
<Translate
|
||||||
|
@ -283,6 +584,8 @@ export default function MySiteComponent1() {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
plugin1File1,
|
plugin1File1,
|
||||||
`
|
`
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
export default function MyComponent() {
|
export default function MyComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -301,6 +604,8 @@ export default function MyComponent() {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
plugin1File2,
|
plugin1File2,
|
||||||
`
|
`
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
export default function MyComponent() {
|
export default function MyComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -317,6 +622,8 @@ export default function MyComponent() {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
plugin1File3,
|
plugin1File3,
|
||||||
`
|
`
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
export default function MyComponent() {
|
export default function MyComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -334,6 +641,8 @@ export default function MyComponent() {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
plugin2File,
|
plugin2File,
|
||||||
`
|
`
|
||||||
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
|
|
||||||
type Props = {hey: string};
|
type Props = {hey: string};
|
||||||
|
|
||||||
export default function MyComponent(props: Props) {
|
export default function MyComponent(props: Props) {
|
||||||
|
|
|
@ -183,157 +183,191 @@ function extractSourceCodeAstTranslations(
|
||||||
sourceCodeFilePath: string,
|
sourceCodeFilePath: string,
|
||||||
): SourceCodeFileTranslations {
|
): SourceCodeFileTranslations {
|
||||||
function sourceWarningPart(node: Node) {
|
function sourceWarningPart(node: Node) {
|
||||||
return `File: ${sourceCodeFilePath} at ${
|
return `File: ${sourceCodeFilePath} at line ${node.loc?.start.line}
|
||||||
node.loc?.start.line
|
Full code: ${generate(node).code}`;
|
||||||
} line\nFull code: ${generate(node).code}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const translations: Record<string, TranslationMessage> = {};
|
const translations: Record<string, TranslationMessage> = {};
|
||||||
const warnings: string[] = [];
|
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, {
|
traverse(ast, {
|
||||||
JSXElement(path) {
|
ImportDeclaration(path) {
|
||||||
if (
|
if (
|
||||||
!path
|
path.node.importKind === 'type' ||
|
||||||
.get('openingElement')
|
path.get('source').node.value !== '@docusaurus/Translate'
|
||||||
.get('name')
|
|
||||||
.isJSXIdentifier({name: 'Translate'})
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
function evaluateJSXProp(propName: string): string | undefined {
|
const importSpecifiers = path.get('specifiers');
|
||||||
const attributePath = path
|
const defaultImport = importSpecifiers.find(
|
||||||
.get('openingElement.attributes')
|
(specifier): specifier is NodePath<t.ImportDefaultSpecifier> =>
|
||||||
.find(
|
specifier.node.type === 'ImportDefaultSpecifier',
|
||||||
(attr) =>
|
);
|
||||||
attr.isJSXAttribute() &&
|
const callbackImport = importSpecifiers.find(
|
||||||
(attr as NodePath<t.JSXAttribute>)
|
(specifier): specifier is NodePath<t.ImportSpecifier> =>
|
||||||
.get('name')
|
specifier.node.type === 'ImportSpecifier' &&
|
||||||
.isJSXIdentifier({name: propName}),
|
((
|
||||||
);
|
(specifier as NodePath<t.ImportSpecifier>).get('imported')
|
||||||
|
.node as t.Identifier
|
||||||
|
).name === 'translate' ||
|
||||||
|
(
|
||||||
|
(specifier as NodePath<t.ImportSpecifier>).get('imported')
|
||||||
|
.node as t.StringLiteral
|
||||||
|
).value === 'translate'),
|
||||||
|
);
|
||||||
|
|
||||||
if (attributePath) {
|
translateComponentName = defaultImport?.get('local').node.name;
|
||||||
const attributeValue = attributePath.get('value') as NodePath;
|
translateFunctionName = callbackImport?.get('local').node.name;
|
||||||
|
|
||||||
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(
|
|
||||||
`<Translate> prop=${propName} should be a statically evaluable object.\nExample: <Translate id="optional.id" description="optional description">Message</Translate>\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(`
|
|
||||||
<Translate> without children must have id prop.\nExample: <Translate id="my-id" />\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 <Translate id="my-id" description="my-description">text</Translate>.\n${sourceWarningPart(
|
|
||||||
path.node,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
CallExpression(path) {
|
traverse(ast, {
|
||||||
if (!path.get('callee').isIdentifier({name: 'translate'})) {
|
...(translateComponentName && {
|
||||||
return;
|
JSXElement(path) {
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
if (
|
||||||
firstArgEvaluated.confident &&
|
!path
|
||||||
typeof firstArgEvaluated.value === 'object'
|
.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<t.JSXAttribute>)
|
||||||
|
.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(
|
||||||
|
`<Translate> prop=${propName} should be a statically evaluable object.
|
||||||
|
Example: <Translate id="optional id" description="optional description">Message</Translate>
|
||||||
|
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(`<Translate> without children must have id prop.
|
||||||
|
Example: <Translate id="my-id" />
|
||||||
|
${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] = {
|
translations[id ?? message] = {
|
||||||
message: message ?? id,
|
message,
|
||||||
...(description && {description}),
|
...(description && {description}),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
warnings.push(
|
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(
|
`Translate content could not be extracted. It has to be a static string and use optional but static props, like <Translate id="my-id" description="my-description">text</Translate>.
|
||||||
path.node,
|
${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};
|
return {sourceCodeFilePath, translations, warnings};
|
||||||
|
|
Loading…
Add table
Reference in a new issue