> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linguolink.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI

> Sync translations and check coverage from your terminal and CI.

The `lingulink` CLI wraps the public REST API for the common developer
loop — pull translations into your repo, push updates back, and gate CI
on coverage. It ships no logic of its own beyond transport; every command
calls the same [public API](/api/api-usage) documented in this section —
`GET /api/v1/pull` and `POST /api/v1/push` are named to match the `pull`
and `push` commands directly.

## Install

```bash theme={null}
npm install -g lingulink
```

Or run it without installing:

```bash theme={null}
npx lingulink --version
```

## Setup

Create an API key from the dashboard (**API Keys**) with the scopes your
workflow needs. Don't know your project's UUID? List every project your
key's organization owns:

```bash theme={null}
curl -H "Authorization: Bearer $LINGULINK_API_KEY" https://linguolink.dev/api/v1/projects
```

Then initialize a config in your project:

```bash theme={null}
export LINGULINK_API_KEY=ll_...
lingulink init --project-id YOUR_PROJECT_ID
```

This writes `lingulink.json` to the current directory:

```json theme={null}
{
  "format": "json",
  "paths": { "pull": "locales/{language}.json" },
  "projectId": "YOUR_PROJECT_ID"
}
```

`{language}` in `paths.pull` is replaced per locale when pulling. Set
`LINGULINK_API_URL` to target a non-default deployment.

### Required scopes

| Command  | Scope(s)                                                           |
| -------- | ------------------------------------------------------------------ |
| `pull`   | `export:read`                                                      |
| `push`   | `translations:write` (plus `translations:read` to poll job status) |
| `status` | `translations:read`                                                |

Scopes are checked literally — `admin` does not implicitly grant them, so
select every scope the command needs when creating the key. See
[API Keys](/api/api-keys) for the full scope list.

## Commands

```bash theme={null}
lingulink pull                    # download all project languages
lingulink pull --lang de tr       # specific languages only
lingulink pull --env production   # apply environment overrides

lingulink push locales/de.json --lang de              # merge-import a JSON file
lingulink push locales/de.json --lang de --dry-run     # preview without writing
lingulink push locales/de.json --lang de --mode replace

lingulink status                  # coverage table per locale
lingulink status --json           # machine-readable output
lingulink status --min 90         # exit 1 if any locale is below 90%
```

`push` accepts JSON files only. Nested objects are flattened into
dot-separated keys (`{"auth":{"title":"Sign in"}}` imports as
`auth.title`); avoid `null` and array values in files you push — see
[Import Translations](/api/import-translations) for the exact semantics.

## Underlying API Requests

Every command is a thin wrapper around the [public API](/api/api-usage) —
useful if you need to reproduce a call with `curl` or reimplement the flow
in another language.

### `pull`

Without `--lang`, `pull` first resolves the project's language list:

```bash theme={null}
curl -H "Authorization: Bearer $LINGULINK_API_KEY" \
  "https://linguolink.dev/api/v1/project?project_id=$PROJECT_ID"
```

Then, per language:

```bash theme={null}
curl -H "Authorization: Bearer $LINGULINK_API_KEY" \
  "https://linguolink.dev/api/v1/pull?project_id=$PROJECT_ID&language=de&format=json&environment=production" \
  -o locales/de.json
```

`environment` is only sent when `--env` is passed.

### `push`

```bash theme={null}
curl -X POST "https://linguolink.dev/api/v1/push" \
  -H "Authorization: Bearer $LINGULINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "'"$PROJECT_ID"'",
    "language": "de",
    "importMode": "merge",
    "dryRun": false,
    "format": "json",
    "content": "{\"auth\":{\"title\":\"Anmelden\"}}"
  }'
```

With `dryRun: true`, the response is an immediate preview —
`{ "dryRun": true, "rowCount", "toCreate", "toUpdate" }` — and no job is
created. Otherwise it's `202 Accepted` with `{ "jobId", "runId" }`, and the
CLI polls the job until it leaves `pending`/`running`:

```bash theme={null}
curl -H "Authorization: Bearer $LINGULINK_API_KEY" \
  "https://linguolink.dev/api/v1/jobs/$JOB_ID"
```

### `status`

```bash theme={null}
curl -H "Authorization: Bearer $LINGULINK_API_KEY" \
  "https://linguolink.dev/api/v1/coverage?project_id=$PROJECT_ID"
```

Returns `{ "data": { "projectName", "baseLanguage", "referenceKeyCount",
"locales": [{ "language", "coveragePercent", "missingCount", ... }] } }` —
`status --min` fails the command if any locale's `coveragePercent` falls
below the threshold.

## CI usage

`status --min` is the building block for a coverage gate:

```yaml theme={null}
- name: Check translation coverage
  env:
    LINGULINK_API_KEY: ${{ secrets.LINGULINK_API_KEY }}
  run: npx lingulink status --min 90
```

## Exit codes

| Code | Meaning                              |
| ---- | ------------------------------------ |
| `0`  | Success                              |
| `1`  | Validation failure, or below `--min` |
| `2`  | Authentication / permission error    |
| `3`  | Network or server error              |

## Next Steps

* Bring data in via [Import Translations](/api/import-translations)
* Wire raw API calls instead in [Integration Examples](/api/integration-examples)
