```yaml
id: UJ-015
title: Queue one string translation
actors: [administrator]
surface: admin-spa
goal: Administrator queues one discovered string for one target language and fresh job state tracks it.
trigger: Activate Generate for missing language in string editor at /wp-admin/admin.php?page=international-press-zone#/translations/themes or plugins.
fixtures: [authenticated-admin, discovered-string, active-target-language, configured-translation-api]
success_state:
  visible: "Translation queued to <language>" renders and target tab becomes processing.
  durable: Exactly one open string job exists for string and target language.
  persistence: Fresh job GET returns exact string, target, and status.
source_specs: [admin/src/pages/string-translate.js, includes/API/TranslateController.php, includes/API/TranslateJobsController.php]
readiness: draft
canonical: false
blockers: []
```

## Path

| Hop | Evidence |
|---|---|
| UI trigger | `admin/src/pages/string-translate.js:845` |
| Request construction | `admin/src/pages/string-translate.js:896` |
| Endpoint auth and parse | `includes/API/TranslateController.php:148` |
| Authoritative write | `includes/API/TranslateController.php:621` |
| Response | `includes/API/TranslateController.php:533` |
| Terminal render | `admin/src/pages/string-translate.js:903` |

## Happy path

### H1
- Setup: Administrator authenticated; discovered string and active missing target exist; API configured; no open duplicate job.
- Action: Open string editor and activate `Generate` for target.
- Request: `POST /wp-json/international-press-zone/v1/translate-async`.
- Response: `200` with `{success:true,jobs:[<job>],already_queued:[]}`.
- Visible: `Translation queued to <language>` renders; target tab becomes processing.
- Durable: One open job links exact string ID, source, target, character count, and processing state.
- Fresh read: `GET /wp-json/international-press-zone/v1/translate-jobs/{id}` returns `200` with exact job.
- Forbidden: No duplicate job, immediate manual translation, source string mutation, or unrelated target request occurs.
- Evidence:
  - Setup: `includes/API/TranslateController.php:555`
  - Action: `admin/src/pages/string-translate.js:845`
  - Request: `admin/src/pages/string-translate.js:896`
  - Response: `includes/API/TranslateController.php:533`
  - Visible: `admin/src/pages/string-translate.js:903`
  - Durable: `includes/API/TranslateController.php:621`
  - Fresh read: `includes/API/TranslateJobsController.php:450`
  - Forbidden: `includes/API/TranslateController.php:597`

## Alternate and failure paths

### A1
- Setup: Administrator authenticated; discovered string and target valid; identical open job exists.
- Action: Activate `Generate` again for target.
- Request: `POST /wp-json/international-press-zone/v1/translate-async`.
- Response: `200` with `{success:true,jobs:[],already_queued:[<target-language>]}`.
- Visible: Queue acknowledgement renders without a second job.
- Durable: Open-job projection remains exactly one row for string and target.
- Fresh read: Job list returns original job ID and no duplicate.
- Forbidden: No duplicate job or second backend translation request occurs.
- Evidence:
  - Setup: `includes/API/TranslateController.php:597`
  - Action: `admin/src/pages/string-translate.js:845`
  - Request: `admin/src/pages/string-translate.js:896`
  - Response: `includes/API/TranslateController.php:533`
  - Visible: `admin/src/pages/string-translate.js:903`
  - Durable: `includes/API/TranslateController.php:597`
  - Fresh read: `includes/API/TranslateJobsController.php:185`
  - Forbidden: `includes/API/TranslateController.php:597`

## Permissions and boundaries

### P1
- Setup: Actor lacks `manage_options`; string and target otherwise valid.
- Action: Submit async string translation.
- Request: `POST /wp-json/international-press-zone/v1/translate-async`.
- Response: `403` with `{code:"rest_forbidden",message:"Sorry, you are not allowed to do that.",data:{status:403}}`.
- Visible: REST denial exposes no string, credential, or job data.
- Durable: No job or translation changes.
- Fresh read: Protected job list returns permission denial.
- Forbidden: No backend request, job insert, translation write, or string disclosure occurs.
- Evidence:
  - Setup: `includes/API/TranslateController.php:961`
  - Action: `includes/API/TranslateController.php:152`
  - Request: `includes/API/TranslateController.php:148`
  - Response: `includes/API/TranslateController.php:963`
  - Visible: `includes/API/TranslateController.php:963`
  - Durable: `includes/API/TranslateController.php:963`
  - Fresh read: `includes/API/TranslateJobsController.php:757`
  - Forbidden: `includes/API/TranslateController.php:963`

## Source specs

- `admin/src/pages/string-translate.js`
- `includes/API/TranslateController.php`
- `includes/API/TranslateJobsController.php`
