---
source: https://cli.eloqnt.dev/docs/cli/crowdin
docs_index: https://cli.eloqnt.dev/llms.txt
---

# eloqnt crowdin

Sync messages with an existing Crowdin project.

When localization is outgrowing your repo, a translation management system like Crowdin adds helpful features that enable non-developers to participate in the workflow:

- Integration with 3rd-party services (Figma, CMS, …)
- Collaboration with team members
- Reviews of translations
- Project-wide glossaries
- Screenshots
- … and more

Note that if you offload the translation step to Crowdin, you can still benefit from [`eloqnt lint`](https://cli.eloqnt.dev/docs/cli/lint) and [`eloqnt review`](https://cli.eloqnt.dev/docs/cli/review) before translation work begins.

## crowdin.yml

The sync is driven by a `crowdin.yml` file at your project root, which maps your messages files onto a file-based Crowdin project.

To authenticate the CLI, create a personal access token in your Crowdin account settings, make it available as `CROWDIN_PERSONAL_TOKEN` in your environment.

Then, map your messages to the Crowdin project:

```yaml title="crowdin.yml"
project_id: '123456'
files:
  - source: /messages/en.json
    translation: /messages/%two_letters_code%.json
```

The file follows the [Crowdin configuration](https://support.crowdin.com/developer/configuration-file/) and supports a subset of its options. Unsupported options will either warn or error based on the severity.

### project_id

The numeric ID of your Crowdin project as a string:

```yaml title="crowdin.yml"
project_id: '123456'
```

Alternatively, you can set `project_id_env` to the name of an environment parameter that holds the value (e.g. `CROWDIN_PROJECT_ID`).

### api_token

A personal access token with manager access:

```yaml title="crowdin.yml"
api_token: '<your token here>'
```

Alternatively, you can set `api_token_env` to the name of an environment parameter that holds the value (e.g. `CROWDIN_PERSONAL_TOKEN`).

### base_path

The directory every pattern resolves against (defaults to the enclosing folder of `crowdin.yml`):

```yaml title="crowdin.yml"
base_path: './apps/site'
```

### base_url

The Crowdin API to talk to (for Crowdin Enterprise):

```yaml title="crowdin.yml"
base_url: 'https://acme.api.crowdin.com'
```

### preserve_hierarchy

By default, Crowdin strips any enclosing folders of your messages. With `preserve_hierarchy`, you can rebuild the same structure in your Crowdin project:

```yaml title="crowdin.yml"
preserve_hierarchy: true
```

### files

One entry per set of source files, mapping them to where their translations live:

```yaml title="crowdin.yml"
files:
  - source: /messages/en.json
    translation: /messages/%two_letters_code%.json
```

#### files[].source

A glob matching the files to upload, where a leading slash is relative to `base_path`:

```yaml title="crowdin.yml"
source: /messages/en.json
```

#### files[].translation

Where each translated file lands, built from placeholders:

```yaml title="crowdin.yml"
translation: /messages/%two_letters_code%.json
```

Placeholders for the target language:

- `%language%` for the name (`French`)
- `%language_id%` for the Crowdin id (`fr`)
- `%two_letters_code%` for the ISO 639-1 code (`fr`)
- `%three_letters_code%` for the ISO 639-2/T code (`fra`)
- `%locale%` for the locale (`fr-FR`)
- `%locale_with_underscore%` for the locale with an underscore (`fr_FR`)
- `%android_code%` for the Android code (`fr-rFR`)
- `%osx_code%` for the macOS code (`fr.lproj`)
- `%osx_locale%` for the macOS locale (`fr`)

Placeholders carried over from the source file:

- `%original_file_name%` for the file name (`en.json`)
- `%file_name%` for the name without its extension (`en`)
- `%file_extension%` for the extension (`json`)
- `%original_path%` for the parent folders on the Crowdin project
- `**` for whatever `**` matched in `source`

#### files[].dest

Where the file lands on the Crowdin project, when that should differ from its path in your repo:

```yaml title="crowdin.yml"
preserve_hierarchy: true
files:
  - source: /packages/admin/messages/en.json
    translation: /packages/admin/messages/%locale%.json
    dest: /admin/%original_file_name%
```

The pattern takes the placeholders carried over from the source file, all resolved against the file in your repo. Crowdin requires `preserve_hierarchy: true` alongside `dest`, and a `source` with a glob needs a `dest` that includes a placeholder, so each match keeps its own path.

#### files[].type

The Crowdin file format, needed when the extension doesn't imply it:

```yaml title="crowdin.yml"
source: /messages/en.json
type: chrome
```

#### files[].ignore

Globs to exclude from `source`, written the same way:

```yaml title="crowdin.yml"
ignore:
  - /messages/internal/**/*.json
```

#### files[].languages_mapping

Overrides the code a placeholder substitutes:

```yaml title="crowdin.yml"
languages_mapping:
  two_letters_code:
    pt-BR: pt-br
    zh-CN: zh-cn
    zh-TW: zh-tw
```

#### files[].content_segmentation

Set to `0` to stop Crowdin splitting text into sentence-level strings, which it does for document formats like Markdown and HTML:

```yaml title="crowdin.yml"
content_segmentation: 0
```

#### files[].update_option

Either `update_as_unapproved` or `update_without_changes` depending on what should happen to existing translations when a source string changes:

```yaml title="crowdin.yml"
update_option: update_as_unapproved
```

#### files[].skip_untranslated_strings

Leaves untranslated strings out of a download instead of defaulting them to source text:

```yaml title="crowdin.yml"
skip_untranslated_strings: true
```

## Upload sources

After adding or changing source strings, push your source files to the Crowdin project.

Files are created on the first upload and updated in place afterwards:

```console
$ eloqnt crowdin upload sources

messages/
  en.json  Created

✔ Uploaded 1 source file to Acme
```

### Flags

| Flag | Type | Description |
| --- | --- | --- |
| `--branch` | `string` | Sync with this branch of the Crowdin project (created when missing) |
| `--config` | `string` | Path to the eloqnt config file (defaults to .eloqnt/config.{ts,mts,js,mjs}) |
| `--json` | `boolean` | Output the result as JSON |

## Upload translations

Seed the project with translations that already exist locally.

They arrive unapproved, so reviewers in Crowdin stay in charge:

```console
$ eloqnt crowdin upload translations

messages/
  de.json  Uploaded
  es.json  Uploaded

✔ Uploaded 2 translation files to Acme
```

### Flags

| Flag | Type | Description |
| --- | --- | --- |
| `--branch` | `string` | Sync with this branch of the Crowdin project |
| `--locale` | `string` | Restrict to these target languages (comma-separated) |
| `--config` | `string` | Path to the eloqnt config file (defaults to .eloqnt/config.{ts,mts,js,mjs}) |
| `--json` | `boolean` | Output the result as JSON |

## Download translations

Once translations are ready in Crowdin, pull them into your repo.

Downloading overwrites local translation files with Crowdin's version:

```console
$ eloqnt crowdin download translations

messages/
  de.json  Updated
  es.json  Updated

✔ Downloaded 2 translation files from Acme
```

### Flags

| Flag | Type | Description |
| --- | --- | --- |
| `--branch` | `string` | Sync with this branch of the Crowdin project |
| `--locale` | `string` | Restrict to these target languages (comma-separated) |
| `--config` | `string` | Path to the eloqnt config file (defaults to .eloqnt/config.{ts,mts,js,mjs}) |
| `--json` | `boolean` | Output the result as JSON |

---

For an index of every eloqnt/cli documentation page, see [https://cli.eloqnt.dev/llms.txt](https://cli.eloqnt.dev/llms.txt).
