# Localize.to — API for AI assistants This file tells AI coding assistants how to manage localized strings (translations) in this project's web/mobile app via the public write API. Use these endpoints whenever you add, change, or remove user-facing text that should be localized. Do not hardcode strings in the app; write them here and reference them by `lkey` in the UI. ## Base URL All endpoints below live under `/v1`. The full base URL depends on the deployment (ask the human operator for it; common form: `https:///v1`). Projects and apikeys do NOT span environments. A key pair that works against a local dev server identifies a different project than any pair on production — pushing to one does not publish to the other. If the operator points you at a local URL "for testing", assume you're writing into a fresh/empty test project. ## Authentication Every request MUST include the header: x-apikey-write: The `apikey2` (write key) is distinct from the read-only `apikey`. Both are issued per project and both identify the SAME project. Privileges are hierarchical: - `apikey` — reads only. Used by the running app to fetch strings at runtime (`GET /v1/language/?apikey=...`). Safe to bundle with the client. - `apikey2` — reads AND writes. Passing `x-apikey-write: ` authorizes both the write endpoints below AND every read endpoint (`/v1/project`, `/v1/language/...`, `/v1/languages`, ...) — so an AI-driven workflow only needs one key. `apikey2` is the ONLY thing that authorizes writes. Do not put it in query strings, URLs, logs, or source control — keep it in a secret store. Common mistake: using `apikey2` from project A together with `apikey` from project B. PUTs succeed (project A gets the new keys) but later bulk reads (project B) don't show them, and the AI concludes the write silently failed. If a write returned 200 but a subsequent read is missing the key, suspect a mismatched key pair before anything else. Confirm with `GET /v1/translations/` (uses `apikey2`) — if that returns the key, the write landed and the read apikey is wrong. ## The ownership rule (read this carefully) Every key and every translation value carries a boolean `ai` flag. - Rows written by the AI (via these endpoints) are stored with `ai=true`. - Rows written by a human (via the web UI) are stored with `ai=false`. - If a human edits a row the AI previously wrote, it flips to `ai=false` and becomes human-owned from that moment on. Enforcement: - The AI may CREATE new keys and new language values at any time. - The AI may MODIFY or DELETE only rows that are currently `ai=true`. - Any attempt to modify or delete a human-owned (`ai=false`) row returns HTTP 409 and is not applied. Why: the same `lkey` is often shared across iOS/Android/Web builds. A human renaming or deleting a key is a conscious decision; the AI must not silently break sibling apps. Practical consequence: after a human touches a string, the AI should treat that string as read-only. If you need to change it, tell the human and let them edit via the UI. ## Endpoints All paths are under `/v1`. All require `x-apikey-write`. All request/response bodies are JSON. ### GET /v1/translations/:lkey Read one key's current state across all languages. GET /v1/translations/nav.home Response: { "id": 123, "lkey": "nav.home", "description": "Label for the Home link in the top nav", "ai": true, "values": { "en": { "text": "Home", "ai": true }, "ru": { "text": "Главная", "ai": false } } } Inspect `ai` on the key and on each value before deciding whether a subsequent write will be accepted. URL-encode the lkey if it contains characters other than letters, digits, dots, dashes, and underscores. ### PUT /v1/translations Create-or-update one key with any number of language values and/or a description. Idempotent — safe to retry. Body: { "lkey": "nav.home", "description": "Label for the Home link in the top nav", "values": { "en": "Home", "ru": "Главная" } } All three fields are optional except `lkey`. You can send just `{ "lkey", "description" }` to set only the description, or just `{ "lkey", "values": { "en": "..." } }` to set a single language. Query parameter: - `?overwrite=true` (default) — update existing AI-owned values and description if present. - `?overwrite=false` — only fill in missing values/description; leave existing AI-owned content untouched. Behavior: - If the key does not exist: created with `ai=true`. Description (if given) is written. Each provided value row is created with `ai=true`. - If the key exists and is `ai=true`: description is updated (per `overwrite`); for each language, a missing value is created (`ai=true`), an existing AI-owned value is updated (per `overwrite`). - If the key exists and is `ai=false` (human-owned): AI may still ADD new languages (created with `ai=true`). AI may NOT change its description (409). AI may NOT overwrite a human-owned value (409). Response: the full post-update state, same shape as GET. ### DELETE /v1/translations Delete a single language value, or a whole key with all its values. Body: { "lkey": "nav.home" } // delete the whole key { "lkey": "nav.home", "language": "ru" } // delete only one language Behavior: - Deleting a single language succeeds only if that value is `ai=true`. - Deleting the whole key succeeds only if the key is `ai=true` AND every value under it is `ai=true`. If any human-owned value exists, the request returns 409 — delete the AI-owned values first, then retry, or leave the key alone. Response: 201 with empty body on success. ### POST /v1/translations/rename Rename a key. Only allowed when the key is `ai=true`. Body: { "lkey": "nav.home", "new_lkey": "nav.homepage" } Fails with 400 if `new_lkey` already exists in the project. Fails with 409 if the key is human-owned. After renaming, update every code reference to the new lkey. ## Workflows ### Check before you write Free insurance that catches most mistakes: `GET /v1/translations/` before a PUT whenever the lkey might already exist (carried over from a previous AI session, the legacy codebase, or shared with a sibling app). The response tells you whether the key exists, whether it's AI-owned, and which languages already have values — enough to know if your PUT will succeed, 409, or silently no-op under `?overwrite=false`. ### Batch languages into one PUT `PUT /v1/translations` accepts any number of languages in one call. If you have `en` + `ru` + `sk` ready, send them together — one round-trip, one atomic state, one response to verify. Don't loop N requests per key. ### Adding a new UI string 1. Pick a stable, dotted lkey that describes where/what the string is (e.g. `screens.checkout.submit_button`). Keep it short and namespaced by feature. 2. `PUT /v1/translations` with the English text (and any other languages you have). 3. Reference the lkey in the UI code. Do not inline the text. ### Updating a string you previously wrote 1. `GET /v1/translations/` to confirm `values..ai === true`. 2. If true: `PUT /v1/translations` with the new value. Succeeds. 3. If false: a human has taken ownership. Do not attempt to overwrite; surface the change request to the human instead. ### Adding a new language to an existing key Always allowed, whether the key is human- or AI-owned: PUT /v1/translations { "lkey": "nav.home", "values": { "de": "Startseite" } } The new value row is stored with `ai=true` and can be edited/deleted by the AI later. ### Removing a deprecated string 1. First remove every reference from the code. 2. `GET /v1/translations/` — confirm the whole key is AI-owned. 3. `DELETE /v1/translations { "lkey": "..." }`. 4. If any value inside is human-owned, delete just the AI-owned values with `{ "lkey": "...", "language": "..." }` and leave the key. ### Refreshing the app's local dictionary after writes Apps typically bundle a local JSON copy of each language's strings, refreshed by `GET /v1/language/?apikey=`. That endpoint returns the WHOLE project's dictionary for that language — so a naive `curl -o path/.json ...` replaces the file wholesale and drops any lkey that isn't in the target project. - When writing to the same project the app reads from: safe to replace the file. Every lkey the app references is in that project. - When writing to a fresh/test project (e.g. a local dev server): do NOT `-o` over a real dictionary. Pull into a scratch file, diff, and merge the new keys into the real JSON by hand — otherwise you will wipe hundreds of existing human-owned strings from the app's bundle and need a `git restore` to recover. - If a pulled dictionary is unexpectedly tiny, you're almost certainly pointed at the wrong project — verify the read apikey matches the `apikey2` you wrote with (see Authentication). - If your app uses an i18n library that expects nested objects (e.g. `{ auth: { login: ... } }` from an `auth.login` lkey), append `&nested=true` to the read URL. It applies to `format=json`, `format=php`, and the default pretty output; `csv`/`ios`/`android`/`xml` ignore it. Collision rule: if both a leaf (`"auth"`) and a prefix (`"auth.login"`) exist, the leaf wins and the deeper key is dropped. ### Describing a key for future maintainers Set `description` when creating (or later via `PUT` with just `{ lkey, description }`). Good descriptions explain where the string appears and any tone/length constraints — they help humans AND future AI sessions pick the right translation. ## Error handling Standard JSON error shape: { "error": "human-readable message" } Status codes you should handle: - 400 — malformed body, missing field, rename target already exists. - 404 — missing `x-apikey-write` header; key/value does not exist. - 409 — project lookup failed (bad apikey2) OR ownership-rule rejection (row is human-owned). Read the message to tell which. Retry 5xx. Never retry 409 with the same body — it will fail the same way; surface the blockage to the human. ## Non-goals / do not do - Do not paste English text into the app source directly. Use an lkey. - Do not invent numeric ids. The API is lkey-addressed; numeric ids are internal and not part of the contract. - Do not call the JWT-protected `/projects/...` endpoints; those are for the web UI and require a Firebase token you do not have. - Do not store `apikey2` in the repo, in screenshots, or in chat logs.