+ {/* 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}.`}
- Support Ukraine 🇺🇦{' '}
-
- Help Provide Humanitarian Aid to Ukraine
-
- .
+
+
+ Help Provide Humanitarian Aid to Ukraine
+
+
+ ),
+ }}>
+ {'Support Ukraine 🇺🇦 {link}.'}
+