# Translation workspace

Source: https://docs.externa.qiick.io/docs/translation-workspace

**Translation workspace** is the multi-locale editing UI on the collection item form in externa-core **1.2.1**: one active locale at a time, toolbar completeness, copy/apply helpers, and optional AI translate. It is not a separate form mode — there is no **Translate** toggle and no side-by-side Source/Target panes.

> **Related**
> [Project settings](/docs/project-settings) (content locales) · [Collection items](/docs/collection-items) · [Field types](/docs/field-types) · [AI configuration](/docs/ai-configuration) · [Public CMS API](/docs/public-cms-api) (`locale` / `include_all_translations`)

## Prerequisites

1. Configure **content locales** under Project settings (at least two codes, e.g. `en` + `it`).
2. Mark fields **Translatable** on the collection schema.

The toolbar locale control appears when the item form has fields and more than one content locale. Completeness scores appear when there is also at least one translatable field.

## Toolbar locale

In the item form toolbar (`resources/js/pages/collections/items/form.tsx`):

- A locale dropdown lists each content locale (flag + name).
- When completeness is available, each row shows **filled/total** next to the locale (`data-test="translation-completeness"`).
- Scores come from saved form defaults (`contentDefaults`): a locale slot counts as filled only when the value is non-null and not an empty/whitespace string.
- Choosing a locale sets the shared content locale so every translatable field switches to that locale (`ContentLocaleProvider` via `defaultLocale`).

When the form is editable, the same menu also offers:

| Control (UI label)         | `en.json` key                      | Effect                                                   |
| -------------------------- | ---------------------------------- | -------------------------------------------------------- |
| **Apply to all locales**   | `collections.localized.applyAll`   | Copy the current locale’s values into every other locale |
| **Apply to empty locales** | `collections.localized.applyEmpty` | Same, but only where the target locale slot is empty     |

Saving still writes the usual EAV locale slots — no separate storage path.

## Per-field locale menu

Each translatable field is wrapped by `LocalizedField` (`resources/js/components/collections/localized-field.tsx`), rendered from `resources/js/components/collections/dynamic-item-fields.tsx`. Only the active locale’s control is visible; other locale inputs stay in the DOM (hidden) so they still submit with the form.

| Control (UI label)                  | `en.json` key                                   | When                                        |
| ----------------------------------- | ----------------------------------------------- | ------------------------------------------- |
| **Copy from {locale}**              | `collections.localized.copyFrom`                | Exactly one other locale                    |
| **Copy from…**                      | `collections.localized.copyFromMenu`            | Two or more other locales (opens a submenu) |
| **Apply to all locales**            | `collections.localized.applyAll`                | Copy actions enabled (not readonly)         |
| **Apply to empty locales**          | `collections.localized.applyEmpty`              | Copy actions enabled (not readonly)         |
| **Translate with AI from {locale}** | `collections.localized.translateWithAiFrom`     | AI allowed and exactly one other locale     |
| **Translate with AI from…**         | `collections.localized.translateWithAiFromMenu` | AI allowed and two or more other locales    |

`{locale}` is the **display name** of the source locale (e.g. “English”), not the code. Copy and AI write into the **active** field locale. Nothing is persisted until you save the item.

Busy / feedback strings from the same block: **Translating…**, **Translated**, **No source text to translate**, **AI translation failed**, **AI is offline**.

## AI translate

Per-field AI actions show only when the editor has `can-use-ai` and the field is not readonly. Before translating, the client checks AI status; if offline it shows **AI is offline**.

Externa posts to `POST /ai/translate-fields` (`resources/js/lib/ai-translate.ts` → `AiTranslateFieldsController`), throttled to **20 requests per minute**, up to **50** fields per call (the item form sends **one** field per request). `ContentTranslator` is locked to the **`local`** provider (`#[Provider('local')]`); configure `LOCAL_AI_*` (optional `LOCAL_AI_TRANSLATE_MODEL` for a faster instruct model). TipTap and markdown fields remount with the translated value. Empty source text stays empty and is not sent.

The request is blocking (no streaming). For local thinking models, set `LOCAL_AI_TRANSLATE_MODEL` and keep nginx `fastcgi_read_timeout` high (the Docker image uses **600s**) — see [AI configuration](/docs/ai-configuration).

## Out of scope

- A **Translate** toggle, Source/Target locale pickers, side-by-side locked source + editable target, and **Copy all from {locale}** — those existed briefly in core and were removed before v1.2.0; they are not in 1.2.0 / 1.2.1.
- Form-wide “translate every field with AI” from the toolbar locale menu — AI translate is per-field only.
- Machine translation vendors (DeepL, Google Translate) and Crowdin/Lokalise sync; AI translate uses the configured **local** Externa AI provider, not cloud providers via `AI_DEFAULT_PROVIDER`.
