# Languages and translations

**For administrators.** How Kartotek translates Post/Content Element text, Category and Tag display names, and the site's own interface text — generation, correction, review, and what a visitor actually sees at each stage. See [Welcome to Kartotek](/_docs/kartotek) for the vocabulary this doc assumes. The two languages currently supported are **English** and **Danish**.

## Effective visitor language

Every page resolves to one **effective language**, in this order: an explicit `?lang=` in the address always wins; absent one, a visitor's own stored language preference (set via the header's language selector — see [Explore a Kartotek site](/_docs/browsing)) applies; with neither, the visiting Domain's own configured **default language** applies (see [Domains, UIDs and addresses](/_docs/addressing)). `?lang=original` always shows a Post's own authoritative original language regardless of the Domain default. This same precedence governs a Post's Full page, a Category/Tag listing's Brief cards, a standalone Content Element page, and the map/marker titles on a List/Map collection view alike.

## Post and Content Element translation

Every Post has one authoritative **source language** — the language it was actually written in, chosen when the Post is created and freely changeable while it's still PrePub. Once a Post has ever been Published, correcting its source language is a separate, deliberate action in the admin editor (a "Correct…" control next to the language, requiring confirmation) rather than a plain field edit — correcting it invalidates every translation generated so far (since they were produced against what turns out to have been the wrong assumption) and clears any Reviewed mark those translations had. It never re-generates anything on its own — that's always a fresh, separate action afterward.

**Publishing a Version automatically generates its translations.** The moment a new Version publishes, every translatable field that changed is sent to [DeepL](https://www.deepl.com/) for each configured language other than the Version's own source language, as part of the same publish action. A field that didn't change reuses its already-succeeded translation instead of being re-sent — a translation belongs to the underlying immutable content row, not the Version, so structural sharing across Versions carries a still-valid translation forward for free. Each language is isolated in its own attempt: a failure in one language, or one field, never blocks publishing and never discards a different language's already-succeeded translation. Only a Post's *current* Version is ever translated automatically — a historical Version is never backfilled or retranslated on its own, though it can still be translated from its own Version's admin editor.

A Full Admin can also act on a translation directly, from that specific Post's own foldable **Translations** section, one Version and one target language at a time:

1. **Generate with DeepL** — a confirmed action since it sends content to an external provider, and states up front how many fields it's about to fill (e.g. "Generate (3 missing)"). It always recalculates what's actually missing at the moment it's clicked and fills exactly that — a field that's already translated, manually corrected, or Reviewed is never touched or re-sent, and a field added since the last time is picked up automatically. This is what the automatic publish-time trigger already does on its own; using it manually is mainly a retry for something that previously failed or came back blank, or a way to pick up content a later edit introduced without publishing a whole new Version.
2. **Review & correct** — opens a side-by-side view of every translatable field: the original text next to DeepL's translated text, editable directly, each visibly badged **Present** or **Missing** so it's obvious at a glance which fields still need attention. A field that's been directly corrected here is marked as such, distinct from one still showing DeepL's own output, and stays untouched by a later Regenerate unless the destructive "everything" option is deliberately chosen (see below). Each present field is also checked automatically for anything that looks structurally wrong — a `{{...}}` content reference, a fenced code block, or a Markdown link that the original had but the translation dropped — flagged right there rather than only discovered after publishing. A **Preview** button in this view renders the Post as it would actually look in this language, using whatever's currently saved (including an uncorrected DeepL draft).
3. **Regenerate** — re-sends already-translated fields to DeepL on purpose (a plain re-click of Generate never touches a field that already succeeded, so this is a separate, explicit action requiring its own confirmation before overwriting authored translation). Two deliberate choices, each confirmed: **regenerate uncorrected fields only** (the default — leaves every manually corrected field exactly as it was) or **regenerate everything** (also overwrites manual corrections, with an explicit warning that they'll be lost). Either one always returns the Version's status to **AI translated** at best (see the graded states below).
4. **Mark Reviewed** — once every translatable field this Version holds actually has usable translated content *and* nothing is flagged with a structural issue, a final explicit, confirmed action marks that specific Version's translation into that language as reviewed and correct. Blocked while any required field is still missing.

Non-translatable technical values (a URL, a filename, an identifier) never count toward completeness at all — only actual prose does: Title and excerpt, Text/Markdown bodies, Image/Video/Audio descriptions and credits, Link descriptions, Attachment and Contact-entry descriptions, and Timeline titles.

### Complete, partial, and unavailable states

A Post is in exactly one of five states, per configured language:

- **Original** — this is the Version's own source language; there is nothing to translate.
- **Translation not available** — no translated translatable field is available at all for this language yet (never requested, still generating, or every attempt so far failed) — a visitor asking for it sees the complete, unmodified original instead.
- **Partial AI translation** — at least one translatable field has genuinely translated content, but one or more of this Version's other required fields don't yet. The page itself reflects this directly: every field that's actually translated shows in the target language, and only the fields still missing fall back to the original, field by field — never the whole page collapsing back to 100% original just because one unrelated field (an Image's credit line, say) hasn't finished yet.
- **AI translated** — every translatable field this Version holds has genuinely translated content, but nobody has marked it Reviewed yet.
- **Reviewed** — every translatable field is present, and a Full Admin has explicitly confirmed this exact Version's complete translation into this language.

A translated value that's null, missing, empty, or whitespace-only counts as not present, the same as a field nobody's ever requested a translation for — a provider response that technically "succeeded" but came back blank is retried by Generate exactly like a genuinely missing field, never silently stuck.

**All five states are visitor-viewable — none of them hides the Post.** Since a translation is generated automatically at publish time, a visitor typically sees **AI translated** (or, briefly, **Partial**) content within moments, without waiting on a Full Admin's review; **Reviewed** ("Author Reviewed" in the byline) exists to mark a translation a Full Admin has actually checked, not as a visibility gate. The byline's own Original item discloses which of these applies whenever a visitor is viewing a Post in a language other than its original — "Original: DA, Partial AI translation," "Original: DA, AI Translated," or "Original: DA, Author Reviewed," for example — silent only when the visitor is viewing the original itself, or when the original language happens to match the visitor's own selected language. It is **not** silenced by a Domain's own default language on its own — a Domain whose default matches most of its content's original language still discloses translation status whenever a visitor actively views a translated version, which is exactly when that disclosure is most useful.

**A manual correction invalidates an existing Reviewed mark, the same as a regeneration does** — since it changes what was actually reviewed, that specific Version's translation into that language returns to **AI translated** (or **Partial**, if the correction happened to be the only thing keeping a field present) until Mark Reviewed is clicked again.

**A list/card view (a Category or Tag listing) resolves its own, narrower completeness — still all-or-nothing for the fields it actually shows.** A list/card view only ever shows a Title and a short excerpt — never a media caption, link label, or any of the other fields the Full page's own five states above account for — so it becomes available as soon as *that* Title and excerpt are both genuinely present, even if some other field on the same Post (never displayed in a list) is still incomplete. Title and excerpt always resolve together. This means a Post can legitimately show up translated in a Category listing while its own Full page still reads **Partial** (or even **Translation not available**), until every field finishes; that's expected, not a bug.

**Every new Version needs its own review, even for content it carries forward unchanged.** Structural sharing means an unchanged Text block, caption, or label reuses its already-succeeded translation without calling the provider again — but the Reviewed mark itself belongs to one specific, immutable Version, never to the underlying content. Publishing a new Version of a Post — even one that changes nothing a particular language actually needs re-translated — always starts that Version's translation into every language back at **AI translated** (or **Partial**, if the new Version also introduced a field nothing has translated yet), requiring a fresh, explicit Mark Reviewed click before it carries that status again.

Every Post's PDF snapshot, native JSON export, and JSON-LD metadata (see [Posts and publishing](/_docs/posts) and [Import, export and external publishing](/_docs/json-import)) reflect this too — the PDF/JSON export pipeline and JSON-LD's own `availableLanguage` use the stricter, whole-Version "every field genuinely present" bar (the same bar Reviewed itself requires) before treating a language as a complete, exportable variant.

### Reading a Post in a specific language

Every Post's own address accepts a `?lang=` query — `?lang=original` always shows the authoritative original regardless of the visiting Domain's default language, and `?lang=da-DK` (or `?lang=en`) requests that specific configured language. The language selector in the top-right of every page switches between every configured language directly; the byline's own Original item is a quick way back to a specific Post's original language from wherever you're reading it. Visiting a Post's normal address with no `?lang=` at all shows it in the visiting Domain's default language when at least a Partial translation into it exists, or the original otherwise. A historical Version can be translated too, from its own Version's admin editor — same explicit Generate/Review actions, entirely independent of the current Version's own translation status, and never touched by the automatic publish-time trigger.

## Category and Tag translation

A Category's Name and a Tag's canonical spelling each have their own **Original language**, and each can carry, per configured language, either a hand-typed translation or one **Generated with DeepL** — either way starting as a draft, not yet visible to visitors, until an explicit **Mark Reviewed** step. See [Organizing and presenting content](/_docs/categories)'s "Translated display names" and "Managing Tags" sections for the admin fields and what a visitor sees; the generation/draft/Reviewed mechanics are the same three-step pattern described above for Post content.

## GUI translations

A different translation surface from Post/Content and Category/Tag translation above: it covers the site's own **system-owned text** — button labels, headings, and Type-reference-page wording — not a Post's own authored content or a Category's/Tag's display name. The admin sidebar's **GUI Translations** section lists each piece of interface text ("a reference"), shown with its English source (fixed, sourced from the codebase itself) and one column per configured language. Click a language cell to add or edit that translation by hand, or use its "DeepL" action to draft one automatically — neither ever overwrites a translation that's already there. Unlike a Post's translation, there's no draft/review step here: whatever's saved in a language cell is what visitors in that language see immediately.

Some English source strings carry a `{placeholder}` — for example "captured {date}" — that gets replaced with a real value (a date, a count, a name) at display time. A translation must keep that placeholder's identifier exactly as-is; only the surrounding words may be translated (e.g. Danish "optaget {date}", never "optaget {dato}"). A manual save or a DeepL draft that renames, drops, or adds a placeholder is rejected outright, with an explanation naming the specific placeholder — this also protects against DeepL itself mistranslating one, since it has no way to know `{date}` is a token rather than a word.

## Generation, correction, and Author Review — dashboard and backfill

The admin sidebar's **Post Translations** section is a tile-based dashboard: **Current activity** (Queued, Processing, Partial), **Completed** (Succeeded, Reviewed), **Needs attention** (Failed, Stale, Missing per language, Awaiting review per language — hidden entirely when there's nothing to flag), and **Usage** (total characters sent to the translation provider so far, its own billing unit). **Partial** counts a Post/language where at least one translatable field is genuinely translated but not every one — the same completeness calculation the byline and a Post's own Translations panel use, so this tile's count and what a Post's page actually shows can never disagree. Every count is a link: clicking a tile opens a matrix of exactly the Posts it represents, with the tile's status and language already applied as filters — Post, Stage, Original language, Target language, State, and Last relevant activity, each independently sortable/searchable/filterable/resizable, same shared grid every other Admin list uses. Each row's Post title links straight into that Post's own **Translations** panel. The same **Needs attention** tiles also appear on the main Admin dashboard (see [Dashboard and maintenance](/_docs/dashboard)).

Below the tiles, a **Recent errors** matrix lists individual field-level failures — Post, target language, a short error summary (long provider errors expand on click rather than forcing the page wider, with a Copy action for the full text), when, and current state (Active or Cleared). Each row offers **Retry** (re-sends that Post/language to the translation provider — a success automatically resolves the error) and, for an active error, **Clear** (an explicit acknowledgement that it doesn't need work; requires confirmation and records who cleared it and when). Clearing removes it from the Failed count without marking the underlying translation successful — a still-missing, stale, or not-yet-Reviewed translation keeps appearing in its own correct tile regardless. A later failure for the same Post/language always creates a fresh active error, never a second confusing entry.

This page is otherwise read-only — every Generate/Review action for a single Post happens on that Post's own editor.

### Backfill relationship

Three of the Dashboard's corpus-wide **Backfill operations** (see [Dashboard and maintenance](/_docs/dashboard) for the shared Backfill design every operation follows) handle translation catch-up:

- **Post & Content translations** — an explicit, resumable pass over every Post (any Stage) and every configured non-original language, in bounded batches. It finds every currently-missing translatable field — one never requested, one whose only attempt failed or went stale, and one a provider response came back blank for — and queues it for generation, exactly like a plain Generate action would for one Post, without ever touching an existing translated, manually corrected, or Reviewed value. Safe to pause and resume from wherever it left off (including across a server restart), and safe to re-run over an already-complete corpus, which simply finds nothing left to do.
- **Tag translations** — walks every Tag and, for each configured language other than that Tag's own original, generates a translation **only where none exists at all** — an existing draft, Reviewed, or stale translation is never touched. Generated translations land as drafts, waiting for the same per-Tag Review step as any manually-entered one; the canonical Tag identifier and its `#` presentation are never changed.
- **GUI translation fill** and **GUI placeholder audit** — the bulk operations for system-owned interface text. Fill generates every currently-missing translation for one language at once. The placeholder audit re-checks every already-saved translation for the placeholder rule above — an unambiguous case (a placeholder's name was simply translated) is corrected automatically, and anything less clear-cut is reported for manual review; it also runs automatically every time the site restarts, so previously-bad data doesn't sit broken indefinitely.
