diff --git a/.eslintrc.js b/.eslintrc.js index 86525d0f0c8f..110a6d688601 100644 --- a/.eslintrc.js +++ b/.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 diff --git a/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx b/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx index 42e6d478fbdd..57937216285f 100644 --- a/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/LastUpdated/index.tsx @@ -82,6 +82,7 @@ export default function LastUpdated({ {process.env.NODE_ENV === 'development' && (
+ {/* eslint-disable-next-line @docusaurus/no-untranslated-text */} (Simulated during dev for better perf)
)} diff --git a/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx b/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx index 62a11ab6ae2c..7ca4c62f326d 100644 --- a/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx +++ b/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx @@ -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
Loading...
; } diff --git a/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx b/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx index 28a8f90fe672..9c164ae36739 100644 --- a/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx +++ b/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx @@ -67,6 +67,7 @@ describe('', () => { it('rejects when children is not a string', () => { expect(() => renderer.create( + // eslint-disable-next-line @docusaurus/string-literal-i18n-messages {/* @ts-expect-error: for test */} aaa diff --git a/packages/docusaurus/src/client/theme-fallback/Error/index.tsx b/packages/docusaurus/src/client/theme-fallback/Error/index.tsx index de8ded15b7df..3376d906eaee 100644 --- a/packages/docusaurus/src/client/theme-fallback/Error/index.tsx +++ b/packages/docusaurus/src/client/theme-fallback/Error/index.tsx @@ -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'; diff --git a/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx b/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx index 761c9c3a77b5..79a0823c3194 100644 --- a/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx +++ b/packages/docusaurus/src/client/theme-fallback/Loading/index.tsx @@ -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'; diff --git a/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx b/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx index 15ccfaefe6ab..78f83dcc2c05 100644 --- a/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx +++ b/packages/docusaurus/src/client/theme-fallback/NotFound/index.tsx @@ -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'; diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js new file mode 100644 index 000000000000..81bfbca20171 --- /dev/null +++ b/packages/eslint-plugin/.eslintrc.js @@ -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'], +}; diff --git a/packages/eslint-plugin/.npmignore b/packages/eslint-plugin/.npmignore new file mode 100644 index 000000000000..5f1c98c31f64 --- /dev/null +++ b/packages/eslint-plugin/.npmignore @@ -0,0 +1,6 @@ +copyUntypedFiles.mjs +.tsbuildinfo +tsconfig* +__tests__ + +.eslintrc.js diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 000000000000..842c2dd41410 --- /dev/null +++ b/packages/eslint-plugin/README.md @@ -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). diff --git a/packages/eslint-plugin/lib/index.js b/packages/eslint-plugin/lib/index.js new file mode 100644 index 000000000000..6ca71896088f --- /dev/null +++ b/packages/eslint-plugin/lib/index.js @@ -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', + }, + }, + }, +}; diff --git a/packages/eslint-plugin/lib/rules/__tests__/no-untranslated-text.test.js b/packages/eslint-plugin/lib/rules/__tests__/no-untranslated-text.test.js new file mode 100644 index 000000000000..12e83856bc59 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/__tests__/no-untranslated-text.test.js @@ -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: '·', + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: '· ', + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: ' · ', + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: '· ·', + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: '· — ×', + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: '{"·"}', + options: [{ignoreStrings: ['·']}], + }, + { + code: "{'·'}", + options: [{ignoreStrings: ['·']}], + }, + { + code: '{`·`}', + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: 'Docusaurus', + options: [{ignoreStrings: ['Docusaurus']}], + }, + { + code: '', + options: [{ignoreStrings: ['​']}], + }, + { + code: `<> + {' · '} + `, + options: [{ignoreStrings: ['·', "'"]}], + }, + ], + + invalid: [ + { + code: 'text', + errors: errorsJSX, + }, + { + code: ' text ', + errors: errorsJSX, + }, + { + code: '"text"', + errors: errorsJSX, + }, + { + code: "'text'", + errors: errorsJSX, + }, + { + code: '`text`', + errors: errorsJSX, + }, + { + code: '{"text"}', + errors: errorsJSX, + }, + { + code: "{'text'}", + errors: errorsJSX, + }, + { + code: '{`text`}', + errors: errorsJSX, + }, + { + code: '<>text', + errors: errorsJSXFragment, + }, + { + code: '· — ×', + errors: errorsJSX, + options: [{ignoreStrings: ['·', '—']}], + }, + { + code: '··', + errors: errorsJSX, + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: ' ·· ', + errors: errorsJSX, + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: '"·"', + errors: errorsJSX, + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: "'·'", + errors: errorsJSX, + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: '`·`', + errors: errorsJSX, + options: [{ignoreStrings: ['·', '—', '×']}], + }, + { + code: 'Docusaurus', + errors: errorsJSX, + options: [{ignoreStrings: ['Docu', 'saurus']}], + }, + ], +}); diff --git a/packages/eslint-plugin/lib/rules/__tests__/string-literal-i18n-messages.test.js b/packages/eslint-plugin/lib/rules/__tests__/string-literal-i18n-messages.test.js new file mode 100644 index 000000000000..8b381f4c1f96 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/__tests__/string-literal-i18n-messages.test.js @@ -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: '{text}', + errors: errorsJSX, + }, + { + code: 'Hi {text} my friend', + errors: errorsJSX, + }, + { + code: ' {text} ', + errors: errorsJSX, + }, + { + code: '`{text}`', + errors: errorsJSX, + }, + { + // eslint-disable-next-line no-template-curly-in-string + code: '{`${text}`}', + errors: errorsJSX, + }, + { + code: 'translate({message: metaTitle})', + errors: errorsFunc, + }, + ], +}); diff --git a/packages/eslint-plugin/lib/rules/no-untranslated-text.js b/packages/eslint-plugin/lib/rules/no-untranslated-text.js new file mode 100644 index 000000000000..007996c84bca --- /dev/null +++ b/packages/eslint-plugin/lib/rules/no-untranslated-text.js @@ -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'); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/lib/rules/string-literal-i18n-messages.js b/packages/eslint-plugin/lib/rules/string-literal-i18n-messages.js new file mode 100644 index 000000000000..7b557026b392 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/string-literal-i18n-messages.js @@ -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: + ' 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'); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/lib/util.js b/packages/eslint-plugin/lib/util.js new file mode 100644 index 000000000000..388cbb08196a --- /dev/null +++ b/packages/eslint-plugin/lib/util.js @@ -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: 'text', + }, + { + code: ' text ', + }, + { + code: '"text"', + }, + { + code: "'text'", + }, + { + code: '`text`', + }, + { + code: '{"text"}', + }, + { + code: "{'text'}", + }, + { + code: '{`text`}', + }, + { + code: '{text}', + }, + { + code: ' {text} ', + }, + { + code: 'translate({message: `My page meta title`})', + }, + { + code: ` + Welcome to my website + `, + }, + { + code: ` + {'Welcome, {firstName}! How are you?'} + `, + }, + { + code: `{'This'} is {\`valid\`}`, + }, + { + 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, +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json new file mode 100644 index 000000000000..3fb79ea135b4 --- /dev/null +++ b/packages/eslint-plugin/package.json @@ -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" + } +} diff --git a/project-words.txt b/project-words.txt index 5bd78cccc06e..9a13f6be9954 100644 --- a/project-words.txt +++ b/project-words.txt @@ -244,12 +244,14 @@ refactorings regexes rehype reponame +requireindex retrocompatibility retrocompatible roadmap rocketvalidator rtcts rtlcss +saurus scaleway searchbar sebastien diff --git a/website/docs/api/misc/_category_.yml b/website/docs/api/misc/_category_.yml new file mode 100644 index 000000000000..2fb307376467 --- /dev/null +++ b/website/docs/api/misc/_category_.yml @@ -0,0 +1,2 @@ +label: Miscellaneous +position: 4 diff --git a/website/docs/api/misc/eslint-plugin/README.md b/website/docs/api/misc/eslint-plugin/README.md new file mode 100644 index 000000000000..c42ba573e722 --- /dev/null +++ b/website/docs/api/misc/eslint-plugin/README.md @@ -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: ['·', '—', '×']}, + ], + }, +}; +``` diff --git a/website/docs/api/misc/eslint-plugin/no-untranslated-text.md b/website/docs/api/misc/eslint-plugin/no-untranslated-text.md new file mode 100644 index 000000000000..66dbc2d76497 --- /dev/null +++ b/website/docs/api/misc/eslint-plugin/no-untranslated-text.md @@ -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 +Hello World +``` + +Examples of **correct** code for this rule: + +```js +// Hello World is translated + + Hello World + +``` + +## Rule Configuration {#configuration} + +Accepted fields: + + + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `ignoreStrings` | `string[]` | `[]` | Text labels that only contain strings in this list will not be reported. | + + + +## 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 diff --git a/website/docs/api/misc/eslint-plugin/string-literal-i18n-messages.md b/website/docs/api/misc/eslint-plugin/string-literal-i18n-messages.md new file mode 100644 index 000000000000..9829a4d89bf7 --- /dev/null +++ b/website/docs/api/misc/eslint-plugin/string-literal-i18n-messages.md @@ -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 `` 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 child +{text} + +// Invalid message attribute +translate({message: text}) +``` + +Examples of **correct** code for this rule: + +```js +// Valid child +Some text to be translated + +// Valid message attribute +translate({message: 'Some text to be translated'}) + +// Valid child using values object as prop + + {'Welcome, {firstName}! How are you?'} + + +// 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 diff --git a/website/docs/docusaurus-core.md b/website/docs/docusaurus-core.md index 63be189238f1..372c1a001fe9 100644 --- a/website/docs/docusaurus-core.md +++ b/website/docs/docusaurus-core.md @@ -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) component. Also supporting [placeholders interpolation](#interpolate). diff --git a/website/package.json b/website/package.json index 9106e8e8fc3c..809956eba85b 100644 --- a/website/package.json +++ b/website/package.json @@ -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" diff --git a/website/src/components/ColorGenerator/index.tsx b/website/src/components/ColorGenerator/index.tsx index aedaf469e2bc..0ce0d508e358 100644 --- a/website/src/components/ColorGenerator/index.tsx +++ b/website/src/components/ColorGenerator/index.tsx @@ -279,6 +279,7 @@ export default function ColorGenerator(): JSX.Element {

src/css/custom.css}}> {'Replace the variables in {cssPath} with these new variables.'} diff --git a/website/src/components/UpgradeGuide/index.tsx b/website/src/components/UpgradeGuide/index.tsx index baf895fc775b..0ba1913aae74 100644 --- a/website/src/components/UpgradeGuide/index.tsx +++ b/website/src/components/UpgradeGuide/index.tsx @@ -82,6 +82,7 @@ function VersionNotice() { @canary}}> {'{canaryTag} release'} diff --git a/website/src/data/tweets.tsx b/website/src/data/tweets.tsx index 0a018cada6b2..1e6dd02ba46d 100644 --- a/website/src/data/tweets.tsx +++ b/website/src/data/tweets.tsx @@ -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'; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 02b5780aace6..86de91b0118a 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -90,9 +90,8 @@ function MigrationAnnouncement() { ), }}> - {`Coming from {docusaurusV1Link}? Check out our {migrationGuideLink}`} + {`Coming from {docusaurusV1Link}? Check out our {migrationGuideLink}.`} - . ); @@ -241,11 +240,19 @@ export default function Home(): JSX.Element {

- Support Ukraine 🇺🇦{' '} - - Help Provide Humanitarian Aid to Ukraine - - . + + + Help Provide Humanitarian Aid to Ukraine + + + ), + }}> + {'Support Ukraine 🇺🇦 {link}.'} +
diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx index 931128ff7f01..8df064852eec 100644 --- a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx +++ b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx @@ -59,10 +59,11 @@ export default function ShowcaseFilterToggle(): JSX.Element { }} checked={operator} /> - {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} ); diff --git a/website/src/pages/versions.tsx b/website/src/pages/versions.tsx index 3a66f2307414..c9239d28edb6 100644 --- a/website/src/pages/versions.tsx +++ b/website/src/pages/versions.tsx @@ -170,9 +170,15 @@ export default function Version(): JSX.Element { )}
-

Docusaurus v1 (Legacy)

+

+ + Docusaurus v1 (Legacy) + +

- Here you can find documentation for legacy version of Docusaurus. + + Here you can find documentation for legacy version of Docusaurus. +

diff --git a/website/src/plugins/changelog/theme/ChangelogList/index.tsx b/website/src/plugins/changelog/theme/ChangelogList/index.tsx index f05400ab5756..cf0d7b7297a3 100644 --- a/website/src/plugins/changelog/theme/ChangelogList/index.tsx +++ b/website/src/plugins/changelog/theme/ChangelogList/index.tsx @@ -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,45 +42,60 @@ function ChangelogListContent(props: Props): JSX.Element {

{blogTitle}

- Subscribe through{' '} - - RSS feeds - - - - {' '} - or follow us on{' '} - - Twitter - - - - {' '} - to stay up-to-date with new releases! + + Twitter + + + + + ), + rssLink: ( + + + + RSS feeds + + + + + + + ), + }}> + { + 'Subscribe through {rssLink} or follow us on {twitterLink} to stay up-to-date with new releases!' + } +

{items.map(({content: BlogPostContent}) => ( diff --git a/website/src/plugins/changelog/theme/ChangelogPage/index.tsx b/website/src/plugins/changelog/theme/ChangelogPage/index.tsx index 21aaf20dcbd4..ef45aa304e6b 100644 --- a/website/src/plugins/changelog/theme/ChangelogPage/index.tsx +++ b/website/src/plugins/changelog/theme/ChangelogPage/index.tsx @@ -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 }> - ← Back to index page + + ← Back to index page + Sidebar Ad diff --git a/yarn.lock b/yarn.lock index 0b46b43a2f9d..a6247726e6d9 100644 --- a/yarn.lock +++ b/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"