# Manual Translation Protection and Source-Event Automation Plan

## Goal and non-negotiable contracts

Implement one overwrite-safety policy for post/page/CPT translations and theme/plugin string translations, then route every manual, synchronous, queued, bulk, callback, and source-event generation path through it. Protection and freshness remain independent: a protected target can become stale, but no generated result may replace it until its persisted override is unlocked. Automatic handling defaults to disabled; when enabled its default behavior is `mark_stale`, which must make zero external translation requests.

Use the exact settings, labels, helper text, tooltip copy, REST errors, terminal outcomes, and trigger semantics in the task specification. Do not add model selection, a protection bypass, destructive string cleanup, draft/autosave/revision translation, or backend protocol changes unless existing metadata transport proves insufficient.

Implementation, verification, and delivery are authorized by the user's 2026-08-09 resume instruction. Continue through all gates; preserve partial work with `/handoff` only if an external blocker stops the run.

## Repository reality to account for

Work is under `plugins/international-press-zone/`. This is the current implementation target and already contains the feature design at `docs/specs/2026-08-09-manual-translation-lock-auto-translate-design.md`. `PROJECT-SPECIFICATION.md` is absent; follow the root `CLAUDE.md`, plugin-local `CLAUDE.md`, `.claude/agents/expert.md`, relevant skills, the design document, `docs/ContentManager-Architecture.md`, and `docs/workflow-state-machine.md`.

Current seams to extend rather than recreate:

- `includes/Translation/Settings.php`, `TranslationFinalizer.php`, `JobReceiver.php`, `JobSender.php`, `JobRecorder.php`, `TranslationService.php`, `TranslationBridge.php`, `BulkActions.php`, `MetaBox.php`, and `PostTypeRegistry.php` already exist.
- `includes/API/TranslateController.php` owns `/translate-async`; `TranslationsController.php` owns content detail/manual save and legacy synchronous generation; `StringTranslateController.php` owns string manual save, async Generate All, synchronous filtered generation, and callbacks; `TranslateJobsController.php` exposes job polling.
- `admin/src/editor/translation-metabox.js` and `admin/src/styles/editor/_translation-metabox.scss` already form a separate editor Webpack entry. Extend that surface and `Translation/MetaBox.php`; do not create a second metabox implementation.
- `admin/src/pages/settings.js` currently auto-saves a flat `auto_translate` setting after debounce. Content and string modals already use language tabs and async generation/polling. The string row Generate action auto-starts generation shortly after opening the modal; remove that behavior or gate it on persisted policy before any request.
- `components/Toggle.js` is a usable real switch baseline. `components/Tooltip.js` lacks keyboard/Escape/ARIA linkage, while the `FormField` exported by `components/Input.js` and `Select.js` still use title-only help. Consolidate the duplicate FormField implementations around one accessible tooltip trigger.
- Current settings REST uses `ipz_auto_translate`/`auto_translate_on_publish`; it has no canonical nested automation/protection object. Existing editor-state and content/string detail responses have no lock shape.
- The existing job pipeline and finalizer are the canonical async seam. Store plugin-owned hashes/lock snapshots in its durable metadata and do not change the backend protocol unless that transport is demonstrably insufficient.
- Canonical content completion is `API/TranslateJobsController::{syncProcessingJobs,handleCallback}` into `TranslationFinalizer::finalize()`. The finalizer currently locks by job ID, so change serialization to target identity before creating/updating a target. Canonical string polling/callback persistence is also in `TranslateJobsController::{saveStringTranslationFromBackend,completeStringJobFromCallback,markStringJobCompleted}` and must use the same transaction/policy guard.
- `/translate-async` records active backend jobs in `wp_presszone_international_jobs` through the current bridge/recorder path. `ipz_translation_jobs` is a separate legacy/history path created by `TranslateController::createJobRecord()` with no observed active processor. Extend the canonical table first; keep legacy REST entry points compatible by routing them into the canonical service or policy-skipping them.
- `Translation/JobReceiver.php` is currently unwired, duplicates the callback route, and calls outdated bridge signatures. Prefer retiring/delegating it to `TranslateJobsController` rather than maintaining a second persistence implementation.
- Content manual save currently creates groups with `MAX(group)+1`, writes post/ACF/mapping without one transaction, and leaves target hashes empty/stale. Fold this into `ContentManager` with group-row serialization and the uniqueness migration. Async queue creation currently uses a MySQL advisory lock only for job creation; it is not an overwrite guard.
- Current string persistence has compatibility provenance (`is_auto_translated`, `translated_by`) but no orthogonal stale/lock state. Current content mappings use `content_hash`/`needs_update`; preserve those semantics where they fit.

Load and follow these plugin skills before implementation: `wordpress-php-integration`, `database-operations`, `settings-management`, `admin-panel-fullstack`, `frontend-javascript`, `frontend-styling-scss`, `users-permissions`, `api-integration`, `migration-tools`, `translation-engine`, and `verification`.

## Implementation sequence

### 1. Establish canonical settings and policy value objects

**Create**

- `includes/Translation/TranslationProtectionPolicy.php`
- `includes/Translation/ProtectionDecision.php` (or equivalent enum/value object; do not duplicate string comparisons in callers)
- `tests/unit/Translation/SettingsTest.php`
- `tests/unit/Translation/TranslationProtectionPolicyTest.php`

**Modify**

- `includes/Translation/Settings.php`
- `includes/API/SettingsController.php`
- `includes/Core/Plugin.php`

Implement `Translation\Settings` as the sole accessor/sanitizer for:

```json
{
  "protect_manual_translations": true,
  "auto_translation": {
    "enabled": false,
    "behavior": "mark_stale",
    "triggers": {
      "edit_post": false,
      "new_post": false,
      "edit_page": false,
      "new_page": false,
      "edit_custom_post_type": false,
      "new_custom_post_type": false,
      "edit_update_plugins": false,
      "new_plugins": false,
      "edit_update_themes": false,
      "new_themes": false
    }
  }
}
```

Store the canonical object in one prefixed option (for example `ipz_translation_settings`) and expose typed getters such as `protectManualTranslations()`, `autoTranslationEnabled()`, `behavior()`, and `triggerEnabled()`. Partial updates merge only supplied booleans; reset uses the defaults above. Reject an unknown behavior with a field-specific REST error and reject `enabled=true` with no selected triggers using an actionable field error. Continue accepting `auto_translate`, `auto_translate_on_publish`, and `ipz_auto_translate` as input aliases during compatibility, but map them only to legacy diagnostics/review state: legacy true must never enable the new master or select paid `translate` behavior. Canonical responses return only the new nested fields alongside unrelated existing settings.

`TranslationProtectionPolicy` implements the ordered truth table exactly:

1. explicit `locked` protects;
2. explicit `unlocked` does not;
3. inherited manual or unknown follows the global default;
4. inherited generated remains eligible.

Its completion method returns typed `Allow`, `SkipProtected`, or `SkipTargetChanged`; a changed target fingerprint wins even if unlocked. Treat missing/invalid lock data, required fingerprints, or target state as a closed-policy skip with a diagnostic, never as permission. Keep HTTP formatting and UI messages outside the policy.

Unit-test every override/provenance/global combination, fingerprint combinations including null missing-target snapshots, completion-time precedence, and fail-closed inputs.

### 2. Add resumable schema migration and fresh-install schema

**Create**

- `includes/Migrations/Migration20260809TranslationProtectionAutomation.php`
- `tests/integration/TranslationProtectionMigrationTest.php`

**Modify**

- `includes/Core/Database.php` (bump `DB_VERSION`; make fresh tables match migrated tables)
- `includes/Entities/Translation.php`
- `includes/Core/Migrations.php` only if discovery/resume support needs correction

Add harmless nullable/backward-compatible columns:

- `ipz_translations`: nullable `lock_override` (`NULL` inherit, `1` locked, `0` unlocked), `provenance` (`manual|generated|unknown`), a recorded source fingerprint for each target, and its current target fingerprint. Continue using `content_hash` for canonical current-post hashing where semantics match and `translation_status='needs_update'` for content stale state.
- `ipz_string_translations`: nullable lock override, canonical provenance, separate `is_stale`, recorded source fingerprint, target fingerprint, source-presence state, and a durable link to the stable system-string identity. Do not overload `needs_review`.
- `ipz_system_strings`: a stable source identity independent of original text, source fingerprint, present/absent state, last successful scan generation, and source metadata required to distinguish extension/file/context occurrences.
- Canonical `presszone_international_jobs`: plugin-owned event/job fingerprint metadata, terminal status supporting `completed`, `skipped_protected`, `skipped_target_changed`, and `failed`, retry data, target identity, and unique idempotency key. Keep `ipz_translation_jobs` as compatibility/history data unless its registered legacy endpoints are deliberately routed to the canonical recorder; do not split new guards across both tables. The current status constraint admits only `pending|processing|completed|failed|cancelled`, so migrate it before recording skip outcomes. Bulk string rows span multiple targets: persist per-item identity/fingerprint/outcome metadata in a durable child/detail structure (or normalized job metadata with atomic item updates), while retaining an aggregate parent status for polling; never treat one parent snapshot as permission for every member.
- New `ipz_auto_translation_events`: normalized event payload without source body text, source/extension identity, event type, source fingerprint, stable event/idempotency key, state, outcome counters, retry schedule/count, timestamps, and diagnostics.
- New `ipz_auto_translation_state`: durable reconciliation cursors and last successful extension inventory fingerprints/generations.
- Reuse `ipz_audit_logs` when present, adding structured metadata fields only if needed; never put source/target body text in audit data.

Make the migration idempotent and resumable with durable checkpoints for long backfills. Backfill translation overrides as `NULL`. Derive string provenance as generated when `is_auto_translated=1`, manual where `translated_by` provides durable evidence, otherwise unknown; content provenance is unknown unless existing durable evidence proves otherwise. Preserve legacy auto-translate option values for diagnostics and set a one-time dismissible review-notice flag if either legacy option was true.

Verify the current scanner identity and, where it depends on mutable original text, replace it with a deterministic identity from source type, extension slug, normalized relative file/callsite, domain, and context; hash original text as part of the source fingerprint, not identity. During migration, map old string translations to all matching canonical system-string identities deterministically, copying metadata/text where one legacy shared key maps to multiple sources rather than dropping data.

Before adding unique `(element_type, translation_group_id, language_code)`, find duplicate content mappings, choose the lowest mapping ID as canonical, keep every post, move each extra mapping into a deterministic recovery group, and write an audit entry keyed so retries cannot repeat the move. Then add the index. For string rows, enforce one row per stable system string/language after preserving ambiguous legacy text. Down migration must not drop text/posts/tables; leave added metadata inert.

Test clean install, representative old schemas, interruption/resume at each checkpoint, rerun idempotency, provenance derivation, legacy notice state, ambiguous string mapping, and duplicate group recovery preserving every post.

### 3. Extend persistence owners and centralize fingerprints/atomic writes

**Create**

- `includes/Translation/TranslationAudit.php`
- `tests/unit/Translation/FingerprintTest.php`
- `tests/integration/TranslationPersistenceTest.php`

**Modify**

- `includes/Translation/TranslationFinalizer.php`
- `includes/Core/ContentManager.php`
- `includes/Core/StringScanner.php`
- `includes/Entities/Translation.php`
- `includes/Compatibility/ACFIntegration.php` only to reuse/clarify the existing `ipz_translatable_meta_keys` contract

Extend `ContentManager` rather than creating a one-implementation repository. Add methods to resolve a target identity by source/group/language, read and atomically update lock/provenance, mark existing targets stale, list missing/stale targets, and read source/target fingerprints in batches. Update `generateContentHash()` to hash a canonical structure: title, excerpt, content, then configured translatable meta/ACF values from `ipz_translatable_meta_keys` in stable key/value order with deterministic recursive normalization. Ignore timestamps, status-only fields, revisions, IDs, and unrelated plugin metadata.

Extend `StringScanner` as the string persistence owner: complete a scan into a generation, compare source fingerprints, mark disappeared identities absent without deleting rows, expose missing/stale targets, and save manual/generated translations with lock/provenance/fingerprints. Only publish a new successful extension inventory fingerprint after the full scoped scan succeeds; a failed scan retains prior inventory/fingerprint and changes no freshness state.

Extend `TranslationFinalizer` as the only generated-result persistence seam for both domains. Queue/synchronous callers pass target identity and queued source/target fingerprints; callbacks cannot pass lock changes. In one database transaction:

1. acquire a cross-process row lock (`SELECT ... FOR UPDATE`) on the target mapping/string row; for a missing content target, first lock the source group row and rely on the new group/language unique key;
2. reload current source fingerprint, target fingerprint, override, provenance, and global setting;
3. skip when the source snapshot is superseded, the target is protected, the target changed, or required state is uncertain;
4. on allow, persist post fields/ACF or string text, set provenance generated without changing explicit lock override, record accepted source/current target fingerprints, and clear stale state;
5. commit the terminal outcome and audit event atomically enough that a retry is idempotent.

A manual content/string save performs target text, `provenance=manual`, optional override, accepted source fingerprint, target fingerprint, and audit entry in one transaction. Omitted override preserves existing state; a new row uses inherit. Direct editor saves use this same persistence method. Avoid process-local mutexes; a request-local “generated write in progress” guard is acceptable only to prevent lifecycle-hook recursion, never as concurrency protection.

### 4. Put every existing generation path behind queue- and completion-time policy

**Modify**

- `includes/API/TranslationsController.php`
- `includes/API/TranslateController.php`
- `includes/API/StringTranslateController.php`
- `includes/API/TranslateJobsController.php`
- `includes/Translation/JobSender.php`
- `includes/Translation/JobReceiver.php` (retire/delegate; do not register a duplicate callback)
- `includes/Translation/JobRecorder.php`
- `includes/Translation/TranslationService.php`
- `includes/Translation/TranslationBridge.php`
- `includes/Translation/BulkActions.php`
- `includes/Services/TranslationJobService.php` and `includes/API/TranslationJobsRestController.php` only where their legacy routes remain registered; delegate to canonical policy/job services
- `includes/Performance/QueueManager.php` and `includes/Performance/AsyncJobProcessor.php` only if still reachable after route inventory; no independent generated persistence
- `includes/Admin/JobsRestController.php` if it formats job summaries
- `includes/Core/Plugin.php` dependency construction

Before any external API call or queue insertion, resolve the target and run policy. For single known protected targets, return HTTP 409, code `translation_protected`, only target identity and effective lock, and the unlock-before-generation message. Do not expose text. For missing targets snapshot target fingerprint as null. Include all required job metadata (`trigger_event_id`, source fingerprint, target fingerprint at queue, target identity, behavior, UTC requested time).

Refactor synchronous content generation in `TranslationsController::generateTranslation()` and its content/string helpers to call the same finalizer in the request transaction. Refactor manual `saveContentTranslation()` to use `ContentManager` instead of duplicating direct group SQL and to accept/validate `lock_override`. Include canonical `lock` in list/detail/save responses.

Refactor string single generation, `StringTranslateController::{generateAll,generateAllAsync}`, `force_retranslate`, and the polling/callback methods in `TranslateJobsController` to pre-filter protected targets, snapshot metadata per item, and finalize each item independently. Persist callback job metadata durably rather than relying only on expiring callback-secret transients; retain HMAC authentication. A callback payload may supply generated text/job identity only, never lock fields. `force_retranslate` means regenerate eligible targets, not bypass policy.

Make `/translate-async`, editor generation, content bulk/Generate All, `JobSender`, `TranslateJobsController`, and every still-registered legacy queue endpoint snapshot policy/fingerprints and route content persistence through the existing `TranslationFinalizer`. Route string polling and callbacks through an equivalent guarded finalization method owned by the same deep policy seam. Retire/delegate the unwired `JobReceiver` instead of registering its duplicate callback. Preserve current backend dispatch/polling contracts and put plugin-owned metadata in `presszone_international_jobs`. Any acceleration layer may dispatch work, but canonical metadata/outcomes/idempotency must remain in MySQL so failover does not lose guards.

Single skips are policy conflicts; batch requests remain successful and return per-item outcomes plus distinct counts for translated, marked stale, protected, changed while queued, and failed. Policy skips are terminal and are never retried as failures. External/transient failures follow bounded existing retry policy.

Integration-test content modal generation, Generate All/bulk queue endpoints, translation-jobs single/bulk, string single/Generate All/force mode, synchronous generation, async worker, and HMAC callback. Include queue-then-lock, queue-then-manual-edit, source-changes-twice, duplicate callback, concurrent missing target, and unknown-state fail-closed races.

### 5. Build the normalized coordinator and durable reconciliation

**Create**

- `includes/Translation/SourceChangeEvent.php`
- `includes/Translation/AutoTranslationCoordinator.php`
- `includes/Translation/AutoTranslationOutcome.php`
- `tests/unit/Translation/AutoTranslationCoordinatorTest.php`
- `tests/integration/AutoTranslationCoordinatorIntegrationTest.php`

**Modify**

- `includes/Core/Plugin.php`
- the canonical durable job service selected in step 4

`SourceChangeEvent` contains event ID/type, source domain and stable identity, prior/current fingerprint, persisted-change timestamp, and extension scan scope where relevant—never translated body content. Use a stable idempotency key derived from event class, source identity, and current fingerprint. Store events before dispatch and use a unique key to coalesce repeated hooks/retries.

`AutoTranslationCoordinator::handle()` must:

1. reject disabled, unselected, unchanged, unsupported, or no-language events with a durable no-op outcome;
2. enumerate every configured active target language except source/default;
3. compute missing and stale targets using persistence owners;
4. in `mark_stale`, update existing affected targets only and prove no API/job client is invoked;
5. in `translate`, mark changed existing targets stale, queue missing targets and unlocked stale targets, and count protected targets without failing the event;
6. snapshot source/target fingerprints and link jobs to the event;
7. persist counts `missing`, `stale`, `queued`, `protected`, `unchanged`, `failed` and audit trigger/skip reasons.

`reconcile(cursor,batchSize)` uses internal bounded batches, durable cursors, and source fingerprints only. It emits the same normalized events as primary hooks, resets/advances a cursor transactionally, schedules exponential retry for transient errors with a cap, and never retries policy skips. Reconciliation covers eligible published content and extension inventory changes; it never triggers external translation in stale-only mode. Surface exhausted failures through an existing admin diagnostics/notice path using IDs/reasons only.

Register cron intervals/actions in `Plugin`, schedule one recurring reconciliation event, and unschedule it on deactivation. Do not expose batch tuning in the UI.

### 6. Add thin WordPress lifecycle adapters

**Create**

- `includes/Translation/Triggers/ContentChangeTrigger.php`
- `includes/Translation/Triggers/ExtensionChangeTrigger.php`
- `tests/unit/Translation/Triggers/ContentChangeTriggerTest.php`
- `tests/unit/Translation/Triggers/ExtensionChangeTriggerTest.php`
- `tests/integration/SourceTriggerIntegrationTest.php`

**Modify**

- `includes/Core/Plugin.php`

`ContentChangeTrigger` classifies persisted events using `transition_post_status`/`post_updated` (and post-meta/ACF completion where needed), then emits only after canonical source fingerprint comparison. It must distinguish first publish from edit for standard posts, pages, and configured translatable CPTs; reject drafts, autosaves, revisions, trash, translated target posts, unsupported types, status-only changes, and plugin-generated writes. ACF/meta changes must be included after values are persisted. Map only to the six exact trigger keys/labels.

`ExtensionChangeTrigger` interprets successful `upgrader_process_complete` install/update results per extension, coalesces multi-package calls by extension, and never treats activation/deactivation/theme switch as install/update. For WordPress plugin/theme file-editor AJAX saves, snapshot the scoped filesystem/inventory fingerprint before core handling and check it at shutdown; emit only when the write completed and fingerprint changed, so failed saves produce no event. Scope each scan to the changed slug. Map install vs update/file-edit to the four exact extension trigger keys.

Remove the current automation implications from activation/deactivation and theme-switch hooks. Retain explicit/manual/initial scanning as inventory maintenance only, with no generation, and route successful updater scans through the adapter/coordinator. Test all ten classifiers and every exclusion, failed/rollback updater results, unchanged files, repeated hooks, activation-only, multi-package coalescing, and extension-scoped scans.

### 7. Expose canonical REST lock/settings contracts and audit notice

**Modify**

- `includes/API/SettingsController.php`
- `includes/API/TranslationsController.php`
- `includes/API/StringTranslateController.php`
- `includes/API/TranslationJobsRestController.php`
- `includes/API/PostsController.php` for canonical per-target lock data in `/posts/{id}/editor-state`
- `includes/Core/Plugin.php` or the existing admin-notice registration seam

Settings GET/update/reset use `Translation\Settings`, preserve unrelated current settings, sanitize every nested boolean/enum, and return canonical shape. Add a dismissible admin notice endpoint/action for legacy-true review state with capability and nonce checks; dismissal must not enable automation.

Content/string detail rows expose:

```json
{"lock":{"override":"inherit","effective":true,"provenance":"manual"}}
```

Manual-save payloads may include `lock_override`; reject unknown values with HTTP 400/code `invalid_lock_override`, preserve state when omitted, and use the same capability as text editing. Generation conflict/batch shapes follow the specification. Job status endpoints expose skipped terminal outcomes and summaries. Audit actor, target identity, old/new override, provenance transition, trigger type, and skip reason, never bodies.

Add REST integration tests for permissions, booleans/enums, partial update, reset, aliases, no-trigger validation, notice dismissal, canonical lock shape, invalid override, protected 409, and mixed batch outcomes.

### 8. Implement dashboard settings, accessible tooltips, status, and lock controls

**Modify**

- `admin/src/pages/settings.js`
- `admin/src/pages/content-translate.js`
- `admin/src/pages/string-translate.js`
- `admin/src/components/Toggle.js`
- `admin/src/components/Input.js`
- `admin/src/components/FormField.js` (currently unused; consolidate or remove duplication)
- `admin/src/components/Tooltip.js`
- `admin/src/components/Select.js`
- `admin/src/components/Checkbox.js`
- `admin/src/components/Modal.js`
- `admin/src/utils/api.js` only as needed to preserve `code`, HTTP status, and structured outcome data
- `admin/src/components/index.js`
- `admin/src/styles/_forms.scss`
- `admin/src/styles/_settings-shared.scss`
- `admin/src/styles/pages/_settings.scss`
- `admin/src/styles/pages/_translations.scss`
- `admin/src/styles/main.scss` if new partial imports are required

Replace the old “Auto-translate on Publish” flat toggle with two cards:

- `Protect manual translations` default on, with the exact visible helper and tooltip text.
- Automation with exact `Enable Auto-translation:` label, behavior radios/select, exact behavior explanations, and ten checkboxes grouped Content/Plugins/Themes with literals exactly as specified (including `Edit\Update Plugins`, capitalization, and slash differences). Keep all controls visible/disabled when master is off; enabling with no selected trigger renders an inline error and blocks auto-save/server save.

Build a real tooltip trigger button component. Assign stable IDs, accessible button names, set/remove `aria-describedby` while the `role=tooltip` element is visible, open on pointer hover and keyboard focus/touch activation, close on leave/blur/Escape/outside tap, avoid focus theft, and support narrow viewports. Remove title-only help from these new controls. Style via SCSS only with theme-controlled `body.dark-mode`, contrast-safe tokens, `:focus-visible`, and reduced motion.

In both manual modals, render the per-language lock control adjacent to Save with exact helper/tooltip copy. Checkbox reflects effective state; display `Uses global default` vs `Custom override`; `Use global default` restores inherit. A user toggle changes local pending override, and generation stays disabled until that override is saved with text. Use `Protect this manual translation` for manual provenance and `Protect this translation` otherwise. Do not infer permission from UI; consume canonical server lock.

Add text-backed `Protected` and stale statuses (allow both together), disable known protected single Generate actions, and keep mixed bulk actions available. Render the exact protected toast and separate result counts for Translated, Marked stale, Protected, Changed while queued, and Failed. Update content Generate/Generate All/bulk and string Generate/Generate All/force result handling to consume structured outcomes rather than generic success/failure.

Because the admin package has no JS unit-test runner, either add a minimal configured test runner for pure settings/tooltip state helpers or keep behavior covered by PHP integration plus remote Playwright; do not claim nonexistent `npm test` coverage. Run `npm run lint:js` and `npm run build` from `admin/` and commit generated `admin/dist/` assets if that is the repository’s established deployment pattern. Verify the manifest and both bundles are regenerated: `admin/dist/asset-manifest.json`, `css/main.css`, `css/editor.css`, `js/main.js`, and `js/editor.js`.

### 9. Add the translated-content WordPress editor surface

**Create**

- `tests/integration/TranslationEditorSaveTest.php`

**Modify**

- `includes/Translation/MetaBox.php`
- `includes/API/PostsController.php`
- `admin/src/editor/translation-metabox.js`
- `admin/src/styles/editor/_translation-metabox.scss`
- `includes/Admin/AssetLoader.php` if localized editor data must be extended
- `includes/Core/Plugin.php`
- `admin/webpack.config.js` only if the existing editor entry needs another asset

Extend the existing metabox/sidebar panel, shown only for translated posts/pages/configured CPTs. Render current canonical lock data and a nonce/capability-safe save path. Add a separate Webpack editor entry and enqueue it only on eligible post edit screens, localizing REST root, nonce, target identity, and initial lock—not text bodies. Use the same lock component behavior/copy as dashboard modals and route editor Generate through the same protected endpoint.

On ordinary WordPress editor save, after post and configured ACF values persist, call `ContentManager` to record manual provenance and current target fingerprint while preserving omitted override. Saving text plus a submitted override is treated as one protected operation; reject unauthorized lock mutation. Exclude autosaves/revisions and generated internal writes. Do not let unsaved metabox state enable Generate.

### 10. Verification and acceptance gates

**Add/extend tests**

- Unit files from prior steps plus existing `tests/unit/Core/ContentManagerTest.php`.
- Integration files from prior steps plus generation/job/controller tests under `tests/integration/`.
- `tests/e2e/manual-translation-lock-auto-translate.spec.js` as the dedicated end-to-end contract suite.
- `tests/e2e/source-event-automation.spec.js` where the remote environment can invoke lifecycle fixtures.
- `tests/editor-translation-regression.node-test.js` and `tests/unit/API/PostsControllerEditorStateStandaloneTest.php` for editor-state/asset regressions.
- `tests/integration/SettingsIdempotencyTest.php` for canonical/partial/reset behavior.
- Update `tests/e2e/settings.spec.js`, `settings-visual.spec.js`, `verify-content-translate-edit.spec.js`, `metabox-translate.spec.js`, `system-translate.spec.js`, `verify-translate-all-button.spec.js`, and `journeys/UJ-021-generate-all-content.spec.js` instead of leaving assertions for the removed old toggle/results. Reconcile the existing four-tab expectation in `settings.spec.js` with the current five-tab UI, and retire/update obsolete `translation-settings.spec.js` assumptions about a Translation/API-key tab.

Unit/integration coverage must prove the complete lock matrix, unknown inherited migration behavior, generated inherited eligibility, explicit override precedence, source/target fingerprint races, all trigger classifiers/exclusions, fingerprint normalization including ordered ACF, event deduplication/supersession, zero API calls in `mark_stale`, atomic manual save, concurrent unique mapping, scoped extension inventory, bounded reconciliation, and distinct protected/changed/failed outcomes. Extend the live standalone suites `tests/unit/API/TranslateControllerPersistenceStandaloneTest.php`, `tests/unit/Translation/TranslateJobsCompletionStandaloneTest.php`, `TranslationFinalizerCopyOnlyStandaloneTest.php`, `TranslationFinalizerRollbackStandaloneTest.php`, `TranslationFinalizerTerminalRaceStandaloneTest.php`, and `tests/unit/ReviewDefectsContractStandaloneTest.php`, plus `tests/integration/AutoPublishStatusTest.php`. Do not rely on `tests/unit/Core/ContentManagerTest.php` until its PHPUnit exclusion and stale method references are deliberately repaired; add focused executable tests for the new ContentManager APIs.

For visual work follow the mandatory before/fix/after Playwright loop. Use `~/.claude/bin/e2e-remote` only—never start a local browser or dev server. Capture before/after screenshots and computed-style/accessibility assertions for settings, both dashboard modals, editor panel, dark mode, and narrow viewport. Test all ten exact labels and unique tooltip copy, hover/focus/touch, Escape, stable `aria-describedby`, visible helper text, stale+protected, and protected action feedback.

Run commands by bare executable and judge exit status:

```bash
cd plugins/international-press-zone
find includes tests -name '*.php' -print0 | xargs -0 -n1 php -l
composer test
composer phpcs
composer phpstan
cd admin
npm ci
npm run lint:js
npm run build
cd ..
~/.claude/bin/e2e-remote --env "WP_BASE_URL=${WP_BASE_URL:?Set WP_BASE_URL to authorized dev1}" -- npm --prefix tests/e2e test -- manual-translation-lock-auto-translate.spec.js source-event-automation.spec.js
```

Run relevant existing E2E generation/settings/string suites through the remote wrapper as a regression set. If a configured gate is already broken, record the exact command, exit status, and baseline failure; do not hide warnings/notices/security output. Verify no generated path can write without `TranslationProtectionPolicy` at queue time and `TranslationFinalizer` at persistence time by searching all calls to `TranslationAPI::translate*`, `wp_update_post`/`wp_insert_post` for translated targets, and `StringScanner::saveTranslation`.

## Definition of done

- Existing rows are inherited and retain all content; duplicate mappings are recovered without deleting posts.
- Manual saves from all three surfaces atomically set manual provenance and optional override.
- Every synchronous, async, bulk, force, editor, callback, finalizer, and automation path performs queue-time and completion-time checks.
- Lock or target/source changes during a job produce terminal skips and preserve the target.
- Automation is off by default; stale-only is default and demonstrably spends no credits.
- All ten lifecycle events have exact semantics, scoped scans, dedupe, durable events, and reconciliation recovery.
- Settings/detail/generation REST contracts and accessible UI match the specified literals and errors.
- Batch summaries distinguish translated, marked stale, protected, changed while queued, and failed.
- PHP, admin build/lint, integration, and remote visual/E2E gates pass, with no unexplained warnings or security findings.
