mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-10 23:02:56 +02:00
feat: Docusaurus ESLint plugin to enforce best Docusaurus practices (#7206)
* feat: add eslint plugin * refactor * add tests * fixups! * fix(no-dynamic-i18n-messages): make translate() recognize template literals * refactor: rename rule no-dynamic-i18n-messages --> string-literal-i18n-messages * feat: add ignoreStrings option and refactor * docs: migrate docs to /docs/api/plugins * docs: fix anchor links in README.md * fix: add some ignored strings * docs: update eslint-plugin docs * fix: update README link * docs: various updates - Reorder sidebar entries - Fix title size - Use Markdown file paths - Simplify relative links * address reviews * wording polish * add npmignore * fix all internal warnings * doc improvements * fix test Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
parent
ae788c536f
commit
3b1170eb44
34 changed files with 885 additions and 52 deletions
39
.eslintrc.js
39
.eslintrc.js
|
@ -32,6 +32,7 @@ module.exports = {
|
|||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:regexp/recommended',
|
||||
'prettier',
|
||||
'plugin:@docusaurus/all',
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
|
@ -41,7 +42,14 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
reportUnusedDisableDirectives: true,
|
||||
plugins: ['react-hooks', 'header', 'jest', '@typescript-eslint', 'regexp'],
|
||||
plugins: [
|
||||
'react-hooks',
|
||||
'header',
|
||||
'jest',
|
||||
'@typescript-eslint',
|
||||
'regexp',
|
||||
'@docusaurus',
|
||||
],
|
||||
rules: {
|
||||
'array-callback-return': WARNING,
|
||||
camelcase: WARNING,
|
||||
|
@ -305,6 +313,24 @@ module.exports = {
|
|||
// locals must be justified with a disable comment.
|
||||
'@typescript-eslint/no-unused-vars': [ERROR, {ignoreRestSiblings: true}],
|
||||
'@typescript-eslint/prefer-optional-chain': ERROR,
|
||||
'@docusaurus/no-untranslated-text': [
|
||||
WARNING,
|
||||
{
|
||||
ignoreStrings: [
|
||||
'·',
|
||||
'-',
|
||||
'—',
|
||||
'×',
|
||||
'', // zwj: ​
|
||||
'@',
|
||||
'WebContainers',
|
||||
'Twitter',
|
||||
'GitHub',
|
||||
'Dev.to',
|
||||
'1.x',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
@ -327,6 +353,7 @@ module.exports = {
|
|||
'header/header': OFF,
|
||||
'global-require': OFF,
|
||||
'@typescript-eslint/no-var-requires': OFF,
|
||||
'@docusaurus/no-untranslated-text': OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -350,6 +377,16 @@ module.exports = {
|
|||
'@typescript-eslint/explicit-module-boundary-types': OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/**',
|
||||
'packages/docusaurus-plugin-debug/**',
|
||||
'website/_dogfooding/**',
|
||||
],
|
||||
rules: {
|
||||
'@docusaurus/no-untranslated-text': OFF,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Internal files where extraneous deps don't matter much at long as
|
||||
// they run
|
||||
|
|
|
@ -82,6 +82,7 @@ export default function LastUpdated({
|
|||
</Translate>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div>
|
||||
{/* eslint-disable-next-line @docusaurus/no-untranslated-text */}
|
||||
<small> (Simulated during dev for better perf)</small>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -23,6 +23,7 @@ function Header({children}: {children: React.ReactNode}) {
|
|||
|
||||
function LivePreviewLoader() {
|
||||
// Is it worth improving/translating?
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ describe('<Translate>', () => {
|
|||
it('rejects when children is not a string', () => {
|
||||
expect(() =>
|
||||
renderer.create(
|
||||
// eslint-disable-next-line @docusaurus/string-literal-i18n-messages
|
||||
<Translate id="foo">
|
||||
{/* @ts-expect-error: for test */}
|
||||
<span>aaa</span>
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// Should we translate theme-fallback?
|
||||
/* eslint-disable @docusaurus/no-untranslated-text */
|
||||
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// Should we translate theme-fallback?
|
||||
/* eslint-disable @docusaurus/no-untranslated-text */
|
||||
|
||||
import React from 'react';
|
||||
import type {LoadingComponentProps} from 'react-loadable';
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// Should we translate theme-fallback?
|
||||
/* eslint-disable @docusaurus/no-untranslated-text */
|
||||
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import Head from '@docusaurus/Head';
|
||||
|
|
10
packages/eslint-plugin/.eslintrc.js
Normal file
10
packages/eslint-plugin/.eslintrc.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: ['../../.eslintrc.js', 'plugin:eslint-plugin/recommended'],
|
||||
};
|
6
packages/eslint-plugin/.npmignore
Normal file
6
packages/eslint-plugin/.npmignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
copyUntypedFiles.mjs
|
||||
.tsbuildinfo
|
||||
tsconfig*
|
||||
__tests__
|
||||
|
||||
.eslintrc.js
|
7
packages/eslint-plugin/README.md
Normal file
7
packages/eslint-plugin/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# `@docusaurus/eslint-plugin`
|
||||
|
||||
ESLint plugin to enforce best Docusaurus practices.
|
||||
|
||||
## Usage
|
||||
|
||||
See [eslint-plugin documentation](https://docusaurus.io/docs/api/misc/@docusaurus/eslint-plugin).
|
25
packages/eslint-plugin/lib/index.js
Normal file
25
packages/eslint-plugin/lib/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const requireIndex = require('requireindex');
|
||||
|
||||
module.exports = {
|
||||
rules: requireIndex(`${__dirname}/rules`),
|
||||
configs: {
|
||||
recommended: {
|
||||
rules: {
|
||||
'@docusaurus/string-literal-i18n-messages': 'error',
|
||||
},
|
||||
},
|
||||
all: {
|
||||
rules: {
|
||||
'@docusaurus/string-literal-i18n-messages': 'error',
|
||||
'@docusaurus/no-untranslated-text': 'warn',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const rule = require('../no-untranslated-text');
|
||||
const {RuleTester} = require('eslint');
|
||||
const {getCommonValidTests} = require('../../util');
|
||||
|
||||
const errorsJSX = [{messageId: 'translateChildren', type: 'JSXElement'}];
|
||||
const errorsJSXFragment = [
|
||||
{messageId: 'translateChildren', type: 'JSXFragment'},
|
||||
];
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
ecmaFeatures: {jsx: true},
|
||||
},
|
||||
});
|
||||
ruleTester.run('no-untranslated-text', rule, {
|
||||
valid: [
|
||||
...getCommonValidTests(),
|
||||
{
|
||||
code: '<Component>·</Component>',
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>· </Component>',
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component> · </Component>',
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>· ·</Component>',
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>· — ×</Component>',
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>{"·"}</Component>',
|
||||
options: [{ignoreStrings: ['·']}],
|
||||
},
|
||||
{
|
||||
code: "<Component>{'·'}</Component>",
|
||||
options: [{ignoreStrings: ['·']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>{`·`}</Component>',
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>Docusaurus</Component>',
|
||||
options: [{ignoreStrings: ['Docusaurus']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>​</Component>',
|
||||
options: [{ignoreStrings: ['']}],
|
||||
},
|
||||
{
|
||||
code: `<>
|
||||
{' · '}
|
||||
</>`,
|
||||
options: [{ignoreStrings: ['·', "'"]}],
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
code: '<Component>text</Component>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Component> text </Component>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Component>"text"</Component>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: "<Component>'text'</Component>",
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Component>`text`</Component>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Component>{"text"}</Component>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: "<Component>{'text'}</Component>",
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Component>{`text`}</Component>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<>text</>',
|
||||
errors: errorsJSXFragment,
|
||||
},
|
||||
{
|
||||
code: '<Component>· — ×</Component>',
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['·', '—']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>··</Component>',
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component> ·· </Component>',
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>"·"</Component>',
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: "<Component>'·'</Component>",
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>`·`</Component>',
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['·', '—', '×']}],
|
||||
},
|
||||
{
|
||||
code: '<Component>Docusaurus</Component>',
|
||||
errors: errorsJSX,
|
||||
options: [{ignoreStrings: ['Docu', 'saurus']}],
|
||||
},
|
||||
],
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const rule = require('../string-literal-i18n-messages');
|
||||
const {RuleTester} = require('eslint');
|
||||
const {getCommonValidTests} = require('../../util');
|
||||
|
||||
const errorsJSX = [{messageId: 'translateChildren', type: 'JSXElement'}];
|
||||
const errorsFunc = [{messageId: 'translateArg', type: 'Identifier'}];
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
ecmaFeatures: {jsx: true},
|
||||
},
|
||||
});
|
||||
ruleTester.run('string-literal-i18n-messages', rule, {
|
||||
valid: [...getCommonValidTests()],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
code: '<Translate>{text}</Translate>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Translate>Hi {text} my friend</Translate>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Translate> {text} </Translate>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: '<Translate>`{text}`</Translate>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
code: '<Translate>{`${text}`}</Translate>',
|
||||
errors: errorsJSX,
|
||||
},
|
||||
{
|
||||
code: 'translate({message: metaTitle})',
|
||||
errors: errorsFunc,
|
||||
},
|
||||
],
|
||||
});
|
71
packages/eslint-plugin/lib/rules/no-untranslated-text.js
Normal file
71
packages/eslint-plugin/lib/rules/no-untranslated-text.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {isTextLabelChild, report} = require('../util');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').Rule.RuleModule}
|
||||
*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'enforce text labels in JSX to be wrapped by translate calls',
|
||||
category: 'Suggestions',
|
||||
url: 'https://docusaurus.io/docs/api/misc/@docusaurus/eslint-plugin/no-untranslated-text',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreStrings: {
|
||||
type: 'array',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
translateChildren:
|
||||
'All text labels in JSX should be wrapped by translate calls',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const stringsToIgnore = context.options[0]?.ignoreStrings ?? [];
|
||||
|
||||
const isParentTranslate = ({child, isParentFragment}) =>
|
||||
!isParentFragment &&
|
||||
child.parent.openingElement.name.name === 'Translate';
|
||||
|
||||
const isChildValid = ({child, isParentFragment}) => {
|
||||
if (!isTextLabelChild({child, ignoreWhitespace: true, stringsToIgnore})) {
|
||||
return true;
|
||||
}
|
||||
return isParentTranslate({child, isParentFragment});
|
||||
};
|
||||
|
||||
const isNodeValid = ({node, isFragment = false} = {}) =>
|
||||
node.children.every((child) =>
|
||||
isChildValid({child, isParentFragment: isFragment}),
|
||||
);
|
||||
|
||||
return {
|
||||
'JSXElement[openingElement.selfClosing=false]': (node) => {
|
||||
if (!isNodeValid({node})) {
|
||||
report(context, node, 'translateChildren');
|
||||
}
|
||||
},
|
||||
'JSXFragment[openingFragment]': (node) => {
|
||||
if (!isNodeValid({node, isFragment: true})) {
|
||||
report(context, node, 'translateChildren');
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {
|
||||
isTextLabelChild,
|
||||
report,
|
||||
isStringWithoutExpressions,
|
||||
} = require('../util');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').Rule.RuleModule}
|
||||
*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'enforce translate APIs to be called on plain text labels',
|
||||
category: 'Possible Problems',
|
||||
url: 'https://docusaurus.io/docs/api/misc/@docusaurus/eslint-plugin/string-literal-i18n-messages',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
translateChildren:
|
||||
'<Translate> children must be hardcoded strings. You can have in-string dynamic placeholders using the values prop.',
|
||||
translateArg:
|
||||
'translation message must be a hardcoded string. You can have in-string dynamic placeholders using the values argument.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const isNodeValid = (node) =>
|
||||
node.children.every((child) => isTextLabelChild({child}));
|
||||
|
||||
return {
|
||||
"JSXElement[openingElement.name.name='Translate']": (node) => {
|
||||
if (!isNodeValid(node)) {
|
||||
report(context, node, 'translateChildren');
|
||||
}
|
||||
},
|
||||
"CallExpression > Identifier[name='translate']": (node) => {
|
||||
const messageProperty = node.parent.arguments[0].properties.find(
|
||||
(property) => property.key.name === 'message',
|
||||
);
|
||||
if (!messageProperty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!isStringWithoutExpressions({
|
||||
text: messageProperty.value,
|
||||
})
|
||||
) {
|
||||
report(context, node, 'translateArg');
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
145
packages/eslint-plugin/lib/util.js
Normal file
145
packages/eslint-plugin/lib/util.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const isMadeOfIgnoredStrings = ({text, stringsToIgnore}) =>
|
||||
text
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.every((string) => stringsToIgnore.includes(string));
|
||||
|
||||
const isWhitespace = (text) => !text || !text.trim();
|
||||
|
||||
const isTextValid = ({text, ignoreWhitespace, stringsToIgnore}) =>
|
||||
!!text &&
|
||||
!(ignoreWhitespace && isWhitespace(text)) &&
|
||||
!isMadeOfIgnoredStrings({
|
||||
text,
|
||||
stringsToIgnore,
|
||||
});
|
||||
|
||||
const isStringWithoutExpressions = ({
|
||||
text,
|
||||
ignoreWhitespace = false,
|
||||
stringsToIgnore = [],
|
||||
} = {}) => {
|
||||
switch (text.type) {
|
||||
case 'Literal':
|
||||
return isTextValid({text: text.value, ignoreWhitespace, stringsToIgnore});
|
||||
case 'TemplateLiteral':
|
||||
return (
|
||||
!text.expressions.length &&
|
||||
isTextValid({
|
||||
text: text.quasis[0].value.raw,
|
||||
ignoreWhitespace,
|
||||
stringsToIgnore,
|
||||
})
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isTextLabelChild = ({
|
||||
child,
|
||||
ignoreWhitespace = false,
|
||||
stringsToIgnore = [],
|
||||
} = {}) => {
|
||||
switch (child.type) {
|
||||
case 'JSXText':
|
||||
return isTextValid({
|
||||
text: child.value,
|
||||
ignoreWhitespace,
|
||||
stringsToIgnore,
|
||||
});
|
||||
case 'JSXExpressionContainer':
|
||||
return isStringWithoutExpressions({
|
||||
text: child.expression,
|
||||
ignoreWhitespace,
|
||||
stringsToIgnore,
|
||||
});
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const report = (context, node, messageId) => {
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
});
|
||||
};
|
||||
|
||||
const getCommonValidTests = () => [
|
||||
{
|
||||
code: '<Translate>text</Translate>',
|
||||
},
|
||||
{
|
||||
code: '<Translate> text </Translate>',
|
||||
},
|
||||
{
|
||||
code: '<Translate>"text"</Translate>',
|
||||
},
|
||||
{
|
||||
code: "<Translate>'text'</Translate>",
|
||||
},
|
||||
{
|
||||
code: '<Translate>`text`</Translate>',
|
||||
},
|
||||
{
|
||||
code: '<Translate>{"text"}</Translate>',
|
||||
},
|
||||
{
|
||||
code: "<Translate>{'text'}</Translate>",
|
||||
},
|
||||
{
|
||||
code: '<Translate>{`text`}</Translate>',
|
||||
},
|
||||
{
|
||||
code: '<Component>{text}</Component>',
|
||||
},
|
||||
{
|
||||
code: '<Component> {text} </Component>',
|
||||
},
|
||||
{
|
||||
code: 'translate({message: `My page meta title`})',
|
||||
},
|
||||
{
|
||||
code: `<Translate
|
||||
id="homepage.title"
|
||||
description="The homepage welcome message">
|
||||
Welcome to my website
|
||||
</Translate>`,
|
||||
},
|
||||
{
|
||||
code: `<Translate
|
||||
values={{firstName: 'Sébastien'}}>
|
||||
{'Welcome, {firstName}! How are you?'}
|
||||
</Translate>`,
|
||||
},
|
||||
{
|
||||
code: `<Translate>{'This'} is {\`valid\`}</Translate>`,
|
||||
},
|
||||
{
|
||||
code: "translate({message: 'My page meta title'})",
|
||||
},
|
||||
{
|
||||
code: "translate({message: 'The logo of site {siteName}'}, {siteName: 'Docusaurus'})",
|
||||
},
|
||||
{
|
||||
code: 'translate({otherProp: metaTitle})',
|
||||
},
|
||||
{
|
||||
code: 'translate({otherProp: `My page meta title`})',
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
isTextLabelChild,
|
||||
report,
|
||||
getCommonValidTests,
|
||||
isStringWithoutExpressions,
|
||||
};
|
32
packages/eslint-plugin/package.json
Normal file
32
packages/eslint-plugin/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "@docusaurus/eslint-plugin",
|
||||
"version": "2.0.0-beta.18",
|
||||
"description": "ESLint plugin to enforce best Docusaurus practices.",
|
||||
"main": "lib/index.js",
|
||||
"keywords": [
|
||||
"eslint",
|
||||
"eslintplugin",
|
||||
"eslint-plugin"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/docusaurus.git",
|
||||
"directory": "packages/eslint-plugin"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"requireindex": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-eslint-plugin": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
|
@ -245,12 +245,14 @@ refactorings
|
|||
regexes
|
||||
rehype
|
||||
reponame
|
||||
requireindex
|
||||
retrocompatibility
|
||||
retrocompatible
|
||||
roadmap
|
||||
rocketvalidator
|
||||
rtcts
|
||||
rtlcss
|
||||
saurus
|
||||
scaleway
|
||||
searchbar
|
||||
sebastien
|
||||
|
|
2
website/docs/api/misc/_category_.yml
Normal file
2
website/docs/api/misc/_category_.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
label: Miscellaneous
|
||||
position: 4
|
74
website/docs/api/misc/eslint-plugin/README.md
Normal file
74
website/docs/api/misc/eslint-plugin/README.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
sidebar_position: 0
|
||||
id: eslint-plugin
|
||||
title: '📦 eslint-plugin'
|
||||
slug: '/api/misc/@docusaurus/eslint-plugin'
|
||||
---
|
||||
|
||||
[ESLint](https://eslint.org/) is a tool that statically analyzes your code and reports problems or suggests best practices through editor hints and command line. Docusaurus provides an ESLint plugin to enforce best Docusaurus practices.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash npm2yarn
|
||||
npm install --save-dev @docusaurus/eslint-plugin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add `@docusaurus` to the plugins section of your `.eslintrc` configuration file:
|
||||
|
||||
```json title=".eslintrc"
|
||||
{
|
||||
"plugins": ["@docusaurus"]
|
||||
}
|
||||
```
|
||||
|
||||
Then, you can extend one of the configs (e.g. the `recommended` config):
|
||||
|
||||
```json title=".eslintrc"
|
||||
{
|
||||
"extends": ["plugin:@docusaurus/recommended"]
|
||||
}
|
||||
```
|
||||
|
||||
Each config contains a set of rules. For more fine-grained control, you can also configure the rules you want to use directly:
|
||||
|
||||
```json title=".eslintrc"
|
||||
{
|
||||
"rules": {
|
||||
"@docusaurus/string-literal-i18n-messages": "error",
|
||||
"@docusaurus/no-untranslated-text": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Configs
|
||||
|
||||
- Recommended: recommended rule set for most Docusaurus sites that should be extended from.
|
||||
- All: **all** rules enabled. This will change between minor versions, so you should not use this if you want to avoid unexpected breaking changes.
|
||||
|
||||
## Supported Rules
|
||||
|
||||
| Name | Description | |
|
||||
| --- | --- | --- |
|
||||
| [`@docusaurus/no-untranslated-text`](./no-untranslated-text.md) | Enforce text labels in JSX to be wrapped by translate calls | |
|
||||
| [`@docusaurus/string-literal-i18n-messages`](./string-literal-i18n-messages.md) | Enforce translate APIs to be called on plain text labels | ✅ |
|
||||
|
||||
✅ = recommended
|
||||
|
||||
## Example configuration
|
||||
|
||||
Here's an example configuration:
|
||||
|
||||
```js title=".eslintrc.js"
|
||||
module.exports = {
|
||||
extends: ['plugin:@docusaurus/recommended'],
|
||||
plugins: ['@docusaurus'],
|
||||
rules: {
|
||||
'@docusaurus/no-untranslated-text': [
|
||||
'warn',
|
||||
{ignoreStrings: ['·', '—', '×']},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
48
website/docs/api/misc/eslint-plugin/no-untranslated-text.md
Normal file
48
website/docs/api/misc/eslint-plugin/no-untranslated-text.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
slug: '/api/misc/@docusaurus/eslint-plugin/no-untranslated-text'
|
||||
---
|
||||
|
||||
# no-untranslated-text
|
||||
|
||||
Enforce text labels in JSX to be wrapped by translate calls.
|
||||
|
||||
When the [i18n feature](../../../i18n/i18n-introduction.md) is used, this rule ensures that all labels appearing on the website are translatable, so no string accidentally slips through untranslated.
|
||||
|
||||
## Rule Details {#details}
|
||||
|
||||
Examples of **incorrect** code for this rule:
|
||||
|
||||
```js
|
||||
// Hello World is not translated
|
||||
<Component>Hello World</Component>
|
||||
```
|
||||
|
||||
Examples of **correct** code for this rule:
|
||||
|
||||
```js
|
||||
// Hello World is translated
|
||||
<Component>
|
||||
<Translate>Hello World</Translate>
|
||||
</Component>
|
||||
```
|
||||
|
||||
## Rule Configuration {#configuration}
|
||||
|
||||
Accepted fields:
|
||||
|
||||
<APITable>
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `ignoreStrings` | `string[]` | `[]` | Text labels that only contain strings in this list will not be reported. |
|
||||
|
||||
</APITable>
|
||||
|
||||
## When Not To Use It {#when-not-to-use}
|
||||
|
||||
If you're not using the [i18n feature](../../../i18n/i18n-introduction.md), you can disable this rule. You can also disable this rule where the text is not supposed to be translated.
|
||||
|
||||
## Further Reading {#further-reading}
|
||||
|
||||
- https://docusaurus.io/docs/docusaurus-core#translate
|
||||
- https://docusaurus.io/docs/docusaurus-core#translate-imperative
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
slug: '/api/misc/@docusaurus/eslint-plugin/string-literal-i18n-messages'
|
||||
---
|
||||
|
||||
# string-literal-i18n-messages
|
||||
|
||||
Enforce translate APIs to be called on plain text labels.
|
||||
|
||||
Docusaurus offers the [`docusaurus write-translations`](../../../cli.md#docusaurus-write-translations-sitedir) API, which statically extracts the text labels marked as translatable. Dynamic values used in `<Translate>` or `translate()` calls will fail to be extracted. This rule will ensure that all translate calls are statically extractable.
|
||||
|
||||
## Rule Details {#details}
|
||||
|
||||
Examples of **incorrect** code for this rule:
|
||||
|
||||
```js
|
||||
const text = 'Some text to be translated'
|
||||
|
||||
// Invalid <Translate> child
|
||||
<Translate>{text}</Translate>
|
||||
|
||||
// Invalid message attribute
|
||||
translate({message: text})
|
||||
```
|
||||
|
||||
Examples of **correct** code for this rule:
|
||||
|
||||
```js
|
||||
// Valid <Translate> child
|
||||
<Translate>Some text to be translated</Translate>
|
||||
|
||||
// Valid message attribute
|
||||
translate({message: 'Some text to be translated'})
|
||||
|
||||
// Valid <Translate> child using values object as prop
|
||||
<Translate values={{firstName: 'Sébastien'}}>
|
||||
{'Welcome, {firstName}! How are you?'}
|
||||
</Translate>
|
||||
|
||||
// Valid message attribute using values object as second argument
|
||||
translate({message: 'The logo of site {siteName}'}, {siteName: 'Docusaurus'})
|
||||
```
|
||||
|
||||
## When Not To Use It {#when-not-to-use}
|
||||
|
||||
If you're not using the [i18n feature](../../../i18n/i18n-introduction.md), you can disable this rule.
|
||||
|
||||
## Further Reading {#further-reading}
|
||||
|
||||
- https://docusaurus.io/docs/docusaurus-core#translate
|
||||
- https://docusaurus.io/docs/docusaurus-core#translate-imperative
|
|
@ -621,7 +621,7 @@ import {interpolate} from '@docusaurus/Interpolate';
|
|||
const message = interpolate('Welcome {firstName}', {firstName: 'Sébastien'});
|
||||
```
|
||||
|
||||
### `translate` {#translate-1}
|
||||
### `translate` {#translate-imperative}
|
||||
|
||||
The imperative counterpart of the [`<Translate>`](#translate) component. Also supporting [placeholders interpolation](#interpolate).
|
||||
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"@docusaurus/eslint-plugin": "2.0.0-beta.18",
|
||||
"@types/jest": "^27.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"rimraf": "^3.0.2"
|
||||
|
|
|
@ -279,6 +279,7 @@ export default function ColorGenerator(): JSX.Element {
|
|||
<p>
|
||||
<Translate
|
||||
id="colorGenerator.text"
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
values={{cssPath: <code>src/css/custom.css</code>}}>
|
||||
{'Replace the variables in {cssPath} with these new variables.'}
|
||||
</Translate>
|
||||
|
|
|
@ -82,6 +82,7 @@ function VersionNotice() {
|
|||
<Link href="/community/canary">
|
||||
<Translate
|
||||
id="upgradeGuide.unreleasedVersion.notice.canaryDocLink.label"
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
values={{canaryTag: <code>@canary</code>}}>
|
||||
{'{canaryTag} release'}
|
||||
</Translate>
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/* eslint-disable @docusaurus/no-untranslated-text */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {Props as Tweet} from '../components/Tweet';
|
||||
|
|
|
@ -90,9 +90,8 @@ function MigrationAnnouncement() {
|
|||
</Link>
|
||||
),
|
||||
}}>
|
||||
{`Coming from {docusaurusV1Link}? Check out our {migrationGuideLink}`}
|
||||
{`Coming from {docusaurusV1Link}? Check out our {migrationGuideLink}.`}
|
||||
</Translate>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -241,11 +240,19 @@ export default function Home(): JSX.Element {
|
|||
<main>
|
||||
<div>
|
||||
<div className={styles.banner}>
|
||||
Support Ukraine 🇺🇦{' '}
|
||||
<Translate
|
||||
id="homepage.banner"
|
||||
values={{
|
||||
link: (
|
||||
<Link to="https://opensource.facebook.com/support-ukraine">
|
||||
<Translate id="homepage.banner.link">
|
||||
Help Provide Humanitarian Aid to Ukraine
|
||||
</Translate>
|
||||
</Link>
|
||||
.
|
||||
),
|
||||
}}>
|
||||
{'Support Ukraine 🇺🇦 {link}.'}
|
||||
</Translate>
|
||||
</div>
|
||||
</div>
|
||||
<HeroBanner />
|
||||
|
|
|
@ -59,10 +59,11 @@ export default function ShowcaseFilterToggle(): JSX.Element {
|
|||
}}
|
||||
checked={operator}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label htmlFor={id} className={clsx(styles.checkboxLabel, 'shadow--md')}>
|
||||
{/* eslint-disable @docusaurus/no-untranslated-text */}
|
||||
<span className={styles.checkboxLabelOr}>OR</span>
|
||||
<span className={styles.checkboxLabelAnd}>AND</span>
|
||||
{/* eslint-enable @docusaurus/no-untranslated-text */}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -170,9 +170,15 @@ export default function Version(): JSX.Element {
|
|||
</div>
|
||||
)}
|
||||
<div className="margin-bottom--lg">
|
||||
<h3 id="legacy">Docusaurus v1 (Legacy)</h3>
|
||||
<h3 id="legacy">
|
||||
<Translate id="versionsPage.legacy.title">
|
||||
Docusaurus v1 (Legacy)
|
||||
</Translate>
|
||||
</h3>
|
||||
<p>
|
||||
<Translate id="versionsPage.legacy.description">
|
||||
Here you can find documentation for legacy version of Docusaurus.
|
||||
</Translate>
|
||||
</p>
|
||||
<table>
|
||||
<tbody>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React from 'react';
|
||||
import BlogLayout from '@theme/BlogLayout';
|
||||
import BlogListPaginator from '@theme/BlogListPaginator';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import type {Props} from '@theme/BlogListPage';
|
||||
import {
|
||||
PageMetadata,
|
||||
|
@ -41,25 +42,10 @@ function ChangelogListContent(props: Props): JSX.Element {
|
|||
<header className="margin-bottom--lg">
|
||||
<h1 style={{fontSize: '3rem'}}>{blogTitle}</h1>
|
||||
<p>
|
||||
Subscribe through{' '}
|
||||
<Link href="pathname:///changelog/rss.xml" className={styles.rss}>
|
||||
<b>RSS feeds</b>
|
||||
<svg
|
||||
style={{
|
||||
fill: '#f26522',
|
||||
position: 'relative',
|
||||
left: 4,
|
||||
top: 1,
|
||||
marginRight: 8,
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M6.503 20.752c0 1.794-1.456 3.248-3.251 3.248-1.796 0-3.252-1.454-3.252-3.248 0-1.794 1.456-3.248 3.252-3.248 1.795.001 3.251 1.454 3.251 3.248zm-6.503-12.572v4.811c6.05.062 10.96 4.966 11.022 11.009h4.817c-.062-8.71-7.118-15.758-15.839-15.82zm0-3.368c10.58.046 19.152 8.594 19.183 19.188h4.817c-.03-13.231-10.755-23.954-24-24v4.812z" />
|
||||
</svg>
|
||||
</Link>{' '}
|
||||
or follow us on{' '}
|
||||
<Translate
|
||||
id="changelog.description"
|
||||
values={{
|
||||
twitterLink: (
|
||||
<Link
|
||||
href="https://twitter.com/docusaurus"
|
||||
className={styles.twitter}>
|
||||
|
@ -78,8 +64,38 @@ function ChangelogListContent(props: Props): JSX.Element {
|
|||
viewBox="0 0 512 512">
|
||||
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
|
||||
</svg>
|
||||
</Link>{' '}
|
||||
to stay up-to-date with new releases!
|
||||
</Link>
|
||||
),
|
||||
rssLink: (
|
||||
<Link
|
||||
href="pathname:///changelog/rss.xml"
|
||||
className={styles.rss}>
|
||||
<b>
|
||||
<Translate id="changelog.description.rssLink">
|
||||
RSS feeds
|
||||
</Translate>
|
||||
</b>
|
||||
<svg
|
||||
style={{
|
||||
fill: '#f26522',
|
||||
position: 'relative',
|
||||
left: 4,
|
||||
top: 1,
|
||||
marginRight: 8,
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M6.503 20.752c0 1.794-1.456 3.248-3.251 3.248-1.796 0-3.252-1.454-3.252-3.248 0-1.794 1.456-3.248 3.252-3.248 1.795.001 3.251 1.454 3.251 3.248zm-6.503-12.572v4.811c6.05.062 10.96 4.966 11.022 11.009h4.817c-.062-8.71-7.118-15.758-15.839-15.82zm0-3.368c10.58.046 19.152 8.594 19.183 19.188h4.817c-.03-13.231-10.755-23.954-24-24v4.812z" />
|
||||
</svg>
|
||||
</Link>
|
||||
),
|
||||
}}>
|
||||
{
|
||||
'Subscribe through {rssLink} or follow us on {twitterLink} to stay up-to-date with new releases!'
|
||||
}
|
||||
</Translate>
|
||||
</p>
|
||||
</header>
|
||||
{items.map(({content: BlogPostContent}) => (
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import BlogLayout from '@theme/BlogLayout';
|
||||
import ChangelogItem from '@theme/ChangelogItem';
|
||||
import ChangelogPaginator from '@theme/ChangelogPaginator';
|
||||
|
@ -86,7 +87,9 @@ function ChangelogPageContent(props: Props): JSX.Element {
|
|||
/>
|
||||
) : undefined
|
||||
}>
|
||||
<Link to={listPageLink}>← Back to index page</Link>
|
||||
<Link to={listPageLink}>
|
||||
<Translate id="changelog.backLink">← Back to index page</Translate>
|
||||
</Link>
|
||||
|
||||
<ChangelogItem
|
||||
frontMatter={frontMatter}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {useLocation} from '@docusaurus/router';
|
|||
|
||||
function SidebarAd() {
|
||||
return (
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
<div style={{border: 'solid thin red', padding: 10, textAlign: 'center'}}>
|
||||
Sidebar Ad
|
||||
</div>
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -6861,6 +6861,14 @@ eslint-module-utils@^2.7.3:
|
|||
debug "^3.2.7"
|
||||
find-up "^2.1.0"
|
||||
|
||||
eslint-plugin-eslint-plugin@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-4.1.0.tgz#40ae944d79e845dc9d4a85328eea3c5bf4ae0f7d"
|
||||
integrity sha512-QJVw+WYXJuG2469gx5G929bz7crfxySDlK1i569FkuT6dpeHDeP7MmDrKaswCx17snG25LRFD6wmVX+AO5x7Qg==
|
||||
dependencies:
|
||||
eslint-utils "^3.0.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-plugin-header@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz#6ce512432d57675265fac47292b50d1eff11acd6"
|
||||
|
@ -13185,6 +13193,11 @@ require-from-string@^2.0.2:
|
|||
resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa"
|
||||
integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==
|
||||
|
||||
requireindex@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
|
||||
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue