mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-30 23:08:54 +02:00
516 lines
16 KiB
Text
516 lines
16 KiB
Text
---
|
||
id: crowdin
|
||
slug: /i18n/crowdin
|
||
toc_max_heading_level: 4
|
||
---
|
||
|
||
# i18n - Using Crowdin
|
||
|
||
The i18n system of Docusaurus is **decoupled from any translation software**.
|
||
|
||
You can integrate Docusaurus with the **tools and SaaS of your choice**, as long as you put the **translation files at the correct location**.
|
||
|
||
We document the usage of [Crowdin](https://crowdin.com/), as **one** possible **integration example**.
|
||
|
||
:::warning
|
||
|
||
This is **not an endorsement of Crowdin** as the unique choice to translate a Docusaurus site, but it is successfully used by Facebook to translate documentation projects such as [Jest](https://jestjs.io/), [Docusaurus](https://docusaurus.io/), and [ReasonML](https://reasonml.github.io/).
|
||
|
||
Refer to the **[Crowdin documentation](https://support.crowdin.com/)** and **[Crowdin support](mailto:support@crowdin.com)** for help.
|
||
|
||
:::
|
||
|
||
:::tip
|
||
|
||
Use this **[community-driven GitHub discussion](https://github.com/facebook/docusaurus/discussions/4052)** to discuss anything related to Docusaurus + Crowdin.
|
||
|
||
:::
|
||
|
||
## Crowdin overview {#crowdin-overview}
|
||
|
||
Crowdin is a translation SaaS, offering a [free plan for open-source projects](https://crowdin.com/page/open-source-project-setup-request).
|
||
|
||
We recommend the following translation workflow:
|
||
|
||
- **Upload sources** to Crowdin (untranslated files)
|
||
- Use Crowdin to **translate the content**
|
||
- **Download translations** from Crowdin (localized translation files)
|
||
|
||
Crowdin provides a [CLI](https://support.crowdin.com/cli-tool/) to **upload sources** and **download translations**, allowing you to automate the translation process.
|
||
|
||
The [`crowdin.yml` configuration file](https://support.crowdin.com/configuration-file/) is convenient for Docusaurus, and permits to **download the localized translation files at the expected location** (in `i18n/[locale]/..`).
|
||
|
||
Read the **[official documentation](https://support.crowdin.com/)** to know more about advanced features and different translation workflows.
|
||
|
||
## Crowdin tutorial {#crowdin-tutorial}
|
||
|
||
This is a walk-through of using Crowdin to translate a newly initialized English Docusaurus website into French, and assume you already followed the [i18n tutorial](./i18n-tutorial.mdx).
|
||
|
||
The end result can be seen at [docusaurus-crowdin-example.netlify.app](https://docusaurus-crowdin-example.netlify.app/) ([repository](https://github.com/slorber/docusaurus-crowdin-example)).
|
||
|
||
### Prepare the Docusaurus site {#prepare-the-docusaurus-site}
|
||
|
||
Initialize a new Docusaurus site:
|
||
|
||
```bash
|
||
npx create-docusaurus@latest website classic
|
||
```
|
||
|
||
Add the site configuration for the French language:
|
||
|
||
```js title="docusaurus.config.js"
|
||
module.exports = {
|
||
i18n: {
|
||
defaultLocale: 'en',
|
||
locales: ['en', 'fr'],
|
||
},
|
||
themeConfig: {
|
||
navbar: {
|
||
items: [
|
||
// ...
|
||
{
|
||
type: 'localeDropdown',
|
||
position: 'left',
|
||
},
|
||
// ...
|
||
],
|
||
},
|
||
},
|
||
// ...
|
||
};
|
||
```
|
||
|
||
Translate the homepage:
|
||
|
||
```jsx title="src/pages/index.js"
|
||
import React from 'react';
|
||
import Translate from '@docusaurus/Translate';
|
||
import Layout from '@theme/Layout';
|
||
|
||
export default function Home() {
|
||
return (
|
||
<Layout>
|
||
<h1 style={{margin: 20}}>
|
||
<Translate description="The homepage main heading">
|
||
Welcome to my Docusaurus translated site!
|
||
</Translate>
|
||
</h1>
|
||
</Layout>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Create a Crowdin project {#create-a-crowdin-project}
|
||
|
||
Sign up on [Crowdin](https://crowdin.com/), and create a project.
|
||
|
||
Use English as the source language, and French as the target language.
|
||
|
||

|
||
|
||
Your project is created, but it is empty for now. We will upload the files to translate in the next steps.
|
||
|
||
### Create the Crowdin configuration {#create-the-crowdin-configuration}
|
||
|
||
This configuration ([doc](https://support.crowdin.com/configuration-file/)) provides a mapping for the Crowdin CLI to understand:
|
||
|
||
- Where to find the source files to upload (JSON and Markdown)
|
||
- Where to download the files after translation (in `i18n/[locale]`)
|
||
|
||
Create `crowdin.yml` in `website`:
|
||
|
||
```yml title="crowdin.yml"
|
||
project_id: '123456'
|
||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||
preserve_hierarchy: true
|
||
files:
|
||
# JSON translation files
|
||
- source: /i18n/en/**/*
|
||
translation: /i18n/%two_letters_code%/**/%original_file_name%
|
||
# Docs Markdown files
|
||
- source: /docs/**/*
|
||
translation: /i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%
|
||
# Blog Markdown files
|
||
- source: /blog/**/*
|
||
translation: /i18n/%two_letters_code%/docusaurus-plugin-content-blog/**/%original_file_name%
|
||
```
|
||
|
||
Crowdin has its own syntax for declaring source/translation paths:
|
||
|
||
- `**/*`: everything in a subfolder
|
||
- `%two_letters_code%`: the 2-letters variant of Crowdin target languages (`fr` in our case)
|
||
- `**/%original_file_name%`: the translations will preserve the original folder/file hierarchy
|
||
|
||
:::info
|
||
|
||
The Crowdin CLI warnings are not always easy to understand.
|
||
|
||
We advise to:
|
||
|
||
- change one thing at a time
|
||
- re-upload sources after any configuration change
|
||
- use paths starting with `/` (`./` does not work)
|
||
- avoid fancy globbing patterns like `/docs/**/*.(md|mdx)` (does not work)
|
||
|
||
:::
|
||
|
||
#### Access token {#access-token}
|
||
|
||
The `api_token_env` attribute defines the **env variable name** read by the Crowdin CLI.
|
||
|
||
You can obtain a `Personal Access Token` on [your personal profile page](https://crowdin.com/settings#api-key).
|
||
|
||
:::tip
|
||
|
||
You can keep the default value `CROWDIN_PERSONAL_TOKEN`, and set this environment variable and on your computer and on the CI server to the generated access token.
|
||
|
||
:::
|
||
|
||
:::warning
|
||
|
||
A Personal Access Tokens grant **read-write access to all your Crowdin projects**.
|
||
|
||
You should **not commit** it, and it may be a good idea to create a dedicated **Crowdin profile for your company** instead of using a personal account.
|
||
|
||
:::
|
||
|
||
#### Other configuration fields {#other-configuration-fields}
|
||
|
||
- `project_id`: can be hardcoded, and is found on `https://crowdin.com/project/<MY_PROJECT_NAME>/settings#api`
|
||
- `preserve_hierarchy`: preserve the folder's hierarchy of your docs on Crowdin UI instead of flattening everything
|
||
|
||
### Install the Crowdin CLI {#install-the-crowdin-cli}
|
||
|
||
This tutorial uses the CLI version `3.5.2`, but we expect `3.x` releases to keep working.
|
||
|
||
Install the Crowdin CLI as an npm package to your Docusaurus site:
|
||
|
||
```bash npm2yarn
|
||
npm install @crowdin/cli@3
|
||
```
|
||
|
||
Add a `crowdin` script:
|
||
|
||
```json title="package.json"
|
||
{
|
||
"scripts": {
|
||
// ...
|
||
"write-translations": "docusaurus write-translations",
|
||
"crowdin": "crowdin"
|
||
}
|
||
}
|
||
```
|
||
|
||
Test that you can run the Crowdin CLI:
|
||
|
||
```bash npm2yarn
|
||
npm run crowdin -- --version
|
||
```
|
||
|
||
Set the `CROWDIN_PERSONAL_TOKEN` env variable on your computer, to allow the CLI to authenticate with the Crowdin API.
|
||
|
||
:::tip
|
||
|
||
Temporarily, you can hardcode your personal token in `crowdin.yml` with `api_token: 'MY-TOKEN'`.
|
||
|
||
:::
|
||
|
||
### Upload the sources {#upload-the-sources}
|
||
|
||
Generate the JSON translation files for the default language in `website/i18n/en`:
|
||
|
||
```bash npm2yarn
|
||
npm run write-translations
|
||
```
|
||
|
||
Upload all the JSON and Markdown translation files:
|
||
|
||
```bash npm2yarn
|
||
npm run crowdin upload
|
||
```
|
||
|
||

|
||
|
||
Your source files are now visible on the Crowdin interface: `https://crowdin.com/project/<MY_PROJECT_NAME>/settings#files`
|
||
|
||

|
||
|
||
### Translate the sources {#translate-the-sources}
|
||
|
||
On `https://crowdin.com/project/<MY_PROJECT_NAME>`, click on the French target language.
|
||
|
||

|
||
|
||
Translate some Markdown files.
|
||
|
||

|
||
|
||
:::tip
|
||
|
||
Use `Hide String` to make sure translators **don't translate things that should not be**:
|
||
|
||
- Front matter: `id`, `slug`, `tags` ...
|
||
- Admonitions: `:::`, `:::note`, `:::tip` ...
|
||
|
||

|
||
|
||
:::
|
||
|
||
Translate some JSON files.
|
||
|
||

|
||
|
||
:::info
|
||
|
||
The `description` attribute of JSON translation files is visible on Crowdin to help translate the strings.
|
||
|
||
:::
|
||
|
||
:::tip
|
||
|
||
**[Pre-translate](https://support.crowdin.com/pre-translation-via-machine/)** your site, and **fix pre-translation mistakes manually** (enable the Global Translation Memory in settings first).
|
||
|
||
Use the `Hide String` feature first, as Crowdin is pre-translating things too optimistically.
|
||
|
||
:::
|
||
|
||
### Download the translations {#download-the-translations}
|
||
|
||
Use the Crowdin CLI to download the translated JSON and Markdown files.
|
||
|
||
```bash npm2yarn
|
||
npm run crowdin download
|
||
```
|
||
|
||
The translated content should be downloaded in `i18n/fr`.
|
||
|
||
Start your site on the French locale:
|
||
|
||
```bash npm2yarn
|
||
npm run start -- --locale fr
|
||
```
|
||
|
||
Make sure that your website is now translated in French at [`http://localhost:3000/fr/`](http://localhost:3000/fr/).
|
||
|
||
### Automate with CI {#automate-with-ci}
|
||
|
||
We will configure the CI to **download the Crowdin translations at build time** and keep them outside of Git.
|
||
|
||
Add `website/i18n` to `.gitignore`.
|
||
|
||
Set the `CROWDIN_PERSONAL_TOKEN` env variable on your CI.
|
||
|
||
Create an npm script to `sync` Crowdin (extract sources, upload sources, download translations):
|
||
|
||
```json title="package.json"
|
||
{
|
||
"scripts": {
|
||
"crowdin:sync": "docusaurus write-translations && crowdin upload && crowdin download"
|
||
}
|
||
}
|
||
```
|
||
|
||
Call the `npm run crowdin:sync` script in your CI, just before building the Docusaurus site.
|
||
|
||
:::tip
|
||
|
||
Keep your deploy-previews fast: don't download translations, and use `npm run build -- --locale en` for feature branches.
|
||
|
||
:::
|
||
|
||
:::warning
|
||
|
||
Crowdin does not support well multiple concurrent uploads/downloads: it is preferable to only include translations to your production deployment, and keep deploy previews untranslated.
|
||
|
||
:::
|
||
|
||
## Advanced Crowdin topics {#advanced-crowdin-topics}
|
||
|
||
### MDX {#mdx}
|
||
|
||
:::warning
|
||
|
||
Pay special attention to the JSX fragments in MDX documents!
|
||
|
||
:::
|
||
|
||
Crowdin **does not support officially MDX**, but they added **support for the `.mdx` extension**, and interpret such files as Markdown (instead of plain text).
|
||
|
||
#### MDX problems {#mdx-problems}
|
||
|
||
Crowdin thinks that the JSX syntax is embedded HTML and can mess up with the JSX markup when you download the translations, leading to a site that fails to build due to invalid JSX.
|
||
|
||
Simple JSX fragments using simple string props like `<Username name="Sebastien"/>` will work fine; more complex JSX fragments using object/array props like `<User person={{name: "Sebastien"}}/>` are more likely to fail due to a syntax that does not look like HTML.
|
||
|
||
#### MDX solutions {#mdx-solutions}
|
||
|
||
We recommend extracting the complex embedded JSX code as separate standalone components. We also added an `mdx-code-block` escape hatch syntax:
|
||
|
||
`````text
|
||
# How to deploy Docusaurus
|
||
|
||
To deploy Docusaurus, run the following command:
|
||
|
||
````mdx-code-block
|
||
import Tabs from '@theme/Tabs';
|
||
|
||
import TabItem from '@theme/TabItem';
|
||
|
||
<Tabs>
|
||
<TabItem value="bash" label="Bash">
|
||
|
||
```bash
|
||
GIT_USER=<GITHUB_USERNAME> yarn deploy
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="windows" label="Windows">
|
||
|
||
```batch
|
||
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"
|
||
```
|
||
|
||
</TabItem>
|
||
</Tabs>
|
||
````
|
||
`````
|
||
|
||
This will:
|
||
|
||
- be interpreted by Crowdin as code blocks (and not mess-up with the markup on download)
|
||
- be interpreted by Docusaurus as regular JSX (as if it was not wrapped by any code block)
|
||
- unfortunately opt-out of MDX tooling (IDE syntax highlighting, Prettier...)
|
||
|
||
### Docs versioning {#docs-versioning}
|
||
|
||
Configure translation files for the `website/versioned_docs` folder.
|
||
|
||
When creating a new version, the source strings will generally be quite similar to the current version (`website/docs`), and you don't want to translate the new version docs again and again.
|
||
|
||
Crowdin provides a `Duplicate Strings` setting.
|
||
|
||

|
||
|
||
We recommend using `Hide`, but the ideal setting depends on how much your versions are different.
|
||
|
||
:::warning
|
||
|
||
Not using `Hide` leads to a much larger amount of `source strings` in quotas, and will affect the pricing.
|
||
|
||
:::
|
||
|
||
### Multi-instance plugins {#multi-instance-plugins}
|
||
|
||
You need to configure translation files for each plugin instance.
|
||
|
||
If you have a docs plugin instance with `id=ios`, you will need to configure those source files as well
|
||
|
||
- `website/ios`
|
||
- `website/ios_versioned_docs` (if versioned)
|
||
|
||
### Maintaining your site {#maintaining-your-site}
|
||
|
||
Sometimes, you will **remove or rename a source file** on Git, and Crowdin will display CLI warnings:
|
||
|
||

|
||
|
||
When your sources are refactored, you should use the Crowdin UI to **update your Crowdin files manually**:
|
||
|
||

|
||
|
||
### VCS (Git) integrations {#vcs-git-integrations}
|
||
|
||
Crowdin has multiple VCS integrations for [GitHub](https://support.crowdin.com/github-integration/), GitLab, Bitbucket.
|
||
|
||
:::warning TL;DR
|
||
|
||
We recommend avoiding them.
|
||
|
||
:::
|
||
|
||
It could have been helpful to be able to edit the translations in both Git and Crowdin, and have a **bi-directional sync** between the 2 systems.
|
||
|
||
In practice, **it didn't work very reliably** for a few reasons:
|
||
|
||
- The Crowdin -> Git sync works fine (with a pull request)
|
||
- The Git -> Crowdin sync is manual (you have to press a button)
|
||
- The heuristics used by Crowdin to match existing Markdown translations to existing Markdown sources are not 100% reliable, and you have to verify the result on Crowdin UI after any sync from Git
|
||
- 2 users concurrently editing on Git and Crowdin can lead to a translation loss
|
||
- It requires the `crowdin.yml` file to be at the root of the repository
|
||
|
||
### In-Context localization {#in-context-localization}
|
||
|
||
Crowdin has an [In-Context localization](https://support.crowdin.com/in-context-localization/) feature.
|
||
|
||
:::warning
|
||
|
||
Unfortunately, it does not work yet for technical reasons, but we have good hope it can be solved.
|
||
|
||
Crowdin replaces Markdown strings with technical IDs such as `crowdin:id12345`, but it does so too aggressively, including hidden strings, and messes up with front matter, admonitions, JSX...
|
||
|
||
:::
|
||
|
||
### Localize edit URLs {#localize-edit-urls}
|
||
|
||
When the user is browsing a page at `/fr/doc1`, the edit button will link by default to the unlocalized doc at `website/docs/doc1.md`.
|
||
|
||
You may prefer the edit button to link to the Crowdin interface instead by using the `editUrl` function to customize the edit URLs on a per-locale basis.
|
||
|
||
```js title="docusaurus.config.js"
|
||
const DefaultLocale = 'en';
|
||
|
||
module.exports = {
|
||
presets: [
|
||
[
|
||
'@docusaurus/preset-classic',
|
||
{
|
||
docs: {
|
||
// highlight-start
|
||
editUrl: ({locale, versionDocsDirPath, docPath}) => {
|
||
// Link to Crowdin for French docs
|
||
if (locale !== DefaultLocale) {
|
||
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
|
||
}
|
||
// Link to GitHub for English docs
|
||
return `https://github.com/facebook/docusaurus/edit/main/website/${versionDocsDirPath}/${docPath}`;
|
||
},
|
||
// highlight-end
|
||
},
|
||
blog: {
|
||
// highlight-start
|
||
editUrl: ({locale, blogDirPath, blogPath}) => {
|
||
if (locale !== DefaultLocale) {
|
||
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
|
||
}
|
||
return `https://github.com/facebook/docusaurus/edit/main/website/${blogDirPath}/${blogPath}`;
|
||
},
|
||
// highlight-start
|
||
},
|
||
},
|
||
],
|
||
],
|
||
};
|
||
```
|
||
|
||
:::note
|
||
|
||
It is currently **not possible to link to a specific file** in Crowdin.
|
||
|
||
:::
|
||
|
||
### Example configuration {#example-configuration}
|
||
|
||
The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance:
|
||
|
||
```mdx-code-block
|
||
import CrowdinConfigV2 from '!!raw-loader!@site/../crowdin-v2.yaml';
|
||
import CodeBlock from '@theme/CodeBlock';
|
||
|
||
<CodeBlock className="language-yaml" title="crowdin.yml">
|
||
{CrowdinConfigV2.split('\n')
|
||
// remove comments
|
||
.map((line) => !line.startsWith('#') && line)
|
||
.filter(Boolean)
|
||
.join('\n')}
|
||
</CodeBlock>
|
||
```
|