```yaml
id: UJ-016
title: Save a manual string translation
actors: [administrator]
surface: admin-spa
goal: Administrator saves exact target text for one discovered string and fresh detail returns it.
trigger: Enter target text and activate Save at /wp-admin/admin.php?page=international-press-zone#/translations/themes or plugins.
fixtures: [authenticated-admin, discovered-string, active-target-language]
success_state:
  visible: "Translation saved" renders and row status updates.
  durable: String translation row contains exact text, target, translator, and manual flag.
  persistence: Fresh GET /wp-json/international-press-zone/v1/system-translate/strings/{id} returns exact translation.
source_specs: [admin/src/pages/string-translate.js, includes/API/StringTranslateController.php, includes/Core/StringScanner.php]
readiness: draft
canonical: false
blockers: []
```

## Path

| Hop | Evidence |
|---|---|
| UI trigger | `admin/src/pages/string-translate.js:862` |
| Request construction | `admin/src/pages/string-translate.js:951` |
| Endpoint auth and parse | `includes/API/StringTranslateController.php:90` |
| Authoritative write | `includes/Core/StringScanner.php:946` |
| Response | `includes/API/StringTranslateController.php:350` |
| Terminal render | `admin/src/pages/string-translate.js:959` |

## Happy path

### H1
- Setup: Administrator authenticated; discovered string and active target exist; unique target text chosen.
- Action: Enter target text and activate `Save`.
- Request: `PUT /wp-json/international-press-zone/v1/system-translate/strings/{id}`.
- Response: `200` with `{success:true,data:<updated-string>,message:"Translation saved successfully"}`.
- Visible: `Translation saved` renders; row and target tab show translated state.
- Durable: String-translation row contains exact key, domain, target language, submitted text, `translated`, current translator, and manual flag.
- Fresh read: `GET /wp-json/international-press-zone/v1/system-translate/strings/{id}` returns `200` with exact translation.
- Forbidden: Original string, other targets, source files, content, and API jobs remain unchanged.
- Evidence:
  - Setup: `includes/Core/StringScanner.php:924`
  - Action: `admin/src/pages/string-translate.js:862`
  - Request: `admin/src/pages/string-translate.js:951`
  - Response: `includes/API/StringTranslateController.php:350`
  - Visible: `admin/src/pages/string-translate.js:959`
  - Durable: `includes/Core/StringScanner.php:946`
  - Fresh read: `includes/API/StringTranslateController.php:90`
  - Forbidden: `includes/Core/StringScanner.php:946`

## Alternate and failure paths

### A1
- Setup: Administrator authenticated; requested string ID does not exist.
- Action: Submit manual target text for missing ID.
- Request: `PUT /wp-json/international-press-zone/v1/system-translate/strings/{id}`.
- Response: `400` with `{success:false,message:"Failed to save translation"}`.
- Visible: `Failed to save translation` renders; editor remains recoverable.
- Durable: No string-translation row is inserted or updated.
- Fresh read: `GET /wp-json/international-press-zone/v1/system-translate/strings/{id}` returns `404` with string-not-found state.
- Forbidden: No orphan translation, source file, content, or job change occurs.
- Evidence:
  - Setup: `includes/Core/StringScanner.php:936`
  - Action: `admin/src/pages/string-translate.js:862`
  - Request: `admin/src/pages/string-translate.js:951`
  - Response: `includes/API/StringTranslateController.php:341`
  - Visible: `admin/src/pages/string-translate.js:984`
  - Durable: `includes/Core/StringScanner.php:936`
  - Fresh read: `includes/API/StringTranslateController.php:320`
  - Forbidden: `includes/Core/StringScanner.php:936`

## Permissions and boundaries

### P1
- Setup: Actor lacks `manage_options`; string and target otherwise valid.
- Action: Submit manual string translation.
- Request: `PUT /wp-json/international-press-zone/v1/system-translate/strings/{id}`.
- Response: `403` with `{code:"rest_forbidden",message:"Sorry, you are not allowed to do that.",data:{status:403}}`.
- Visible: REST denial exposes no string detail.
- Durable: No string-translation row changes.
- Fresh read: Protected string GET returns permission denial.
- Forbidden: No translation write or source-string disclosure occurs.
- Evidence:
  - Setup: `includes/API/StringTranslateController.php:284`
  - Action: `includes/API/StringTranslateController.php:101`
  - Request: `includes/API/StringTranslateController.php:90`
  - Response: `includes/API/StringTranslateController.php:286`
  - Visible: `includes/API/StringTranslateController.php:286`
  - Durable: `includes/API/StringTranslateController.php:284`
  - Fresh read: `includes/API/StringTranslateController.php:90`
  - Forbidden: `includes/API/StringTranslateController.php:284`

## Source specs

- `admin/src/pages/string-translate.js`
- `includes/API/StringTranslateController.php`
- `includes/Core/StringScanner.php`
