# Writing and translations

**Beta.** Translations, `agh strings`, the `--locale` options and the `get_strings` MCP tool are new and may change, including whether each language stays an asset of its own.

agh tracks writing the way it tracks art: someone requests it, a writer or a tool delivers it, a person reviews it, and accepted files are published to the repository with how they were made. Translations are assets too, each one pointing at the asset it translates.

| You want | Kind | Files agh reads |
| --- | --- | --- |
| A whole piece of writing: a lore entry, quest text, an item description, a store blurb | `text` | Markdown (`.md`), plain text (`.txt`), Ink (`.ink`), Yarn Spinner (`.yarn`) |
| Keyed lines the game looks up: UI labels, item names, barks, dialogue lines by id | `strings` | gettext `.po` and `.pot`, `.json` (flat or nested keys, and ARB descriptions), `.csv` with a header row, XLIFF 1.2 and 2.0 (`.xlf`, `.xliff`) |
| The same thing in another language | the source's kind | the source's format |

Tag writing by what it is, such as `role=dialogue` or `role=lore`, rather than with new kinds. Files in other formats, such as a `.docx`, are stored and reviewed as they are, without the line-by-line checks.

## Request writing with its translations

Give a text or strings request the languages to translate it into:

```text
agh request ui.menu.strings --kind strings --path locale/en/menu.po \
      --brief "Main menu labels" --spec max_length=24 --locales fr,de,pt-BR
```

agh adds one translation request per language to the same batch: `ui.menu.strings.fr`, `ui.menu.strings.de` and `ui.menu.strings.pt_br`. Languages are BCP 47 tags such as `fr`, `pt-BR` or `zh-Hans`; the id uses the tag lowercased with `_`. The source is in `en` unless the request gives `--locale`.

A translation goes at the source's path with the language swapped in: `locale/en/menu.po` becomes `locale/fr/menu.po`, `i18n/en.json` becomes `i18n/fr.json`, and `text/menu.en.json` becomes `text/menu.fr.json`. A path that doesn't name the language gets it before the extension, so `lore/prologue.md` becomes `lore/prologue.fr.md`. For another layout, give `--locale-path translations/{locale}/menu.po`.

In a request file the same fields are `locale`, `locales`, `locale_path` and `translation_of`:

```yaml
assets:
  - id: lore.prologue
    kind: text
    path: lore/prologue.md
    brief: The prologue the narrator reads over the opening.
    locales: [fr, ja]
```

To add a language later, request the translation on its own, or choose **Add a locale…** on the source's page in the web app's Library, which shows where each file will go before it asks:

```text
agh request --translation-of ui.menu.strings --locale ja
```

The translation takes its kind, path, tags and spec from the source, and a brief saying what to translate. A request can override the brief, path or notes.

## Waiting for the source

A translation can't be claimed until its source has an accepted file, since there is nothing to translate yet. `agh next`, `agh list --claimable` and guest links skip it, and `agh show` says it is waiting. Approving the batch approves the translations with the source, so they open the moment the source is accepted.

## Translate

```text
agh next --locale fr                     # the next translation into French
agh get ui.menu.strings.fr --which source
agh strings ui.menu.strings.fr           # the source's lines, with comments for translators
agh fill ui.menu.strings.fr ./fr.po --provenance ai-assisted --made-by "DeepL, edited by Jeanne"
```

Over MCP, `claim_next_request` takes `locale`, `get_strings` reads the source line by line, and `deliver_text` delivers the translation. A translation is delivered in the source's format, with the same keys. Translate the text; keep placeholders such as `{name}`, `%s`, `{{count}}` and markup such as `[b]` or `<color=red>` as they are.

Record how it was made: an untouched machine or AI translation is `ai`, a person's edit of one is `ai-assisted`, and a translator's own work is `human`.

## What agh checks

A strings file has to be the format its path names. A `.po` that isn't PO, or a `.json` that isn't JSON, is refused, since nothing could read it.

Everything else is a warning for the reviewer, the way an image's size is checked against its spec. None of it blocks an acceptance:

- keys the translation leaves out, or has that the source doesn't;
- placeholders and markup that differ from the source's line;
- lines longer than the spec's `max_length`, counted in characters as players see them;
- lines left as the source wrote them;
- lines the file itself marks as unfinished, such as PO's `fuzzy` flag or an XLIFF state of `initial`;
- in a source file, empty lines and keys that appear twice.

For writing, agh compares paragraph by paragraph and says when the counts differ.

## Review

`agh strings <asset-id>` (`GET /projects/{project}/assets/{id}/strings`, MCP `get_strings`) shows a file line by line: a source's key, text, comment and flags, or a translation's source text beside its own. `--flagged` shows only the lines with a flag. A source under review marks the lines that differ from its accepted file.

Accepting and rejecting work as for any asset.

In the web app, Review shows writing as text to read, with **Changes** comparing it word by word with the file before it and **Source** showing the file as delivered. A strings file, and any translation, is a table you can narrow to **All**, **Flagged** or **Changed**; it opens on Flagged when anything is flagged. Rejecting a flagged file sends its flags back as the note, unless you write your own.

The Library shows each translation as a language chip on the writing it translates, marked done, in review, out of date, waiting for source or not started. Tick **Translations as tiles** to see them as tiles of their own.

## When the source changes

Accepting a new version of a source keeps its translations in step:

- Each translation accepted or published against the older source reopens as a revision. For strings, the note names the keys added, changed and removed, so the translator redoes only those. A published translation stays in the game until its replacement is accepted and merged.
- A translation waiting for review stays there, and its review says it was made from an older source and marks the lines that changed since.
- A new version that only changes comments, or nothing a translator sees, reopens nothing.

Taking back the source's acceptance (`agh review --undo`) holds its translations again. A source can't be removed while it has translations; remove them first.

## Publishing and the AI disclosure

Translations go in the same pull request as everything else. `.agh/provenance.yaml` lists each translation's `locale` and `translation_of`.

In the AI disclosure, writing in the game's own language counts as story and writing, and translations count as localization, with the counts for each language so the share of AI translation shows per locale.

## Guest links for translators

A guest link can reach one language, so a translator hired outside agh sees only those requests: pick the language on Open requests and choose **Share with a guest**, or send `"locale": "fr"` on `POST /guests`. The guest page fetches the source with `which=source`.

## Strings already in your repository (beta)

`agh adopt` finds strings and writing files already on the branch, including JSON, CSV, Markdown and plain text whose path names a language, such as `i18n/en.json`, `i18n/en-us.json` or `locale/fr/menu.po`. Files at the same path in other languages are adopted as translations of the English one. If it takes a settings file for strings, `agh untrack` drops it and keeps it out of later adoptions. See `agh guide adopt`.
