mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-06 10:20:09 +02:00
feat: make Translate children optional (#5683)
Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
c8739ec28e
commit
92104c7c3b
6 changed files with 123 additions and 69 deletions
|
@ -141,16 +141,16 @@ declare module '@docusaurus/Interpolate' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@docusaurus/Translate' {
|
declare module '@docusaurus/Translate' {
|
||||||
import type {
|
import type {ReactNode} from 'react';
|
||||||
InterpolateProps,
|
import type {InterpolateValues} from '@docusaurus/Interpolate';
|
||||||
InterpolateValues,
|
|
||||||
} from '@docusaurus/Interpolate';
|
|
||||||
|
|
||||||
export type TranslateParam<Str extends string> = Partial<
|
// TS type to ensure that at least one of id or message is always provided
|
||||||
InterpolateProps<Str>
|
// (Generic permits to handled message provided as React children)
|
||||||
> & {
|
type IdOrMessage<MessageKey extends 'children' | 'message'> =
|
||||||
message: Str;
|
| ({[key in MessageKey]: string} & {id?: string})
|
||||||
id?: string;
|
| ({[key in MessageKey]?: string} & {id: string});
|
||||||
|
|
||||||
|
export type TranslateParam<Str extends string> = IdOrMessage<'message'> & {
|
||||||
description?: string;
|
description?: string;
|
||||||
values?: InterpolateValues<Str, string | number>;
|
values?: InterpolateValues<Str, string | number>;
|
||||||
};
|
};
|
||||||
|
@ -160,9 +160,9 @@ declare module '@docusaurus/Translate' {
|
||||||
values?: InterpolateValues<Str, string | number>,
|
values?: InterpolateValues<Str, string | number>,
|
||||||
): string;
|
): string;
|
||||||
|
|
||||||
export type TranslateProps<Str extends string> = InterpolateProps<Str> & {
|
export type TranslateProps<Str extends string> = IdOrMessage<'children'> & {
|
||||||
id?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
values?: InterpolateValues<Str, ReactNode>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Translate<Str extends string>(
|
export default function Translate<Str extends string>(
|
||||||
|
|
|
@ -5,11 +5,8 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import {ReactNode} from 'react';
|
||||||
import Interpolate, {
|
import {interpolate, InterpolateValues} from '@docusaurus/Interpolate';
|
||||||
interpolate,
|
|
||||||
InterpolateValues,
|
|
||||||
} from '@docusaurus/Interpolate';
|
|
||||||
import type {TranslateParam, TranslateProps} from '@docusaurus/Translate';
|
import type {TranslateParam, TranslateProps} from '@docusaurus/Translate';
|
||||||
|
|
||||||
// Can't read it from context, due to exposing imperative API
|
// Can't read it from context, due to exposing imperative API
|
||||||
|
@ -19,10 +16,16 @@ function getLocalizedMessage({
|
||||||
id,
|
id,
|
||||||
message,
|
message,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
}): string {
|
}): string {
|
||||||
return codeTranslations[id ?? message] ?? message;
|
if (typeof id === 'undefined' && typeof message === 'undefined') {
|
||||||
|
throw new Error(
|
||||||
|
'Docusaurus translation declarations must have at least a translation id or a default translation message',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeTranslations[(id ?? message)!] ?? message ?? id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Imperative translation API is useful for some edge-cases:
|
// Imperative translation API is useful for some edge-cases:
|
||||||
|
@ -32,7 +35,7 @@ export function translate<Str extends string>(
|
||||||
{message, id}: TranslateParam<Str>,
|
{message, id}: TranslateParam<Str>,
|
||||||
values?: InterpolateValues<Str, string | number>,
|
values?: InterpolateValues<Str, string | number>,
|
||||||
): string {
|
): string {
|
||||||
const localizedMessage = getLocalizedMessage({message, id}) ?? message;
|
const localizedMessage = getLocalizedMessage({message, id});
|
||||||
return interpolate(localizedMessage, values);
|
return interpolate(localizedMessage, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,16 +45,14 @@ export default function Translate<Str extends string>({
|
||||||
children,
|
children,
|
||||||
id,
|
id,
|
||||||
values,
|
values,
|
||||||
}: TranslateProps<Str>): JSX.Element {
|
}: TranslateProps<Str>): ReactNode {
|
||||||
if (typeof children !== 'string') {
|
if (children && typeof children !== 'string') {
|
||||||
console.warn('Illegal <Translate> children', children);
|
console.warn('Illegal <Translate> children', children);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The Docusaurus <Translate> component only accept simple string values',
|
'The Docusaurus <Translate> component only accept simple string values',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const localizedMessage: string =
|
const localizedMessage: string = getLocalizedMessage({message: children, id});
|
||||||
getLocalizedMessage({message: children, id}) ?? children;
|
return interpolate(localizedMessage, values);
|
||||||
|
|
||||||
return <Interpolate values={values}>{localizedMessage}</Interpolate>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {translate} from '../Translate';
|
||||||
|
|
||||||
|
describe('translate', () => {
|
||||||
|
test('accept id and use it as fallback', () => {
|
||||||
|
expect(translate({id: 'some-id'})).toEqual('some-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accept message and use it as fallback', () => {
|
||||||
|
expect(translate({message: 'some-message'})).toEqual('some-message');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accept id+message and use message as fallback', () => {
|
||||||
|
expect(translate({id: 'some-id', message: 'some-message'})).toEqual(
|
||||||
|
'some-message',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reject when no id or message', () => {
|
||||||
|
// @ts-expect-error: TS should protect when both id/message are missing
|
||||||
|
expect(() => translate({})).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Docusaurus translation declarations must have at least a translation id or a default translation message"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -84,7 +84,7 @@ const unrelated = 42;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('extract from a translate() function call', async () => {
|
test('extract from a translate() functions calls', async () => {
|
||||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
extension: 'js',
|
extension: 'js',
|
||||||
content: `
|
content: `
|
||||||
|
@ -92,6 +92,8 @@ export default function MyComponent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
|
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
|
||||||
|
|
||||||
|
<input text={translate({id: 'codeId1'})}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -107,12 +109,13 @@ 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'},
|
||||||
},
|
},
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('extract from a <Translate> component', async () => {
|
test('extract from a <Translate> components', async () => {
|
||||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||||
extension: 'js',
|
extension: 'js',
|
||||||
content: `
|
content: `
|
||||||
|
@ -122,6 +125,8 @@ export default function MyComponent() {
|
||||||
<Translate id="codeId" description={"code description"}>
|
<Translate id="codeId" description={"code description"}>
|
||||||
code message
|
code message
|
||||||
</Translate>
|
</Translate>
|
||||||
|
|
||||||
|
<Translate id="codeId1" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -137,6 +142,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'},
|
||||||
},
|
},
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
|
|
|
@ -177,14 +177,10 @@ function extractSourceCodeAstTranslations(
|
||||||
ast: Node,
|
ast: Node,
|
||||||
sourceCodeFilePath: string,
|
sourceCodeFilePath: string,
|
||||||
): SourceCodeFileTranslations {
|
): SourceCodeFileTranslations {
|
||||||
function staticTranslateJSXWarningPart() {
|
function sourceWarningPart(node: Node) {
|
||||||
return 'Translate content could not be extracted.\nIt has to be a static string and use optional but static props, like <Translate id="my-id" description="my-description">text</Translate>.';
|
return `File: ${sourceCodeFilePath} at ${
|
||||||
}
|
node.loc?.start.line
|
||||||
function sourceFileWarningPart(node: Node) {
|
} line\nFull code: ${generate(node).code}`;
|
||||||
return `File=${sourceCodeFilePath} at line=${node.loc?.start.line}`;
|
|
||||||
}
|
|
||||||
function generateCode(node: Node) {
|
|
||||||
return generate(node).code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const translations: Record<string, TranslationMessage> = {};
|
const translations: Record<string, TranslationMessage> = {};
|
||||||
|
@ -228,9 +224,9 @@ function extractSourceCodeAstTranslations(
|
||||||
return attributeValueEvaluated.value;
|
return attributeValueEvaluated.value;
|
||||||
} else {
|
} else {
|
||||||
warnings.push(
|
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${sourceFileWarningPart(
|
`<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,
|
path.node,
|
||||||
)}\n${generateCode(path.node)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,41 +234,51 @@ function extractSourceCodeAstTranslations(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only handle the optimistic case where we have a single non-empty content
|
const id = evaluateJSXProp('id');
|
||||||
const singleChildren = path
|
const description = evaluateJSXProp('description');
|
||||||
.get('children')
|
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!
|
// Remove empty/useless text nodes that might be around our translation!
|
||||||
// Makes the translation system more reliable to JSX formatting issues
|
// Makes the translation system more reliable to JSX formatting issues
|
||||||
.filter(
|
.filter(
|
||||||
(childrenPath) =>
|
(children) =>
|
||||||
!(
|
!(
|
||||||
childrenPath.isJSXText() &&
|
children.isJSXText() &&
|
||||||
childrenPath.node.value.replace('\n', '').trim() === ''
|
children.node.value.replace('\n', '').trim() === ''
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pop();
|
.pop();
|
||||||
|
const isJSXText = singleChildren && singleChildren.isJSXText();
|
||||||
if (singleChildren && singleChildren.isJSXText()) {
|
const isJSXExpressionContainer =
|
||||||
const message = singleChildren.node.value.trim().replace(/\s+/g, ' ');
|
|
||||||
|
|
||||||
const id = evaluateJSXProp('id');
|
|
||||||
const description = evaluateJSXProp('description');
|
|
||||||
|
|
||||||
translations[id ?? message] = {
|
|
||||||
message,
|
|
||||||
...(description && {description}),
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
singleChildren &&
|
singleChildren &&
|
||||||
singleChildren.isJSXExpressionContainer() &&
|
singleChildren.isJSXExpressionContainer() &&
|
||||||
(singleChildren.get('expression') as NodePath).evaluate().confident
|
(singleChildren.get('expression') as NodePath).evaluate().confident;
|
||||||
) {
|
|
||||||
const message = (
|
|
||||||
singleChildren.get('expression') as NodePath
|
|
||||||
).evaluate().value;
|
|
||||||
|
|
||||||
const id = evaluateJSXProp('id');
|
if (isJSXText || isJSXExpressionContainer) {
|
||||||
const description = evaluateJSXProp('description');
|
message = isJSXText
|
||||||
|
? singleChildren.node.value.trim().replace(/\s+/g, ' ')
|
||||||
|
: (singleChildren.get('expression') as NodePath).evaluate().value;
|
||||||
|
|
||||||
translations[id ?? message] = {
|
translations[id ?? message] = {
|
||||||
message,
|
message,
|
||||||
|
@ -280,9 +286,9 @@ function extractSourceCodeAstTranslations(
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`${staticTranslateJSXWarningPart()}\n${sourceFileWarningPart(
|
`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,
|
path.node,
|
||||||
)}\n${generateCode(path.node)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -308,21 +314,21 @@ function extractSourceCodeAstTranslations(
|
||||||
) {
|
) {
|
||||||
const {message, id, description} = firstArgEvaluated.value;
|
const {message, id, description} = firstArgEvaluated.value;
|
||||||
translations[id ?? message] = {
|
translations[id ?? message] = {
|
||||||
message,
|
message: message ?? id,
|
||||||
...(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${sourceFileWarningPart(
|
`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,
|
path.node,
|
||||||
)}\n${generateCode(path.node)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`translate() function only takes 1 or 2 args\n${sourceFileWarningPart(
|
`translate() function only takes 1 or 2 args\n${sourceWarningPart(
|
||||||
path.node,
|
path.node,
|
||||||
)}\n${generateCode(path.node)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -253,6 +253,16 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
You can even omit a children prop and specify a translation string in your `code.json` file manually after running the `docusaurus write-translations` CLI command.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Translate id="homepage.title" />
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
## Hooks {#hooks}
|
## Hooks {#hooks}
|
||||||
|
|
||||||
### `useDocusaurusContext` {#usedocusauruscontext}
|
### `useDocusaurusContext` {#usedocusauruscontext}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue