mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 10:22:30 +02:00
docs: elaborate on i18n tutorial (#6428)
This commit is contained in:
parent
807b4c2ced
commit
cbcbdaa9d3
4 changed files with 199 additions and 82 deletions
|
@ -419,7 +419,7 @@ When your sources are refactored, you should use the Crowdin UI to **update your
|
|||
|
||||
Crowdin has multiple VCS integrations for [GitHub](https://support.crowdin.com/github-integration/), GitLab, Bitbucket.
|
||||
|
||||
:::warning
|
||||
:::warning TL;DR
|
||||
|
||||
We recommend avoiding them.
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ title: i18n - Using git
|
|||
slug: /i18n/git
|
||||
---
|
||||
|
||||
A **possible translation strategy** is to **version control the translation files** to Git (or any other [VCS](https://en.wikipedia.org/wiki/Version_control)).
|
||||
A **possible translation strategy** is to **version control the translation files** with Git (or any other [VCS](https://en.wikipedia.org/wiki/Version_control)).
|
||||
|
||||
## Tradeoffs {#tradeoffs}
|
||||
|
||||
This strategy has advantages:
|
||||
|
||||
- **Easy to get started**: just add the `i18n` folder to Git
|
||||
- **Easy to get started**: just commit the `i18n` folder to Git
|
||||
- **Easy for developers**: Git, GitHub and pull requests are mainstream developer tools
|
||||
- **Free** (or without any additional cost, assuming you already use Git)
|
||||
- **Low friction**: does not require signing up to an external tool
|
||||
|
@ -19,7 +19,7 @@ This strategy has advantages:
|
|||
Using Git also present some shortcomings:
|
||||
|
||||
- **Hard for non-developers**: they do not master Git and pull-requests
|
||||
- **Hard for professional translations**: they are used to SaaS translation softwares and advanced features
|
||||
- **Hard for professional translators**: they are used to SaaS translation softwares and advanced features
|
||||
- **Hard to maintain**: you have to keep the translated files **in sync** with the untranslated files
|
||||
|
||||
:::note
|
||||
|
@ -30,7 +30,7 @@ Refer to the [Docusaurus i18n RFC](https://github.com/facebook/docusaurus/issues
|
|||
|
||||
:::
|
||||
|
||||
## Git tutorial {#git-tutorial}
|
||||
## Initialization
|
||||
|
||||
This is a walk-through of using Git to translate a newly initialized English Docusaurus website into French, and assume you already followed the [i18n tutorial](./i18n-tutorial.md).
|
||||
|
||||
|
@ -145,7 +145,7 @@ npm run build -- --locale fr
|
|||
|
||||
Follow the same process for each locale you need to support.
|
||||
|
||||
## Maintain the translations {#maintain-the-translations}
|
||||
## Maintenance
|
||||
|
||||
Keeping translated files **consistent** with the originals **can be challenging**, in particular for Markdown documents.
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ The goals of the Docusaurus i18n system are:
|
|||
- **Flexible translation workflows**: use Git (monorepo, forks, or submodules), SaaS software, FTP
|
||||
- **Flexible deployment options**: single, multiple domains, or hybrid
|
||||
- **Modular**: allow plugin authors to provide i18n support
|
||||
- **Low-overhead runtime**: documentation is mostly static and does not require a heavy JS library or polyfills
|
||||
- **Low-overhead runtime**: documentation is mostly static and does not require heavy JS libraries or polyfills
|
||||
- **Scalable build-times**: allow building and deploying localized sites independently
|
||||
- **Localize assets**: an image of your site might contain text that should be translated
|
||||
- **No coupling**: not forced to use any SaaS, yet integrations are possible
|
||||
- **Easy to use with [Crowdin](https://crowdin.com/)**: multiple Docusaurus v1 sites use Crowdin, and should be able to migrate to v2
|
||||
- **Easy to use with [Crowdin](https://crowdin.com/)**: a lot of Docusaurus v1 sites use Crowdin and should be able to migrate to v2
|
||||
- **Good SEO defaults**: we set useful SEO headers like [`hreflang`](https://developers.google.com/search/docs/advanced/crawling/localized-versions) for you
|
||||
- **RTL support**: locales reading right-to-left (Arabic, Hebrew, etc.) are supported and easy to implement
|
||||
- **Default translations**: classic theme labels are translated for you in [many languages](https://github.com/facebook/docusaurus/tree/main/packages/docusaurus-theme-translations/locales)
|
||||
|
@ -33,7 +33,7 @@ The goals of the Docusaurus i18n system are:
|
|||
|
||||
We don't provide support for:
|
||||
|
||||
- **Automatic locale detection**: opinionated, and best done on the [server](../deployment.mdx)
|
||||
- **Automatic locale detection**: opinionated, and best done on the [server (your hosting provider)](../deployment.mdx)
|
||||
- **Translation SaaS software**: you are responsible to understand the external tools of your choice
|
||||
- **Translation of slugs**: technically complicated, little SEO value
|
||||
|
||||
|
@ -49,7 +49,7 @@ Overview of the workflow to create a translated Docusaurus website:
|
|||
|
||||
### Translation files {#translation-files}
|
||||
|
||||
You will work with 2 kinds of translation files.
|
||||
You will work with three kinds of translation files.
|
||||
|
||||
#### Markdown files {#markdown-files}
|
||||
|
||||
|
@ -61,9 +61,9 @@ Markdown and MDX documents are translated as a whole, to fully preserve the tran
|
|||
|
||||
JSON is used to translate:
|
||||
|
||||
- your React code: using the `<Translate>` component
|
||||
- your theme: the navbar, footer
|
||||
- your plugins: the docs sidebar category labels
|
||||
- Your React code: standalone React pages in `src/pages`, or other components
|
||||
- Layout labels provided through `themeConfig`: navbar, footer
|
||||
- Layout labels provided through plugin options: docs sidebar category labels, blog sidebar title...
|
||||
|
||||
The JSON format used is called **Chrome i18n**:
|
||||
|
||||
|
@ -85,6 +85,10 @@ The choice was made for 2 reasons:
|
|||
- **Description attribute**: to help translators with additional context
|
||||
- **Widely supported**: [Chrome extensions](https://developer.chrome.com/docs/extensions/mv2/i18n-messages/), [Crowdin](https://support.crowdin.com/file-formats/chrome-json/), [Transifex](https://docs.transifex.com/formats/chrome-json), [Phrase](https://help.phrase.com/help/chrome-json-messages), [Applanga](https://www.applanga.com/docs/formats/chrome_i18n_json), etc.
|
||||
|
||||
#### Data files
|
||||
|
||||
Some plugins may read from external data files that are localized as a whole. For example, the blog plugin uses an [`authors.yml`](../blog.mdx#global-authors) file that can be translated by creating a copy under `i18n/[locale]/docusaurus-plugin-content-blog/authors.yml`.
|
||||
|
||||
### Translation files location {#translation-files-location}
|
||||
|
||||
The translation files should be created at the correct filesystem location.
|
||||
|
@ -106,33 +110,25 @@ Translating a very simple Docusaurus site in French would lead to the following
|
|||
```bash
|
||||
website/i18n
|
||||
└── fr
|
||||
├── code.json
|
||||
├── code.json # Any text label present in the React code
|
||||
│ # Includes text labels from the themes' code
|
||||
├── docusaurus-plugin-content-blog # translation data the blog plugin needs
|
||||
│ └── 2020-01-01-hello.md
|
||||
│
|
||||
├── docusaurus-plugin-content-blog
|
||||
│ └── 2020-01-01-hello.md
|
||||
├── docusaurus-plugin-content-docs # translation data the docs plugin needs
|
||||
│ ├── current
|
||||
│ │ ├── doc1.md
|
||||
│ │ └── doc2.mdx
|
||||
│ └── current.json
|
||||
│
|
||||
├── docusaurus-plugin-content-docs
|
||||
│ ├── current #
|
||||
│ │ ├── doc1.md
|
||||
│ │ └── doc2.mdx
|
||||
│ └── current.json
|
||||
│
|
||||
└── docusaurus-theme-classic
|
||||
├── footer.json
|
||||
└── navbar.json
|
||||
└── docusaurus-theme-classic # translation data the classic theme needs
|
||||
├── footer.json # Text labels in your footer theme config
|
||||
└── navbar.json # Text labels in your navbar theme config
|
||||
```
|
||||
|
||||
The JSON files are initialized with the [`docusaurus write-translations`](../cli.md#docusaurus-write-translations-sitedir) CLI command.
|
||||
The JSON files are initialized with the [`docusaurus write-translations`](../cli.md#docusaurus-write-translations-sitedir) CLI command. Each plugin sources its own translated content under the corresponding folder, while the `code.json` file defines all text labels used in the React code.
|
||||
|
||||
The `code.json` file is extracted from React components using the `<Translate>` API.
|
||||
|
||||
:::info
|
||||
|
||||
Notice that the `docusaurus-plugin-content-docs` plugin has a `current` subfolder and a `current.json` file, useful for the **docs versioning feature**.
|
||||
|
||||
:::
|
||||
|
||||
Each content plugin or theme is different, and **define its own translation files location**:
|
||||
Each content plugin or theme is different, and **defines its own translation files location**:
|
||||
|
||||
- [Docs i18n](../api/plugins/plugin-content-docs.md#i18n)
|
||||
- [Blog i18n](../api/plugins/plugin-content-blog.md#i18n)
|
||||
|
|
|
@ -4,6 +4,11 @@ title: i18n - Tutorial
|
|||
slug: /i18n/tutorial
|
||||
---
|
||||
|
||||
```mdx-code-block
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
```
|
||||
|
||||
This tutorial will walk you through the basics of the **Docusaurus i18n system**.
|
||||
|
||||
We will add **French** translations to a **newly initialized English Docusaurus website**.
|
||||
|
@ -22,11 +27,24 @@ Use the [site i18n configuration](./../api/docusaurus.config.js.md#i18n) to decl
|
|||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
locales: ['en', 'fr', 'fa'],
|
||||
localeConfigs: {
|
||||
en: {
|
||||
htmlLang: 'en-GB',
|
||||
},
|
||||
// You can omit a locale (e.g. fr) if you don't need to override the defaults
|
||||
fa: {
|
||||
direction: 'rtl',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The locale names are used for the translation files' locations, as well as your translated locales' base URL. When building all locales, only the default locale will have its name omitted in the base URL.
|
||||
|
||||
Docusaurus uses the locale names to provide **sensible defaults**: the `<html lang="...">` attribute, locale label, calendar format, etc. You can customize these defaults with the `localeConfigs`.
|
||||
|
||||
### Theme configuration {#theme-configuration}
|
||||
|
||||
Add a **navbar item** of type `localeDropdown` so that users can select the locale they want:
|
||||
|
@ -58,7 +76,7 @@ npm run start -- --locale fr
|
|||
|
||||
Your site is accessible at **`http://localhost:3000/fr/`**.
|
||||
|
||||
We haven't provided any translation, and the site is **mostly untranslated**.
|
||||
We haven't provided any translation yet, so the site is mostly untranslated.
|
||||
|
||||
:::tip
|
||||
|
||||
|
@ -76,56 +94,99 @@ Each locale is a **distinct standalone single-page application**: it is not poss
|
|||
|
||||
## Translate your site {#translate-your-site}
|
||||
|
||||
The French translations will be added in `website/i18n/fr`.
|
||||
|
||||
Docusaurus is modular, and each content plugin has its own subfolder.
|
||||
All translation data for the French locale is stored in `website/i18n/fr`. Each plugin sources its own translated content under the corresponding folder, while the `code.json` file defines all text labels used in the React code.
|
||||
|
||||
:::note
|
||||
|
||||
After copying files around, restart your site with `npm run start -- --locale fr`.
|
||||
|
||||
Hot-reload will work better when editing existing files.
|
||||
After copying files around, restart your site with `npm run start -- --locale fr`. Hot-reload will work better when editing existing files.
|
||||
|
||||
:::
|
||||
|
||||
### Use the translation APIs {#use-the-translation-apis}
|
||||
### Translate your React code
|
||||
|
||||
Open the homepage, and use the [translation APIs](../docusaurus-core.md#translate):
|
||||
For any React code you've written yourself: React pages, React components, etc., you will use the [**translation APIs**](../docusaurus-core.md#translate).
|
||||
|
||||
Locate all text labels in your React code that will be visible to your users, and mark them with the translation APIs. There are two kinds of APIs:
|
||||
|
||||
- The `<Translate>` component wraps a string as a JSX component;
|
||||
- The `translate()` callback takes a message and returns a string.
|
||||
|
||||
Use the one that better fits the context semantically. For example, the `<Translate>` can be used as React children, while for props that expect a string, the callback can be used.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Before">
|
||||
|
||||
```jsx title="src/pages/index.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
// highlight-start
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout>
|
||||
{/* highlight-next-line */}
|
||||
<h1>Welcome to my website</h1>
|
||||
<main>
|
||||
{/* highlight-start */}
|
||||
You can also visit my
|
||||
<Link to="https://docusaurus.io/blog">blog</Link>
|
||||
{/* highlight-end */}
|
||||
<img
|
||||
src="/img/home.png"
|
||||
// highlight-next-line
|
||||
alt="Home icon"
|
||||
/>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="After">
|
||||
|
||||
```jsx title="src/pages/index.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
// highlight-next-line
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
// highlight-end
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>
|
||||
{/* highlight-start */}
|
||||
{/* highlight-next-line */}
|
||||
<Translate>Welcome to my website</Translate>
|
||||
{/* highlight-end */}
|
||||
</h1>
|
||||
<main>
|
||||
{/* highlight-start */}
|
||||
<Translate
|
||||
id="homepage.visitMyBlog"
|
||||
description="The homepage message to ask the user to visit my blog"
|
||||
values={{blog: <Link to="https://docusaurus.io/blog">blog</Link>}}>
|
||||
{'You can also visit my {blog}'}
|
||||
values={{
|
||||
blogLink: (
|
||||
<Link to="https://docusaurus.io/blog">
|
||||
<Translate
|
||||
id="homepage.visitMyBlog.linkLabel"
|
||||
description="The label for the link to my blog">
|
||||
blog
|
||||
</Translate>
|
||||
</Link>
|
||||
),
|
||||
}}>
|
||||
{'You can also visit my {blogLink}'}
|
||||
</Translate>
|
||||
{/* highlight-end */}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
<img
|
||||
src="/img/home.png"
|
||||
alt={
|
||||
// highlight-start
|
||||
translate({
|
||||
message: 'Hello',
|
||||
description: 'The homepage input placeholder',
|
||||
message: 'Home icon',
|
||||
description: 'The homepage icon alt message',
|
||||
})
|
||||
// highlight-end
|
||||
}
|
||||
|
@ -136,7 +197,10 @@ export default function Home() {
|
|||
}
|
||||
```
|
||||
|
||||
:::caution
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
|
||||
Docusaurus provides a **very small and lightweight translation runtime** on purpose, and only supports basic [placeholders interpolation](../docusaurus-core.md#interpolate), using a subset of the [ICU Message Format](https://formatjs.io/docs/core-concepts/icu-syntax/).
|
||||
|
||||
|
@ -144,13 +208,64 @@ Most documentation websites are generally **static** and don't need advanced i18
|
|||
|
||||
:::
|
||||
|
||||
### Translate JSON files {#translate-json-files}
|
||||
The `docusaurus write-translations` command will statically analyze all React code files used in your site, extract calls to these APIs, and aggregate them in the `code.json` file. The translation files will be stored as maps from IDs to translation message objects (including the translated label and the description of the label). In your calls to the translation APIs (`<Translate>` or `translate()`), you need to specify either the default untranslated message or the ID, in order for Docusaurus to correctly correlate each translation entry to the API call.
|
||||
|
||||
JSON translation files are used for everything that is not contained in a Markdown document:
|
||||
:::caution text labels must be static
|
||||
|
||||
- React/JSX code
|
||||
- Layout navbar and footer labels
|
||||
- Docs sidebar category labels
|
||||
The `docusaurus write-translations` command only does **static analysis** of your code. It doesn't actually run your site. Therefore, dynamic messages can't be extracted, as the message is an _expression_, not a _string_:
|
||||
|
||||
```tsx
|
||||
const items = [
|
||||
{id: 1, title: 'Hello'},
|
||||
{id: 2, title: 'World'},
|
||||
]
|
||||
|
||||
function ItemsList() {
|
||||
return (
|
||||
<ul>
|
||||
{/* DON'T DO THIS: doesn't work with the write-translations command */}
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<Translate>{item.title}</Translate>
|
||||
</li>
|
||||
))}
|
||||
<ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This still behaves correctly at runtime. However, in the future, we may provide a "no-runtime" mechanism, allowing the translations to be directly inlined in the React code through Babel transformations, instead of calling the APIs at runtime. Therefore, to be future-proof, you should always prefer statically analyzable messages. For example, we can refactor the code above to:
|
||||
|
||||
```tsx
|
||||
const items = [
|
||||
{id: 1, title: <Translate>Hello</Translate>},
|
||||
{id: 2, title: <Translate>World</Translate>},
|
||||
]
|
||||
|
||||
function ItemsList() {
|
||||
return (
|
||||
<ul>
|
||||
{/* The titles are now already translated when rendering! */}
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>{item.title}</li>
|
||||
))}
|
||||
<ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can see the calls to the translation APIs as purely _markers_ that tell Docusaurus that "here's a text label to be replaced with a translated message".
|
||||
|
||||
:::
|
||||
|
||||
### Translate plugin data
|
||||
|
||||
JSON translation files are used for everything that is interspersed in your code:
|
||||
|
||||
- React code, including the translated labels you have marked above
|
||||
- Navbar and footer labels in theme config
|
||||
- Docs sidebar category labels in `sidebars.js`
|
||||
- Blog sidebar title in plugin options
|
||||
- ...
|
||||
|
||||
Run the [write-translations](../cli.md#docusaurus-write-translations-sitedir) command:
|
||||
|
@ -159,24 +274,30 @@ Run the [write-translations](../cli.md#docusaurus-write-translations-sitedir) co
|
|||
npm run write-translations -- --locale fr
|
||||
```
|
||||
|
||||
It will extract and initialize the JSON translation files that you need to translate.
|
||||
|
||||
The homepage translations are statically extracted from React source code:
|
||||
It will extract and initialize the JSON translation files that you need to translate. The `code.json` file at the root includes all translation API calls extracted from the source code, which could either be written by you or provided by the themes, some of which may already be translated by default.
|
||||
|
||||
```json title="i18n/fr/code.json"
|
||||
{
|
||||
// No ID for the <Translate> component: the default message is used as ID
|
||||
"Welcome to my website": {
|
||||
"message": "Welcome to my website",
|
||||
"description": "The homepage welcome message"
|
||||
"message": "Welcome to my website"
|
||||
},
|
||||
"Hello": {
|
||||
"message": "Hello",
|
||||
"description": "The homepage input placeholder"
|
||||
"home.visitMyBlog": {
|
||||
"message": "You can also visit my {blog}",
|
||||
"description": "The homepage message to ask the user to visit my blog"
|
||||
},
|
||||
"homepage.visitMyBlog.linkLabel": {
|
||||
"message": "Blog",
|
||||
"description": "The label for the link to my blog"
|
||||
},
|
||||
"Home icon": {
|
||||
"message": "Home icon",
|
||||
"description": "The homepage icon alt message"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Plugins and themes will also write their own **JSON translation files**, such as:
|
||||
Plugins and themes will also write their own JSON translation files, such as:
|
||||
|
||||
```json title="i18n/fr/docusaurus-theme-classic/navbar.json"
|
||||
{
|
||||
|
@ -207,7 +328,7 @@ Official Docusaurus content plugins extensively use Markdown/MDX files and allow
|
|||
|
||||
#### Translate the docs {#translate-the-docs}
|
||||
|
||||
Copy your docs Markdown files to `i18n/fr/docusaurus-plugin-content-docs/current`, and translate them:
|
||||
Copy your docs Markdown files from `docs/` to `i18n/fr/docusaurus-plugin-content-docs/current`, and translate them:
|
||||
|
||||
```bash
|
||||
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current
|
||||
|
@ -216,7 +337,7 @@ cp -r docs/** i18n/fr/docusaurus-plugin-content-docs/current
|
|||
|
||||
:::info
|
||||
|
||||
`current` is needed for the docs versioning feature: each docs version has its own subfolder.
|
||||
Notice that the `docusaurus-plugin-content-docs` plugin always divides its content by versions. The data in `./docs` folder will be translated in the `current` subfolder and `current.json` file. See [the doc versioning guide](../guides/docs/versioning.md#terminology) for more information about what "current" means.
|
||||
|
||||
:::
|
||||
|
||||
|
@ -241,17 +362,13 @@ cp -r src/pages/**.mdx i18n/fr/docusaurus-plugin-content-pages
|
|||
|
||||
:::caution
|
||||
|
||||
We only copy `.md` and `.mdx` files, as pages React components are translated through JSON translation files already.
|
||||
We only copy `.md` and `.mdx` files, as React pages are translated through JSON translation files already.
|
||||
|
||||
:::
|
||||
|
||||
### Use explicit heading ids {#use-explicit-heading-ids}
|
||||
:::tip Use explicit heading ids
|
||||
|
||||
By default, a Markdown heading `### Hello World` will have a generated id `hello-world`.
|
||||
|
||||
Other documents can target it with `[link](#hello-world)`.
|
||||
|
||||
The translated heading becomes `### Bonjour le Monde`, with id `bonjour-le-monde`.
|
||||
By default, a Markdown heading `### Hello World` will have a generated id `hello-world`. Other documents can link it with `[link](#hello-world)`. However, after translation, the heading becomes `### Bonjour le Monde`, with id `bonjour-le-monde`.
|
||||
|
||||
Generated ids are not always a good fit for localized sites, as it requires you to localize all the anchor links:
|
||||
|
||||
|
@ -260,8 +377,6 @@ Generated ids are not always a good fit for localized sites, as it requires you
|
|||
+ [link](#bonjour-le-monde)
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
For localized sites, it is recommended to use **[explicit heading ids](../guides/markdown-features/markdown-features-headings.mdx#explicit-ids)**.
|
||||
|
||||
:::
|
||||
|
@ -334,3 +449,9 @@ It is also possible to deploy each locale as a separate subdomain, assemble the
|
|||
|
||||
- Deploy your site as `fr.docusaurus.io`
|
||||
- Configure a CDN to serve it from `docusaurus.io/fr`
|
||||
|
||||
## Managing translations
|
||||
|
||||
Docusaurus doesn't care about how you manage your translations: all it needs is that all translation files (JSON, Markdown, or other data files) are available in the file system during building. However, as site creators, you would need to consider how translations are managed so your translation contributors could collaborate well.
|
||||
|
||||
We will share two common translation collaboration strategies: [**using git**](./i18n-git.md) and [**using Crowdin**](./i18n-crowdin.mdx).
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue