# Multilingual Site Entry and Layout Design

Audience: AI coding agents first.

## Outcome

Make multilingual site entry automatic and understandable.

1. A translated static homepage MUST render at each language root, e.g. `/he/`.
2. A translated posts page MUST retain its WordPress role in each language.
3. Pages UI MUST identify homepage, posts page, ordinary page, source language, translation state, and assigned layout.
4. Site Editor content MUST be translated outside Pages under a separate **Site Content** tab.
5. Block structure, links, dynamic blocks, and theme updates MUST survive translation.
6. Classic-theme PHP templates MUST remain code; translate their registered strings under **Theme Text**.
7. Runtime behavior MUST derive from live WordPress settings on the deployed site. NEVER infer dev1 state from local WordPress fixtures.

Plan slug: `multilingual-site-entry-and-layout`.

## Existing Gaps to Replace

- `international-press-zone.php::mpz_init()` currently initializes full plugin on `plugins_loaded` priority 10. A callback created there cannot register or run `plugins_loaded` priority 0 locale work; install a minimal locale bootstrap directly from the main plugin file before that action registration.
- `includes/Core/Plugin.php::filter_language_root_request()` currently rewrites an exact language root to canonical `page_on_front`; it does not resolve the linked target-language Page.
- `includes/Frontend/ContentFilter.php::filterPosts()` filters by language after routing; filtering cannot substitute the correct special-page ID.
- `includes/API/PostsController.php::getPosts()` returns page rows without Reading-role or assigned-layout metadata and does not canonicalize translation groups into one source row.
- `includes/Translation/PostTypeRegistry.php` correctly protects `wp_template`, `wp_template_part`, `wp_navigation`, and `wp_pattern` from generic post translation. Keep that protection; route those entities through Site Content handlers.
- `includes/Translation/TranslationBridge.php::create_translation()` does not preserve `_wp_page_template` or mapped hierarchy.
- `admin/src/pages/translations.js` has no Site Content route and defaults to Theme strings.

Replace these seams surgically. Do not loosen `PostTypeRegistry::PROTECTED_TYPES` to force Site Editor entities through post translation.

## User Mental Model

Use these product terms consistently.

| UI term | WordPress object | Meaning |
|---|---|---|
| Homepage | `show_on_front`, `page_on_front` | First page shown at a language root |
| Posts page | `page_for_posts` | Page shell used for the blog index |
| Page | `page` | URL-bearing editorial content |
| Layout | assigned page template or block template | Structure around page content |
| Site Content | block templates, template parts, navigation, synced patterns | Reusable content and structure shared across pages |
| Theme Text | registered theme strings | Text supplied by theme code |
| Plugin Text | registered plugin strings | Text supplied by plugin code |

Never call block templates “template pages.” Pages and layouts are different objects.

## Product Decisions

### Navigation taxonomy

Replace current translation tab order with:

1. **Pages**
2. **Posts**
3. **Custom Types**
4. **Site Content**
5. **Theme Text**
6. **Plugin Text**

Default to **Pages**. Preserve legacy hashes through redirects.

`Site Content` MUST remain visible in every theme. Block themes show supported entities. Classic themes show a disabled explanatory state directing Page content to Pages and code-owned text to Theme Text; never show fake template rows.

### Pages screen

Add a pinned **Site entry pages** summary above the table.

Static-home mode:

- Homepage card: source page title, ID, source language, effective layout from native hierarchy, translation coverage, Translate action.
- Posts-page card: title, ID, source language, effective `home`/`index` layout, translation coverage, Translate action, and `Blog index: editor content is not displayed` guidance when configured.
- Posts-page layout guidance MUST point to Site Content → Templates → Home. Never claim the selected Posts page’s editor content or assigned Page template controls the blog index.
- Missing posts-page configuration: state this without inventing a page.
- Treat `page_for_posts` as active only while `show_on_front=page`; expose any retained value in latest-posts mode as inactive configuration, not an active role.

Latest-posts mode:

- Homepage card MUST read `Latest posts (automatic)`.
- Explain that no homepage Page exists.
- Link actions to Posts and Site Content; never ask user to search Pages.

Page rows MUST expose:

- `Homepage` badge when row belongs to `page_on_front` translation group.
- `Posts page` badge when row belongs to `page_for_posts` translation group.
- assigned layout label; use `Default layout` when absent.
- source language.
- translation coverage.
- publication status.

Filters:

- role: `All pages`, `Homepage`, `Posts page`, `Regular pages`.
- layout: `All layouts` plus discovered page-template labels.
- source language.
- existing status/search filters.

Default listing MUST show one canonical source row per translation group. Never list translated copies as unrelated Pages. Unlinked Pages remain individual rows. Group and filter at query/data-source level before pagination; totals, pages, and filters MUST describe canonical rows, never a post-processed page slice.

### Site Content screen

Use subtabs:

1. **Templates** — Front Page (`front-page`), Home / Posts Index (`home`), Page, Single, Archive, Search, 404, and custom block templates.
2. **Headers & Footers** — `wp_template_part`, grouped by area.
3. **Navigation** — `wp_navigation` entities.
4. **Patterns** — synced `wp_block` patterns and eligible registered patterns with persisted content.

Each row MUST show:

- human label and stable identity.
- entity kind.
- theme/file origin or database customization origin.
- where-used summary when WordPress supplies it cheaply.
- source revision state.
- per-language state: Missing, Translating, Ready, Needs update, Failed.
- Translate, Review, Regenerate actions as state permits.

Templates remain one shared structure by default. Translation overlays replace translatable text only. Add no per-language design fork control in this scope.

### Editor guidance

Every Page details modal MUST distinguish:

- `Page content`: translate here.
- `Layout: <name>`: shared structure; open corresponding Site Content item when it contains translatable text.
- `Theme text`: translate under Theme Text.

Never imply that changing a Page translation changes its shared header/footer.

## Homepage and Posts-Page Runtime Contract

### Special-page role resolution

Create one service responsible for live role detection and translation mapping.

```text
SpecialPageResolver::getOverview(): SpecialPageOverview
SpecialPageResolver::getRole(int $postId): null|'front_page'|'posts_page'
SpecialPageResolver::resolveForLanguage(string $role, string $languageCode): SpecialPageResolution
EffectiveTemplateResolver::forRole(string $role, ?int $resolvedPostId): EffectiveTemplateResolution
```

`EffectiveTemplateResolver` MUST follow native template hierarchy, not `_wp_page_template` alone.

- Static front page: `front-page` wins when present; otherwise follow assigned custom template and native Page hierarchy.
- Posts index: `home` wins, then `index`; selected Posts page body and assigned Page template are ignored.
- Classic themes: report effective PHP template basename as read-only layout information; never offer PHP source translation.
- Block themes: link effective `front-page` or `home` entity to Site Content.
- Homepage/posts-page cards MUST show `Effective layout`; ordinary Page rows continue showing assigned Page layout.

`SpecialPageOverview` MUST carry:

- mode: `static_page` or `latest_posts`.
- canonical front-page ID or null.
- canonical posts-page ID or null.
- role translation groups.
- per-language resolved ID, status, and fallback flag.

Resolution rules:

1. Read current `show_on_front`, `page_on_front`, and `page_for_posts` values.
2. Treat configured IDs as canonical source-role anchors regardless of page title.
3. Resolve target through existing translation relationships.
4. Accept target only when same post type and publicly viewable status.
5. For `front_page`, use canonical source as explicit whole-document fallback when target is absent, draft, private, deleted, or invalid.
6. For `posts_page`, return `missing` with no target ID or fallback URL when target is unavailable; never serve source blog index at a target-language posts URL.
7. Mark front-page fallback explicitly for locale, SEO, and admin diagnostics.
8. Recompute after Reading settings, post status, deletion, or translation-link changes. Cache only derived results with complete invalidation.

### Locale context lifecycle

Separate requested URL language from effective rendered language.

```text
LocaleContextCoordinator::bootstrap(): LanguageRequestContext
LocaleContextCoordinator::finalizeForParsedRequest(array $queryVars): LanguageRequestContext
LocaleContextCoordinator::restore(): void
```

- Add `LocaleBootstrap::register()` call directly in `international-press-zone.php` after autoload/constants and before `add_action('plugins_loaded', ...mpz_init, 10)`. Main file contains registration only; bootstrap class owns logic.
- `LocaleBootstrap::register()` MUST immediately install `determine_locale`, `plugins_loaded` priority 0, and shutdown hooks. Full `Core\Plugin` initialization remains at priority 10 and consumes bootstrap context.
- `determine_locale` callback MUST lazily and idempotently ensure request context if WordPress asks for locale before `plugins_loaded` priority 0.
- Bootstrap MUST fail closed to canonical locale when requirements, schema, languages, URL marker, or options are unavailable; it MUST NOT create/upgrade schema or initialize full plugin services.
- On `plugins_loaded` priority 0, validate URL marker through one route matcher, capture canonical Reading options, resolve exact-root front-page fallback, and set requested/effective language context.
- If WordPress locale already differs, call documented `switch_to_locale(effectiveLocale)`; rely on WordPress locale switching/`change_locale` machinery for loaded text domains, never mutate MO files or global translation registries directly.
- On `parse_request`, validate parsed marker against early context and finalize before main query. Any mismatch fails closed to canonical source context.
- Theme/plugin strings rendered after bootstrap, query filtering, `<html lang>`, direction, and locale-sensitive formatting MUST use effective language.
- Do not switch locale in admin, Site Editor, generic REST, AJAX management, cron, CLI, preview, embed, trackback, or non-HTML feed contexts.
- Track whether this coordinator switched locale. Restore previous locale on `shutdown` at `PHP_INT_MAX`, test teardown, and worker request-finalization. Clear request context and adapter caches at same boundary.

### WordPress request lifecycle

Stored Reading options remain canonical source IDs. Request-local resolution MUST never call `update_option()`.

Hook ownership:

| Hook | Priority | Contract |
|---|---:|---|
| main plugin file load | immediate | call `LocaleBootstrap::register()` before full-plugin `plugins_loaded` registration |
| plugin bootstrap `determine_locale` filter | 1 | lazily ensure context, then return effective locale |
| `plugins_loaded` | 0 | finalize requested language/canonical options/effective locale before full plugin init at priority 10 |
| `option_page_on_front` | 10 | map captured canonical front-page ID only in eligible public HTML context |
| `option_page_for_posts` | 10 | map captured active posts-page ID only when published target exists in eligible public HTML context |
| existing `parse_request` root normalization | `PHP_INT_MAX` | exact active-language root only; preserve language marker and set query vars from filtered role ID |
| existing `pre_get_posts` language filter | 10 | main query only; use effective document language from special-page context |
| `wp` | 1 | finalize role/fallback context from queried object and conditionals; never mutate canonical options |
| `redirect_canonical` | 10 | delegate special-page redirect decisions to canonical URL service; suppress only conflicting native redirect |
| `template_redirect` | 1 | issue one validated special-page redirect before output |
| `page_link`, `post_type_link` | 10 | return canonical role URL for special-page translations |
| `shutdown` | `PHP_INT_MAX` | restore prior locale when switched; clear request-local context/caches |

Lifecycle rules:

- Exact language-root routes MUST retain requested language through query parsing.
- Frontend option filters MUST resolve to published translation IDs so native `is_front_page()`, `is_home()`, queried object, body classes, and pagination remain correct.
- Resolver MUST use canonical values captured before option filters; never recurse through filtered `get_option()` calls.
- Admin, Site Editor, generic REST, cron, CLI, AJAX management, preview, embed, trackback, and non-HTML feed contexts MUST retain canonical option values unless an explicit plugin preview requests target language.
- Latest-posts mode remains a language-filtered posts index; never manufacture a homepage Page.
- `page_for_posts` mapping is active only in static-home mode.
- Existing language filtering MUST not remove a resolved fallback source Page; it uses effective document language for that request.
- Mapping MUST work for subdirectory, subdomain, and query-parameter URL modes.
- Request context MUST be resettable for unit tests and long-running workers; no mutable translation variant may leak between requests.

### Canonical URL service

Use one service for permalinks, redirects, switchers, canonical tags, alternates, and sitemaps.

```text
LocalizedURLResolver::forRole(string $role, string $languageCode): URLResolution
LocalizedURLResolver::forContent(int $postId, string $languageCode): URLResolution
```

For a translated static homepage:

- canonical public URL is language root, e.g. `/he/`.
- translated page’s ordinary slug URL MUST 301 redirect to that root.
- `get_permalink()` for that role translation MUST return language root.
- language switcher MUST move root-to-root.

For a translated posts page:

- published target’s canonical URL uses its translated slug under active URL mode.
- target request MUST set native `is_home()` true and `is_page()` false through translated `page_for_posts` resolution.
- translated copy MUST not create a second posts-index URL.
- title/slug and relationship translate; editor body and assigned Page template do not control rendered blog index.

When posts-page target is missing/unpublished:

- do not create or advertise a target-language posts URL.
- language switcher marks target unavailable and has no target link.
- guessed target-language source slug follows normal routing, normally 404; plugin MUST NOT intercept it as fallback.
- source-language posts URL remains canonical and indexable.
- emit no target alternate or sitemap entry.
- once target publishes, option mapping, switcher, canonical, alternate, and sitemap become available atomically.

Redirect rules:

- Compare normalized absolute URLs before redirecting.
- Preserve safe pagination/query state explicitly; drop unrecognized redirect parameters.
- Never redirect previews, feeds, embeds, REST, admin, cron, or CLI.
- Detect target-equals-request and previously normalized role URL; never loop.

### Missing translation behavior

Use whole-document source fallback for an exact active language root only.

- Keep requested language in routing diagnostics, but set effective document language to source language.
- Render source content, theme/plugin localization, query filters, `<html lang>`, and direction consistently in source language.
- Language switcher MUST indicate source language as rendered and expose requested target as unavailable; never claim source content is translated.
- Emit `noindex,follow`.
- Canonical URL points to source-language canonical root.
- Do not emit target-language `hreflang` or sitemap entry.
- Never apply this fallback to arbitrary missing Pages, previews, feeds, REST, or Site Editor requests.
- Admin MUST show `Missing homepage translation` or `Homepage translation is not published`.

### SEO ownership matrix

Generate one alternate set through `LocalizedURLResolver`.

| Output | Plugin ownership |
|---|---|
| core/plugin `hreflang` | plugin emits published translations plus `x-default` once |
| Yoast alternates | use documented Yoast filters to replace only duplicate alternate output |
| canonical | provide special-page/fallback canonical through documented core/SEO-plugin filters |
| robots | add `noindex,follow` only for explicit whole-document fallback context |
| WordPress sitemap | rewrite published special-page entries to canonical role URL; remove duplicate slug entry |

Never scrape rendered HTML or broadly disable SEO-plugin output. Include published translations only. Invalidate URL/SEO caches on Reading settings, relationship, status, deletion, language-default, URL-mode, and theme changes.

## Site Content Architecture

### Catalog

Create a catalog over WordPress’s public block-template APIs and persisted site entities.

```text
SiteContentCatalog::list(SiteContentQuery $query): SiteContentPage
SiteContentCatalog::get(SiteEntityIdentity $identity): ?SiteEntity
```

`SiteEntityIdentity`:

```json
{
  "type": "wp_template|wp_template_part|wp_navigation|wp_block",
  "key": "theme-or-site-scoped-stable-key"
}
```

Requirements:

- Enumerate theme-file and database-customized templates through `get_block_templates()`; never scan theme directories as runtime source of truth.
- Validate every requested identity against catalog output. Never convert request keys into filesystem paths.
- Preserve origin, theme, slug, area, title, status, and source content.
- Treat unsynced patterns embedded in Pages as Page content, not separate Site Content.
- Hide unsupported dynamic or non-persisted entities with a reason; never silently submit empty jobs.

### Block segment codec

Translate extracted segments, never raw serialized block documents.

```text
BlockSegmentCodec::extract(string $serializedBlocks, SegmentContext $context): SegmentDocument
BlockSegmentCodec::apply(string $serializedBlocks, SegmentTranslationSet $translations): CompiledBlockTranslation
```

Extraction MUST:

1. Parse with WordPress block APIs.
2. Assign segment identity independently from source text. Segment record MUST contain opaque `segment_id`, structural fingerprint, source hash, block kind, field kind, and sequence context.
3. Reconcile revisions through block-tree and sibling-sequence matching. Insertion/reordering MUST retain identity when one unambiguous structural/textual match exists. Duplicate or ambiguous subtrees MUST become review-required; never guess.
4. Extract visible text and translatable attributes.
5. Use block metadata `attributes.*.role=content` on WordPress 6.7+; maintain tested core/known-block schema fallbacks for WordPress 6.0–6.6.
6. Cover core text, button labels, image alt/caption/title, table text, list text, and safe HTML text nodes.
7. Preserve URLs, IDs, block names, comments, shortcodes, placeholders, dynamic block settings, and non-content attributes.
8. Expose filters for registered third-party block schemas; default unknown structural attributes to non-translatable.
9. Exclude empty, numeric-only, code, and non-user-visible segments.

Apply MUST:

1. Require exact segment IDs and protected placeholders.
2. Reject missing, duplicate, unknown, or structurally invalid output.
3. Rebuild serialized blocks without changing block order or nesting.
4. Apply field-specific validation: plain-text fields reject markup; rich-text fields allow only source-permitted markup; URL/ID/structural fields are never provider-writable; attributes use their declared WordPress schema.
5. Providers MUST NOT introduce markup, URLs, block attributes, placeholders, or segment IDs unless that segment schema explicitly permits the value.
6. Round-trip parse and compare the compiled block tree against permitted structural changes.
7. Return an error without replacing the last ready translation when validation fails.

### Translation resource pipeline

Generalize async jobs by resource handler; preserve existing post behavior.

```text
TranslationResourceHandler
  supports(string $contentType): bool
  prepare(int $contentId, string $targetLanguage): TranslationPayload|WP_Error
  finalize(int $contentId, string $targetLanguage, TranslationResult $result): FinalizeResult
```

Implement exactly two handlers:

- existing post/page/custom-post handler.
- site-content handler.

Jobs retain numeric `content_id`. Site-content jobs reference numeric translation-record IDs, never hashes masquerading as IDs. Add nullable idempotency key, actor ID, request fingerprint, prior-response metadata, and expiry columns/indexes to existing job storage; existing jobs remain valid.

Backend request adds a backward-compatible segmented resource shape:

```json
{
  "resource_type": "site_content",
  "source_language": "en",
  "target_language": "he",
  "segments": [
    {
      "id": "stable-segment-id",
      "text": "Source text",
      "context": "core/heading"
    }
  ]
}
```

Backend response MUST return every input ID exactly once. Existing title/excerpt/content requests remain unchanged.

### Storage

Add one additive site-content translation table because theme-file entities have no durable WordPress post ID.

Required columns:

- numeric primary ID.
- entity type and stable entity key.
- source and target language codes.
- source revision SHA-256.
- monotonic unsigned `translation_revision`, initialized to 1 and incremented atomically on every successful segment/status mutation.
- translated segment set as durable authority.
- compiled serialized content as disposable cache only.
- status: `missing`, `translating`, `ready`, `needs_update`, `failed`.
- last error, creator/updater IDs, timestamps.
- unique key on entity type + entity key + target language.

All review/update/delete writes MUST compare expected `source_revision` and `translation_revision` in the same atomic update. Zero affected rows returns `409`; never perform read-then-unconditional-write.

Never persist a translated template structure as authority. Runtime MUST start from current source structure and apply exact compatible translated segments. Replace compiled cache only after codec validation and optimistic source-revision check.

Source changes:

- unchanged, unambiguously reconciled segments retain approved translations.
- new/changed segments render current source text and become `needs_update`.
- removed segments disappear immediately because runtime starts from current source structure.
- ambiguous structural matches render current source text and require review.
- obsolete compiled cache MUST never keep removed theme markup live.

### Runtime adapters

Use separate adapters because WordPress resolves each entity family through different APIs.

```text
BlockTemplateTranslationAdapter::resolve(WP_Block_Template $entity, string $languageCode): WP_Block_Template
NavigationTranslationAdapter::resolve(WP_Post $navigation, string $languageCode): WP_Post
SyncedPatternTranslationAdapter::resolve(WP_Post $pattern, string $languageCode): WP_Post
```

Block template requirements:

- Hook official single/list block-template filters for `wp_template` and `wp_template_part` only.
- Overlay current source structure with exact compatible translated segments.
- Resolve template parts independently; never inline translated part content into parent storage.

Navigation requirements:

- Hook navigation retrieval/render seams, not block-template filters.
- Translate labels through segments.
- Map internal links to published target-language translations when available.
- Preserve external URLs and structural/menu identifiers.

Synced pattern requirements:

- Hook persisted `wp_block` retrieval/render seams, not block-template filters.
- Translate synced persisted pattern content only.
- Exclude unsynced and non-persisted registered patterns; after insertion, their blocks belong to containing Page or template.

Shared requirements:

- Runtime MUST apply translations only in explicit public frontend or plugin target-language preview context.
- Source Site Editor, generic REST, admin, cron, CLI, feeds that do not render site chrome, and background jobs MUST receive canonical source entities.
- Do not use `is_admin()` as context detection. Define a request-context value object from explicit constants/query intent and pass it to adapters.
- Never place translated variants into process-global canonical object caches.
- Cache resolved entity by adapter kind + identity + language + source revision + translation revision + request context.
- Fall back changed/invalid segments to current source text; never fall back to obsolete structure.

## REST API

Use WordPress cookie authentication with `X-WP-Nonce` validation performed by WordPress plus route-level capability and object authorization. Non-cookie authentication follows WordPress REST authentication; never hard-code a second universal nonce check.

- Page overview/list: existing route gains role, layout, canonical-group, and source-language fields.
- `GET /site/overview`: Reading mode, role cards, Site Editor support, counts.
- `GET /site-content`: paginated/filterable catalog with translation states and opaque `catalog_id`.
- `GET /site-content/{type}/{catalog_id}`: source details, extracted segments, language states.
- `POST /site-content/{type}/{catalog_id}/translations`: queue one or more target languages.
- `PUT /site-content/{type}/{catalog_id}/translations/{language}`: save reviewed segments.
- `DELETE /site-content/{type}/{catalog_id}/translations/{language}`: remove overlay only; source entity untouched.

`catalog_id` MUST be a stable URL-safe digest returned by `SiteContentCatalog`; raw composite keys such as `theme//slug` MUST never occupy a route segment. Resolve digest against current catalog and fail closed on zero or multiple matches.

Capabilities and trust-boundary validation:

- Page translation: existing content-edit capability rules plus object-level `edit_post` authorization for every affected Page.
- Site Content read/translate/review/delete: `edit_theme_options` plus existing translation entitlement.
- Validate active/configured language membership and reject source-equals-target writes.
- Enforce request byte, segment-count, per-segment length, and total-character limits before queueing.
- POST queueing MUST require an idempotency key. Persist key, actor, resource/language scope, request fingerprint, prior response/job ID, and expiry through terminal status for 24 hours.
- Replaying same key and fingerprint before expiry returns original in-flight or terminal response without charging/queueing again. Same key with different fingerprint returns `409`. Job cleanup MUST retain idempotency records until expiry.
- PUT/DELETE MUST require source revision plus translation revision; return `409` on stale writes.
- Never authorize by `is_admin()` or catalog-key obscurity.

Errors MUST distinguish unsupported entity, stale source, stale review write, invalid segment result, malformed catalog ID, missing entitlement, permission denied, size limit, and job failure.

## Page Translation Fidelity

Translated Pages MUST preserve non-language layout semantics.

On creation/update:

- copy assigned page template (`_wp_page_template`).
- preserve menu order and safe publication attributes.
- map parent to target-language parent when available; otherwise leave parent unset and warn.
- preserve featured image by reference unless configured otherwise elsewhere.
- run existing ACF copy/translate policy.
- never copy identity, lock, revision, cache, or language-link metadata blindly.

Special-page roles remain derived from canonical WordPress options and translation groups; never write translated IDs into `page_on_front` or `page_for_posts`.

## Admin Interaction and Accessibility

- Every icon-only control MUST have an accessible name.
- Tabs and filters MUST be keyboard operable and expose selected state.
- Status MUST use text plus icon; never color alone.
- Async jobs MUST announce start, completion, and failure through existing live-region patterns.
- Preserve focus after modal refresh.
- Support RTL, dark mode tokens, narrow admin widths, and reduced motion.
- Add no inline CSS.

## Failure Handling

| Failure | Required behavior |
|---|---|
| Homepage translation missing | Source fallback; admin warning; no false alternate |
| Homepage translation draft/private | Same as missing |
| Front-page translation relationship broken | Whole-document source fallback; diagnostic state; no redirect loop |
| Posts-page translation missing/broken | Target unavailable; no target URL, fallback, alternate, or sitemap entry |
| Source role changed | Recompute immediately; old translated page becomes ordinary translated Page |
| Theme update changes template | Reconcile against current structure; translated compatible segments remain; changed/ambiguous segments render source and become `needs_update` |
| Backend omits/duplicates segment | Fail job; preserve ready overlay |
| Block output fails round trip | Fail job; preserve ready overlay |
| Navigation target lacks translation | Preserve source target and show review warning |
| Unsupported classic template | Explain Theme Text/Page Content routes; never offer fake translation |
| Cache stale | Invalidate on settings, relationship, entity, translation, theme-switch, and plugin-update events |

## Migration and Compatibility

1. Add schema and handlers without rewriting existing translation rows.
2. Keep existing post translation REST and backend contracts backward compatible.
3. Do not auto-create Site Content translations during migration.
4. Derive homepage/posts-page roles on first read; no migration data needed.
5. Flush rewrite rules only on activation/versioned migration, never each request.
6. Support WordPress 6.0 baseline; feature-detect 6.7+ block attribute content roles and use tested fallbacks below 6.7.
7. Preserve legacy translation-tab URLs via redirects.
8. Deploy backend segmented-contract support before plugin UI can queue Site Content jobs; negotiate capability through backend metadata.
9. On default/source-language change, invalidate every role/entity cache, recompute source relationships, and mark overlays needing review when source identity changes; never relabel stored translation payloads blindly.
10. On theme switch, retain old-theme translation rows as inactive for rollback, exclude them from runtime/catalog, and schedule bounded orphan review/cleanup.
11. Uninstall follows existing plugin data-retention policy. If retention is enabled, keep site translations; destructive removal requires existing explicit uninstall mechanism.
12. Multisite activation MUST create/upgrade tables per site. New-site creation MUST run current schema setup. Never share role options or site-content rows across blogs.
13. Versioned cleanup MUST be bounded, resumable, idempotent, and safe when entities disappear mid-run.

## Verification

### PHP unit/integration

Cover:

- static front page per language.
- latest-posts mode and inactive retained `page_for_posts` values.
- posts-page mapping, missing-target no-fallback behavior, and ignored editor-body/Page-template semantics.
- effective `front-page` versus `home` template hierarchy for classic and block themes.
- default and secondary languages.
- missing, draft, private, deleted, and wrong-type translations.
- whole-document fallback language, direction, locale switch before theme rendering, loaded text-domain behavior, localization, robots, switcher, query filtering, and shutdown/test restoration.
- main-file bootstrap registration order: `determine_locale` and `plugins_loaded` priority 0 exist before full plugin init at priority 10; lazy pre-`plugins_loaded` locale resolution is idempotent and fail-closed.
- all URL modes.
- exact language root versus colliding page slug.
- previews, feeds, embeds, REST, Site Editor, cron, CLI, and source-editing isolation.
- multisite activation, new-site schema, and per-blog option scope.
- default-language change, theme switch, orphan retention/cleanup.
- role detection and cache invalidation.
- canonical, redirect-loop, pagination, hreflang, and sitemap output.
- grouped-row filtering and pagination totals before slicing.
- REST authentication, object authorization, language/size limits, 24-hour in-flight/terminal idempotency replay, conflicting-key rejection, catalog ID, and optimistic source/translation revision concurrency.
- Site Content storage concurrency and source revisions.
- block extraction/apply round trips across nested core, HTML, dynamic, and unknown blocks.
- duplicate text, sibling insertion/reordering, ambiguous subtree review, and current-source structural fallback.
- navigation and synced-pattern adapters independent from block-template hooks.

### JavaScript unit

Cover:

- special-page cards in static and latest-posts modes.
- role/layout/source-language filters.
- canonical source-row grouping.
- Site Content subtab states and actions.
- stale/failure/review states.
- keyboard and live-region behavior.

### Backend unit/integration

Cover:

- segmented payload validation.
- exact ID preservation.
- placeholder preservation.
- retries/idempotency.
- malformed provider output.
- backward compatibility with structured post fields.

### Browser verification

Use `e2e-remote`; never launch Playwright locally.

Prove:

1. Admin clearly identifies homepage without knowing its title.
2. Latest-posts mode clearly states no homepage Page exists.
3. Homepage/posts cards identify effective `front-page`/`home` layout correctly.
4. Missing posts-page translation exposes no false target URL or fallback.
5. `/he/` renders the published Hebrew homepage translation.
6. translated homepage slug redirects once to `/he/`.
7. language switcher moves home root-to-root.
8. missing homepage translation renders whole source document with source `lang`/direction, source localization, `noindex,follow`, source canonical, and no false `hreflang`.
9. Site Content translation changes visible template text without changing block structure.
10. source template update produces `Needs update`, not silent corruption.

Capture before/after screenshots for Pages and Site Content at desktop and narrow admin widths.

### Dev1 acceptance

Treat dev1 as deployment truth. After release:

- read dev1’s actual Reading configuration.
- identify its canonical homepage/posts page in the new overview.
- translate one non-default homepage and verify root routing.
- translate one safe Site Content fixture and verify frontend output.
- verify canonical, alternates, redirects, cache invalidation, RTL, and rollback.
- remove only created test fixtures; preserve pre-existing content.

Never use local WordPress content to claim dev1 behavior.

## Delivery Order

### Release 1 — site entry and Pages

1. Special-page resolver, request lifecycle, canonical URL service, SEO ownership, and tests.
2. Pages UX: summary cards, badges, source grouping before pagination, filters, and layout fidelity.
3. Build/deploy plugin to dev1; verify actual Reading configuration and language roots.

No backend protocol dependency. Release independently.

### Release 2 — templates and template parts

1. Backend segmented translation contract and capability metadata.
2. Site Content schema, catalog, codec, resource handler, REST limits/concurrency.
3. Template/template-part runtime adapter and admin subtabs.
4. Deploy backend first, then plugin; enable Templates and Headers & Footers only after capability negotiation.
5. Verify safe fixture on dev1.

### Release 3 — navigation and synced patterns

1. Navigation and synced-pattern runtime adapters.
2. Navigation link remapping/review diagnostics.
3. Admin subtabs and adapter-specific tests.
4. Deploy plugin and verify mixed site-chrome scenarios on dev1.

Each release MUST pass full gates, build distribution artifacts, remain independently rollback-safe, and never expose UI whose backend/runtime capability is absent.

## Architecture Decisions

Self-review results:

- Collapse role detection, option mapping, and admin overview into `SpecialPageResolver`; separate readers would duplicate one invariant.
- Keep `EffectiveTemplateResolver` separate from Page layout metadata because native `front-page`/`home` hierarchy differs from assigned Page templates and serves both classic/block themes.
- Keep `LocaleContextCoordinator` separate from URL generation; it owns process/request locale switching and restoration, not routing policy.
- Keep Page layout-label discovery inside existing Page API flow; a standalone layout service would have one shallow caller.
- Keep `SiteContentCatalog`, `BlockSegmentCodec`, and entity-specific runtime adapters separate. Each hides a replaceable WordPress seam and passes deletion test: removal scatters enumeration, structural validation, or runtime hook logic.
- Keep `TranslationResourceHandler` because exactly two materially different finalizers exist. Do not generalize beyond those handlers.
- Reject a universal “translatable entity” domain model. Pages and Site Content have different identity, storage, lifecycle, and runtime semantics.

Product decisions:

- Keep Pages and Site Content separate. Combining them fails the user mental model and WordPress object model.
- Share template structure; translate text overlays. Default per-language template cloning was rejected because source design changes would drift across copies.
- Use one additive site-content table. Existing translation table cannot identify theme-file entities because `element_id` is numeric.
- Generalize jobs through two resource handlers. Duplicating async pipelines would scatter retries, accounting, and finalization.
- Derive homepage roles from WordPress options. Storing duplicate per-language homepage settings would create competing sources of truth.
- Use source fallback for missing special-page translations. A hard 404 makes active language roots unavailable; false translated alternates remain forbidden.
- Keep classic PHP templates outside Site Content. Executable theme code is not translation content.
- Include navigation and synced patterns because headers/footers commonly depend on them; omitting them leaves a visibly mixed-language site.

## Industry References

- WordPress Reading settings define static homepage/posts-page semantics: <https://wordpress.org/documentation/article/settings-reading-screen/>.
- WordPress front-page conditionals depend on native query and option state: <https://developer.wordpress.org/reference/functions/is_front_page/>.
- WordPress distinguishes Pages from Site Editor templates and template parts: <https://wordpress.org/documentation/article/site-editor/> and <https://developer.wordpress.org/themes/templates/>.
- WordPress exposes supported template retrieval/filter seams: <https://developer.wordpress.org/reference/functions/get_block_templates/>, <https://developer.wordpress.org/reference/hooks/get_block_templates/>, and <https://developer.wordpress.org/reference/hooks/pre_get_block_template/>.
- WordPress REST cookie authentication uses `X-WP-Nonce`; route callbacks still own capabilities/object authorization: <https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/>.
- Public block attribute content roles require WordPress 6.7+; earlier supported versions need schema fallbacks: <https://developer.wordpress.org/news/2024/10/whats-new-for-developers-october-2024/>.
- WPML translates Pages separately from Templates and Template Parts: <https://wpml.org/documentation/getting-started-guide/translating-content-created-using-gutenberg-editor/>.
- Polylang automatically maps translated static homepages to language roots and treats Page, navigation, pattern, and template-part translation separately: <https://polylang.pro/documentation/support/getting-started/define-your-home-page-as-a-static-page/> and <https://polylang.pro/documentation/support/guides/site-editor/>.

## Non-Goals

- Per-language theme selection.
- Arbitrary per-language template redesign.
- Translating PHP template source files.
- Translating dynamic query results as template text.
- Replacing WordPress Reading settings.
- Redesigning unrelated translation, licensing, or workflow screens.
