# comvi push

Upload local translation files to your Comvi project, preview the diff with --dry-run, and choose how conflicts with existing values are resolved.

Canonical: https://comvi.io/docs/cli/push

Upload the translation files under `translationsPath` to the project so new keys reach
translators.

## Usage

`Terminal`

```bash
comvi push [options]
```

## Options

| Option | Default | Meaning |
| --- | --- | --- |
| `-c, --config <path>` | nearest `.comvirc.json` | Config file to read |
| `-l, --locale <locales>` | `locales` in config, else all detected | Comma-separated BCP-47 codes to upload |
| `-n, --ns <namespaces>` | `namespaces` in config, else all detected | Comma-separated namespaces to upload |
| `-p, --path <path>` | `translationsPath` in config | Source directory |
| `--dry-run` | off | Print how many keys would be created, updated and conflict, upload nothing |
| `--force-mode <mode>` | `push.forceMode` in config, else `ask` | Conflict handling: `override`, `keep`, `ask` or `abort`. Any other value exits `4` |

A flag replaces the config value for that run; the two lists never merge.

## Source layout

`src/locales/`

```text
src/locales/
├── en.json            # default namespace
├── de.json
├── common/
│   ├── en.json
│   └── de.json
└── auth/
    └── en.json
```

`src/locales/common/en.json`

```json
{
"greeting": "Hello, {name}!",
"nav": { "home": "Home" },
"nav.settings": "Settings"
}
```

The layout follows `fileTemplate`, the same one `comvi pull` writes. With the default
template the root files are the default namespace and each directory is another
namespace. A file may be flat or nested: `{"nav": {"home": "Home"}}` uploads the key
`nav.home`, and dots written in a key are kept. The next pull writes that file back flat.

Every leaf must be a string. A number, boolean, `null` or array fails the push with exit
`4` and names the file and key, and so does a key defined both flat and nested. i18next
plural suffixes such as `_one` and `_other` are not converted to ICU; import an i18next
catalog through the dashboard instead.

## Conflicts

`comvi push --dry-run`

```text
📦 Push preview (dry run):
✅ Created: 3 keys
📝 Updated: 1 translations
⚠️  Conflicts: 2 keys

Run with --force-mode override to overwrite remote values.
Run with --force-mode keep to upload only non-conflicting values.
```

A conflict is a key whose platform value differs from the local one. `--force-mode` decides
what happens to it:

| Mode | Behaviour |
| --- | --- |
| `ask` | Prompt for `override`, `keep` or `abort` when at least one conflict exists. The prompt needs a TTY; without one the push fails. A conflict-free push runs unattended |
| `override` | Local values replace the platform values |
| `keep` | Platform values stay; only new keys are created |
| `abort` | Stop at the first conflict; nothing is written, new keys included |

Push never deletes keys. A namespace the project does not have is created and never
becomes the default namespace. A locale the project does not have is skipped without an
error, and so is a locale outside the API key owner's language restrictions: the push
reports success and those values never arrive. Add the locale to the project first.

> **caution**
>
> `--force-mode override` replaces translations that translators may have finished. Use it
> only when the local files are the source of truth, such as the source language after a
> copy change.

## Examples

`Preview, then push the source language`

```bash
comvi push --locale en --dry-run
comvi push --locale en --force-mode keep
```

`.github/workflows/push-translations.yml`

```yaml
on:
push:
    branches: [main]
    paths: ['src/locales/**']
jobs:
push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx comvi push --locale en --force-mode abort
        env:
          COMVI_API_KEY: ${{ secrets.COMVI_API_KEY }}
```

`Push one namespace from a monorepo package`

```bash
comvi push --config ../../.comvirc.json --ns common
```

CI has no TTY, so pick `keep`, `override` or `abort` there. `abort` fails the job the
moment a local value would overwrite translator work; `keep` uploads the new keys and
leaves existing values alone.

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Push done, dry run finished, or no translation files found |
| `1` | Missing `.comvirc.json`, auth failure, network error, `abort` on a conflict, or `ask` without a TTY |
| `4` | Invalid config, an unknown `--force-mode`, a non-string leaf, a key defined both flat and nested, or a `--dotenv` file that does not exist |

## Next steps

- [comvi pull](/docs/cli/pull): download the project back to disk.
- [comvi typegen](/docs/cli/typegen): regenerate types after pushing new keys.
- [Split translations into namespaces](/docs/i18n/namespaces): what a namespace costs the app that loads it.
- [CI/CD integration](/docs/platform/recipes/ci-cd-integration): full pipeline examples.
