- 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.
The loop
Set up (once)
An owner or admin sets up the project and mints an API token
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: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.Push your source strings
From the repo, upload the code-owned bundles: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.Translate in the dashboard
Translators open the project, pick a language and namespace, and
edit values inline. Each save overwrites just that one value.
(Today only simple string values are editable — plurals and contexts 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.Pull translations back into code
Bring the translators’ work into the repo:
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.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 |
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 (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.
Self-hosting
Don’t have a server yet? Stand one up first.