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

# The team workflow

> The push → edit → pull loop: how code and translators share one source of truth without clobbering each other.

astilba splits ownership of a project cleanly in two, and that split is what lets developers
and translators work the same project without stepping on each other:

* **Code owns the source language.** It's authored in the repo and pushed up.
* **The dashboard owns every other language.** Translators edit there; their work is never
  overwritten by a push.

Everything else follows from that invariant. The loop is **push → edit → pull**.

## The loop

<Steps>
  <Step title="Set up (once)">
    An owner or admin sets up the project and mints an [API token](/guides/orgs-and-tokens)
    from the **dashboard**, which authenticates with your session — no token needed yet:
    create the project with the **New project** form, then mint a token on the **Tokens** page
    (owner/admin only). Export both for the CLI:

    ```sh theme={null}
    export ASTILBA_URL=https://astilba.example.com
    export ASTILBA_TOKEN=…
    ```

    Prefer to create the project from the CLI? `astilba create --project web --name "Web App"`
    does the same — but run it *after* exporting the token, since it authenticates just like
    `push`/`pull` and fails fast without a server and token.
  </Step>

  <Step title="Push your source strings">
    From the repo, upload the code-owned bundles:

    ```sh theme={null}
    npx astilba push --project web ./locales
    ```

    The server applies the ownership policy: the **source language overwrites** (code owns
    it), and **every other language fills only its gaps** — a push never clobbers a
    translation someone wrote in the dashboard. Push is **additive**: removing a key locally
    doesn't delete it on the server. Run with `--dry-run` first to preview the counts.
  </Step>

  <Step title="Translate in the dashboard">
    Translators open the project, pick a language and namespace, and
    [edit values inline](/guides/dashboard). Each save overwrites just that one value.
    (Every string value is editable — including a plural's exploded forms, `item_one`/
    `item_other`, and context keys; only non-string leaves are read-only.)

    A target language shows the keys it already has. Those arrive from a push that includes
    that language, or from a running app registering them via i18next `saveMissing` — a
    source-only push does **not** create empty rows in the target languages, and there's no
    "untranslated source keys" view in the dashboard yet.
  </Step>

  <Step title="Pull translations back into code">
    Bring the translators' work into the repo:

    ```sh theme={null}
    npx astilba pull --project web ./locales
    ```

    `pull` writes one `./locales/<lang>/<ns>.json` per language and namespace — the server
    renders each bundle, so `pull` never re-implements i18next. Commit the result.
  </Step>

  <Step title="Guard fidelity in CI">
    Keep the round-trip honest — fail the build if any locale dropped, added, or altered an
    interpolation placeholder:

    ```yaml theme={null}
    - run: npx astilba check
    ```
  </Step>
</Steps>

## Why push and edit don't fight

The two writers touch disjoint data:

| Writer         | Writes                  | Rule                                       |
| -------------- | ----------------------- | ------------------------------------------ |
| `astilba push` | the **source** language | overwrites (code is the source of truth)   |
| `astilba push` | **target** languages    | fill-only — only keys that don't exist yet |
| dashboard edit | **target** languages    | overwrites one value at a time             |

So a `push` and a dashboard edit never race: `push` only ever overwrites the **source**
language, and a translator's edit to a **target** language can't be undone by the next push
(targets are fill-only). A key you delete locally stays on the server until it's removed there
— pushes never delete.

## Authentication

Everything after setup runs on an **[org API token](/guides/orgs-and-tokens)** (`ASTILBA_TOKEN`,
sent as `x-api-key`). The token is org-scoped: it reaches **every project in the org** by slug,
and another org's slug returns the same `404` as a project that doesn't exist. Mint tokens per
CI system or per developer so you can revoke them individually.

<Card title="Self-hosting" icon="server" href="/guides/self-hosting">
  Don't have a server yet? Stand one up first.
</Card>
