diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 859e090..397665e 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -2,7 +2,7 @@
## Overview
-v1 ports the Płytarium (fonoteka) headless PHP backend to a single Go binary without vue-fonoteka-app or fonoteka-mcp noticing. The journey starts with the smallest possible kernel plus a day-one parity harness, proves the whole stack on one real endpoint (`GET /_fonoteka/api/v1/genres`) before any further kernel design, then broadens outward: full data-layer fidelity, the three-auth-group HTTP layer, the user plugin, OAuth2.1 for MCP/ChatGPT, the admin schema pipeline and its Vue SPA, jobs/realtime/search infrastructure, the 154-route API surface (split into two delivery slices), domain jobs and external integrations, and finally a cutover phase where the parity harness is green on all 154 routes and both real clients run unchanged against the Go backend.
+v1 ports the Płytarium (fonoteka) headless PHP backend to a single Go binary without vue-fonoteka-app or fonoteka-mcp noticing. The journey starts with the smallest possible kernel plus a day-one parity harness, proves the whole stack on one real endpoint (`GET /_fonoteka/api/v1/genres`) before any further kernel design, then broadens outward: full data-layer fidelity, the three-auth-group HTTP layer, the user plugin, OAuth2.1 for MCP/ChatGPT, the admin schema pipeline and its Vue SPA, jobs/realtime/search infrastructure, the 154-route API surface (split into two delivery slices), domain jobs and external integrations, a local Nuxt-on-Go dress rehearsal, the Journal plugin and a first blog (grzybyfunkcjonalne.pl) on reusable views, and finally production cutover of Płytarium where the parity harness is green on every manifest route and both real clients run unchanged against the Go backend.
## Phases
@@ -27,7 +27,10 @@ Decimal phases appear between their surrounding integers in numeric order.
- [x] **Phase 12: Płytarium API — Collections and Albums** - Core content endpoints ported with byte-level parity (completed 2026-10-02)
- [ ] **Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes** - Remaining core API surface
- [ ] **Phase 14: Domain jobs and external integrations** - CSV/Discogs jobs, wishlist digest, reindex, Discogs client, AI recognition, feedback
-- [ ] **Phase 15: Cutover** - Parity harness green on all 154 routes, both real clients run unchanged
+- [ ] **Phase 14.2: Local Fonoteka frontend on local SummerCMS backend** - vue-fonoteka-app locally against fonoteka.go
+- [ ] **Phase 15: Journal plugin** - Port Golem15.Journal to sm-journal-plugin
+- [ ] **Phase 16: grzybyfunkcjonalne.pl on reusable blog views** - First blog on SummerCMS
+- [ ] **Phase 20: Płytarium cutover** - Parity harness green, both real clients run unchanged in production
## Phase Details
@@ -639,7 +642,7 @@ Plans:
**Requirements**: TBD
**Depends on:** Phase 12 (user groups tables and the `Groups` relation from 12-01)
**Repos:** `sm-user-plugin` (mounted in fonoteka.go at `plugins/golem15/user`); `summercms.go` only if the admin pipeline is missing a feature the screens need
-**Ordering:** independent of Phase 13; must land before Phase 15 (cutover)
+**Ordering:** independent of Phase 13; must land before Phase 20 (Płytarium cutover)
**Success Criteria** (what must be TRUE):
1. Users, User Groups and Organisations each have a list (columns, search, filters as in the PHP `config_filter.yaml`) and a create/update form ported from the PHP model YAML, reachable from admin navigation and gated by backend permissions.
@@ -808,17 +811,103 @@ Plans:
**Wave 2** *(blocked on Wave 1 completion)*
- [x] 14.1-02-PLAN.md — Unit tests last: OAuthIdentityApiTest port, migration, MeToken null, secret-leak, corpus 175/175/0
-### Phase 15: Cutover
+### Phase 14.2: Local Fonoteka frontend on local SummerCMS backend (INSERTED)
-**Goal**: The parity harness is green on all 154 routes and `vue-fonoteka-app` and `fonoteka-mcp` run unchanged against the Go backend in daily use — the project's definition of done.
+**Goal:** A developer runs the existing `vue-fonoteka-app` locally against the local `fonoteka.go` SummerCMS backend (not PHP), logs in, and uses the real Nuxt flows so the port is proven in daily use before production cutover.
**Mode:** mvp
-**Depends on**: Phase 2, Phase 8, Phase 9, Phase 10, Phase 12, Phase 13, Phase 14, Phase 14.1
+**Depends on:** Phase 14.1
+**Repos:** `vue-fonoteka-app`, `fonoteka.go` (local API, CORS, cookies, env); `summercms.go` only if a framework helper is missing
+**Requirements**: QA-05
+**Success Criteria** (what must be TRUE):
+
+ 1. Documented local run: Nuxt and the Go backend start together, with API base URL, cookies and CORS matching how the SPA talks to Winter today.
+ 2. A signed-in session against Go covers the core Nuxt flows: browse collections and albums, search, edit, rate, upload a cover, wishlist add/remove.
+ 3. Failures against Go are visible (no silent fall-through to PHP); the local stack does not require the production origin.
+ 4. The new glue (env, proxy, CORS, run docs) has tests or a fail-closed check script, delivered in the phase's last plan.
+
+**Plans:** 0 plans
+
+Plans:
+- [ ] TBD (run $gsd-plan-phase 14.2 to break down)
+
+### Phase 14.2.1: Translate plugin (INSERTED)
+
+**Goal:** Golem15.Translate is ported to Go as `sm-translate-plugin` so Journal (Phase 15) can keep translatable fields. Lean core only: Locale model and Locales admin, Translatable API, cabana `mltext`/`mlmarkdown`, and PHP Translator locale resolution (URL prefix / session / cookie / default).
+**Requirements**: TBD
+**Depends on:** Phase 14.2
+**Repos:** `sm-translate-plugin` (new); `sm-grzybyfunkcjonalne-app` (proof host); `summercms.go` for ML field types and the surf locale seam
+**Success Criteria** (what must be TRUE):
+
+ 1. Plugin repo exists with `winter_translate_*` schema (locales, attributes, indexes, empty messages table), Locale model, en/pl seed, and context-safe Translator.
+ 2. Translatable API (`Translatable()`, `WithLocale`, get/set, default-locale fallback) and permissioned Locales admin work; a fixture model saves and reads `en`+`pl`.
+ 3. Cabana `markdown`/`mltext`/`mlmarkdown` compose; nested locale writes are not dropped; docs, OpenAPI, TS types and `boardwalk` `dist/` update in the same change.
+ 4. Proof host `sm-grzybyfunkcjonalne-app` boots with user+translate; unit tests are the last plan.
+
+**Plans:** 4 plans
+
+Plans:
+
+**Wave 1**
+- [ ] 14.2.1-01-PLAN.md — Plugin repo, `winter_translate_*` schema, Locale, en/pl seed, Translator, surf Resolver
+
+**Wave 2** *(blocked on Wave 1 completion)*
+- [ ] 14.2.1-02-PLAN.md — Translatable API, Locales admin, fixture save/read
+
+**Wave 3** *(blocked on Wave 2 completion)*
+- [ ] 14.2.1-03-PLAN.md — Cabana markdown/ML fields, nested save, docs/OpenAPI/dist, proof host boot
+
+**Wave 4** *(blocked on Wave 3 completion)*
+- [ ] 14.2.1-04-PLAN.md — Unit/integration tests last, phase gate, security review
+
+### Phase 15: Journal plugin
+
+**Goal:** The Golem15 Journal plugin is ported to Go as `sm-journal-plugin` and mounts in a host application the same way `sm-user-plugin` does, so a blog can run on SummerCMS without the PHP plugin.
+**Mode:** mvp
+**Depends on:** Phase 7, Phase 10, Phase 12.1
+**Repos:** `sm-journal-plugin` (new, under `git.golem15.com/golem15/`); a host application only as needed to boot and test the plugin; `summercms.go` only if a framework feature is missing
+**Requirements**: TBD
+**Success Criteria** (what must be TRUE):
+
+ 1. The Journal plugin repo exists with models, migrations and admin screens ported from PHP `Golem15.Journal`, driven by YAML forms/lists, permission-gated in the admin SPA.
+ 2. A host application can mount the plugin as a submodule and serve its public content (posts, categories, the PHP-equivalent routes or a documented successor).
+ 3. Plugin README and docs stay application-neutral (`the application`, example names such as `blog`).
+ 4. The new code has unit tests, delivered in the phase's last plan.
+
+**Plans:** 0 plans
+
+Plans:
+- [ ] TBD (run $gsd-plan-phase 15 to break down)
+
+### Phase 16: grzybyfunkcjonalne.pl on reusable blog views
+
+**Goal:** grzybyfunkcjonalne.pl runs on SummerCMS with a new blog views layer that other Golem15 blogs can reuse, instead of a one-off theme.
+**Mode:** mvp
+**Depends on:** Phase 15
+**Repos:** the grzybyfunkcjonalne application (Go host + views/frontend); `sm-journal-plugin`; `sm-sitemap-plugin` if the blog needs it (todo `sitemap-plugin-port.md`); `summercms.go` only if a framework feature is missing
+**Requirements**: TBD
+**Success Criteria** (what must be TRUE):
+
+ 1. grzybyfunkcjonalne.pl is served by a SummerCMS binary (Journal content, routing, i18n as the site needs).
+ 2. The blog views (templates/components/layout) live in a reusable place, not a site-only fork, so a later blog can mount them with its own content.
+ 3. Public pages that PHP Journal + the current theme rendered have a named successor on Go; sitemap is in if the site still needs it.
+ 4. The new code has unit tests, delivered in the phase's last plan.
+
+**Plans:** 0 plans
+
+Plans:
+- [ ] TBD (run $gsd-plan-phase 16 to break down)
+
+### Phase 20: Płytarium cutover
+
+**Goal**: The parity harness is green on every manifest route and `vue-fonoteka-app` and `fonoteka-mcp` run unchanged against the Go backend in daily use in production — the Płytarium definition of done.
+**Mode:** mvp
+**Depends on**: Phase 2, Phase 8, Phase 9, Phase 10, Phase 12, Phase 12.1, Phase 13, Phase 14, Phase 14.1, Phase 14.2, Phase 15, Phase 16
**Repos:** fonoteka.go, summercms.go
**Requirements**: API-09, QA-05
**Success Criteria** (what must be TRUE):
- 1. All 154 routes are registered on the correct auth groups with identical paths, methods and status codes.
- 2. The parity harness runs green across recorded fixtures for all 154 routes.
+ 1. All manifest routes are registered on the correct auth groups with identical paths, methods and status codes.
+ 2. The parity harness runs green across recorded fixtures for every manifest route (zero `pending`).
3. `vue-fonoteka-app` runs unchanged against the Go backend for a full manual session (browse, edit, upload, invite, OAuth-connect an MCP client).
4. `fonoteka-mcp` completes its install/auth flow and a representative set of tool calls unchanged against the Go backend.
@@ -827,7 +916,7 @@ Plans:
## Progress
**Execution Order:**
-Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 15
+Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 14.1 → 14.2 → 15 → 16 → 20
(Phase 2 depends on Phase 1's command kernel; the two are no longer parallel.)
| Phase | Plans Complete | Status | Completed |
@@ -849,7 +938,10 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 13. Płytarium API — wishlist, notifications, CSV, credentials, public routes | 6/6 | In Progress| |
| 14. Domain jobs and external integrations | 6/6 | In Progress| |
| 14.1. OAuth identities and fonoteka me routes (INSERTED) | 2/2 | Complete | 2026-10-05 |
-| 15. Cutover | 0/TBD | Not started | - |
+| 14.2. Local Fonoteka frontend on local SummerCMS backend (INSERTED) | 0/TBD | Not started | - |
+| 15. Journal plugin | 0/TBD | Not started | - |
+| 16. grzybyfunkcjonalne.pl on reusable blog views | 0/TBD | Not started | - |
+| 20. Płytarium cutover | 0/TBD | Not started | - |
## Backlog
@@ -857,7 +949,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
**Goal:** Visitors to summercms.io subscribe for updates through the initial Go version of the Golem15 Newsletter plugin, which confirms each email by double opt-in. The signup widget is added to the Phase 11.2 landing page, and the site gains Polish alongside English.
**Requirements**: TBD
-**Deferred:** 2026-10-02, formerly Phase 11.3. It is one more Golem15 plugin port; the Journal plugin and delivering Płytarium come first. Before planning, re-check what 11.2 shipped since: the landing-page design has no signup slot, nginx on rome blocks `/backend` and non-GET methods, and rome has no mail relay configured.
+**Deferred:** 2026-10-02, formerly Phase 11.3. It is one more Golem15 plugin port; Journal is Phase 15 and Płytarium cutover is Phase 20. Before planning, re-check what 11.2 shipped since: the landing-page design has no signup slot, nginx on rome blocks `/backend` and non-GET methods, and rome has no mail relay configured.
**Repos:** `sm-newsletter-plugin` (new, under `git.golem15.com/golem15/`, Go package `newsletter`), plus changes to the Phase 11.2 repos (`vue-summercmsio-app` for the widget and Polish locale, `sm-summercmsio-app` to wire the plugin). `summercms.go` changes only if a framework feature is missing.
**Port source:** Golem15.Newsletter, `github.com/golem15com/wn-newsletter-plugin`, checked out at `/media/nvme/dev/golem15/horoskopia.eu/plugins/golem15/newsletter`. The PHP plugin is work in progress. Its audience comes from registered users (it requires `Golem15.User`), and its only public routes are unsubscribe. It has no public subscriber model or signup widget, so this phase adds those in Go. The PHP original is not changed.
**Success Criteria** (what must be TRUE):
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 26a0a73..60316f5 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -1,18 +1,18 @@
---
gsd_state_version: "1.0"
milestone: v1.0
-current_phase: 11
-current_phase_name: Jobs, realtime and search infrastructure
-status: planning
+current_phase: 14.2.1
+current_phase_name: Translate plugin
+status: executing
stopped_at: Phase 14.2.1 context gathered
-last_updated: "2026-10-06T08:41:44.935Z"
+last_updated: "2026-10-06T09:22:24.449Z"
last_activity: 2026-10-05
last_activity_desc: Phase 14.1 complete, transitioned to Phase 11
-state_head: 6d2e5e6f4bdaea373a140517c6a41d3a0eecc91e
+state_head: e723c391fba5b8f8aa6412fa4dce4e6270db81b7
progress:
total_phases: 26
completed_phases: 12
- total_plans: 125
+ total_plans: 129
completed_plans: 125
milestone_name: milestone
---
@@ -28,9 +28,9 @@ See: .planning/PROJECT.md (updated 2026-09-16)
## Current Position
-Phase: 11 — Jobs, realtime and search infrastructure
+Phase: 14.2.1 (Translate plugin) — READY TO EXECUTE
Plan: Not started
-Status: Ready to plan
+Status: Ready to execute
Last activity: 2026-10-05 — Phase 14.1 complete, transitioned to Phase 11
Progress: [███████░░░] 67%
diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md
new file mode 100644
index 0000000..1253bff
--- /dev/null
+++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md
@@ -0,0 +1,227 @@
+---
+phase: 14.2.1-translate-plugin
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - ../sm-translate-plugin/go.mod
+ - ../sm-translate-plugin/plugin.go
+ - ../sm-translate-plugin/README.md
+ - ../sm-translate-plugin/config/config.yaml
+ - ../sm-translate-plugin/models/registry.go
+ - ../sm-translate-plugin/models/locale.go
+ - ../sm-translate-plugin/updates/registry.go
+ - ../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go
+ - ../sm-translate-plugin/updates/202610060002_create_winter_translate_attributes.go
+ - ../sm-translate-plugin/updates/202610060003_create_winter_translate_indexes.go
+ - ../sm-translate-plugin/updates/202610060004_create_winter_translate_messages.go
+ - ../sm-translate-plugin/updates/202610060005_seed_en_pl_locales.go
+ - ../sm-translate-plugin/classes/translator.go
+ - modules/surf/locale_resolver.go
+ - modules/surf/router.go
+ - modules/surf/README.md
+autonomous: false
+requirements: [D-01, D-02, D-03, D-04, D-07, D-08, D-10, D-12, D-15, D-16]
+must_haves:
+ truths:
+ - "D-01/D-02/D-03/D-04: implementation is derived only from the read-only PHP tree at SHA 725d547ec839f02b5fdc0f0a6faaed601a414d50; no PHP file changes."
+ - "D-10: gormigrate creates final `winter_translate_locales`, `winter_translate_attributes`, `winter_translate_indexes`, and empty `winter_translate_messages`; it creates no rainlab-era or `golem15_translate_*` tables."
+ - "D-08: an idempotent seed creates enabled `en` as default at sort_order 1 and enabled non-default `pl` as Polski at sort_order 2, and never seeds `de`."
+ - "D-07/D-12: a backpack-published resolver selects URL prefix, valid user preferred_locale, remembered `golem15.translate.locale`, cookie-gated Accept-Language, then default, and surf stores only a validated code on context."
+ - "KERN-07: no package-level mutable current locale exists; `Translator.Locale(ctx)` and `towel.WithLocale` carry request state."
+ - "D-15/D-16: the new module is `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, plugin ID `golem15.translate`, in the intended sibling checkout."
+ artifacts:
+ - path: "../sm-translate-plugin/plugin.go"
+ provides: "compiled Plugin registration, migrations/models/config, Resolver publication"
+ contains: "golem15.translate"
+ - path: "../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go"
+ provides: "squashed Locale schema"
+ contains: "winter_translate_locales"
+ - path: "../sm-translate-plugin/updates/202610060004_create_winter_translate_messages.go"
+ provides: "empty compatibility Messages table without admin"
+ contains: "winter_translate_messages"
+ - path: "../sm-translate-plugin/classes/translator.go"
+ provides: "context-only Translator and request Resolver"
+ contains: "func ("
+ - path: "modules/surf/locale_resolver.go"
+ provides: "framework-owned optional resolver contract"
+ contains: "LocaleResolver"
+ key_links:
+ - from: "../sm-translate-plugin/plugin.go"
+ to: "modules/surf/locale_resolver.go"
+ via: "Boot publishes the framework interface into backpack"
+ pattern: "LocaleResolver"
+ - from: "modules/surf/router.go"
+ to: "modules/towel/context.go"
+ via: "validated resolver result is written with towel.WithLocale"
+ pattern: "WithLocale"
+ - from: "../sm-translate-plugin/classes/translator.go"
+ to: "../sm-translate-plugin/models/locale.go"
+ via: "every candidate is checked against enabled Locale rows"
+ pattern: "is_enabled"
+---
+
+
+Create the translate plugin repository, exact Winter-compatible schema and seed, Locale model, context-safe Translator, and optional surf resolver seam.
+
+Purpose: prove one real request locale path from a mounted compiled plugin through Postgres-backed enabled locales into `towel.WithLocale` before adding the broader model/admin surface.
+Output: a compiling sibling plugin and framework seam, with no new dependencies and no Messages admin.
+
+
+
+@~/.codex/gsd-core/workflows/execute-plan.md
+@~/.codex/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/STATE.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
+@../fonoteka.go/plugins/golem15/user/plugin.go
+@../fonoteka.go/plugins/golem15/user/updates/00_base.go
+@modules/surf/router.go
+@modules/surf/locale_from_principal.go
+
+
+## Spec-less probe fallback
+
+Phase 14.2.1 has no mapped REQUIREMENTS.md IDs, so requirement probing is visibly skipped. This plan uses D-01/D-02/D-03/D-04/D-07/D-08/D-10/D-12/D-15/D-16 and the RESEARCH validation rows as its acceptance contract.
+
+## Artifacts this phase produces
+
+- Module `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, `Plugin.ID() == "golem15.translate"`.
+- `models.Locale`, model and migration registries, five gormigrate entries, and exact `winter_translate_*` tables.
+- `classes.Translator`, `Translator.Locale(context.Context)`, and a request resolver implementing surf's `LocaleResolver`.
+- Surf optional resolver seam with unchanged fallback behavior when the translate plugin is absent.
+- Application-neutral plugin and surf README updates for exported contracts.
+
+
+
+
+ Task 1: Create the public Gitea remote and authorize local repository setup
+ ../sm-translate-plugin/
+ .planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md (D-15/D-16 and Remotes), .planning/notes/core-plugins-own-repos.md
+ The user creates the public empty Gitea repository `git@git.golem15.com:golem15/sm-translate-plugin.git`, because this account-level operation is the only non-automatable prerequisite. After the user resumes, verify it with `git ls-remote`, then create or clone `/media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin`, initialize `main` as required by the server, and set origin. Also clone the already-existing `sm-grzybyfunkcjonalne-app` remote into `/media/nvme/dev/golem15/summercms.io/summercms/sm-grzybyfunkcjonalne-app`; leave its source layout empty until the D-13 decision gate in Plan 03.
+ Create one empty public repository in Gitea with owner `golem15` and name `sm-translate-plugin`; do not add generated README, license, or gitignore. Return here after Gitea displays the SSH URL.
+ The executor runs `git ls-remote`, clones/initializes both local checkouts, and verifies their origin URLs before continuing.
+
+ git ls-remote git@git.golem15.com:golem15/sm-translate-plugin.git
+ Non-zero exit, authentication denial, or repository-not-found text.
+
+
+ - The remote is public and reachable at the exact D-15 SSH URL.
+ - `git -C ../sm-translate-plugin remote get-url origin` prints `git@git.golem15.com:golem15/sm-translate-plugin.git`.
+ - The local checkout is the D-16 sibling path, not an install/runtime mirror.
+ - `git -C ../sm-grzybyfunkcjonalne-app rev-parse --is-inside-work-tree` prints `true`, and its origin names the already-created proof-host remote; no submodule layout has been committed yet.
+
+ The intended tracked plugin repository exists locally and remotely, ready for source commits.
+ Reply `remote-created` after the empty public repository exists.
+
+
+
+ Task 2: Confirm the one-way Winter table contract
+ ../sm-translate-plugin/updates/
+ .planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md (D-10), .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Tables, columns, indexes), /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/updates/version.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/Locale.php
+ Record the selected schema contract before migrations are implemented. Research resolved D-10's provisional prefix against the frozen source: the only compatible selection is the final `winter_translate_*` table family. This gate records the one-way import/storage choice; it does not reopen the PHP pin or permit a JSON-column alternative.
+ Which persistent table contract should this shared plugin publish?
+ Once released and imported by Journal, renaming these tables requires coordinated data migrations in every host. The frozen PHP pin explicitly reads `winter_translate_*` after its RainLab rename history.
+
+
+
+
+
+
+ test -f /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/Locale.php && git -C /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate rev-parse HEAD
+ Non-zero exit or the printed SHA is not `725d547ec839f02b5fdc0f0a6faaed601a414d50`.
+
+
+ - The response is recorded in the summary.
+ - Task 3 starts only for `winter-final`; either alternative stops execution as a locked-decision conflict.
+
+ Select `winter-final` to implement the locked/researched contract, or select an alternative to stop.
+
+
+
+ Task 3: Resolve one URL-prefixed request through the plugin, enabled Locale row, surf, and context
+ The four table names and columns become the import target; changing them after release requires data migrations in every host.
+ Tasks 1 and 2 completed, with schema option `winter-final`.
+ ../sm-translate-plugin/go.mod, ../sm-translate-plugin/plugin.go, ../sm-translate-plugin/README.md, ../sm-translate-plugin/config/config.yaml, ../sm-translate-plugin/models/registry.go, ../sm-translate-plugin/models/locale.go, ../sm-translate-plugin/updates/registry.go, ../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go, ../sm-translate-plugin/updates/202610060002_create_winter_translate_attributes.go, ../sm-translate-plugin/updates/202610060003_create_winter_translate_indexes.go, ../sm-translate-plugin/updates/202610060004_create_winter_translate_messages.go, ../sm-translate-plugin/updates/202610060005_seed_en_pl_locales.go, ../sm-translate-plugin/classes/translator.go, modules/surf/locale_resolver.go, modules/surf/router.go, modules/surf/README.md
+ ../fonoteka.go/plugins/golem15/user/go.mod, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/user/models/registry.go, ../fonoteka.go/plugins/golem15/user/updates/registry.go, ../fonoteka.go/plugins/golem15/user/updates/00_base.go, modules/backpack/app.go, modules/surf/router.go, modules/surf/locale_from_principal.go, modules/towel/context.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/Translator.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/LocaleMiddleware.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/ApiLocaleMiddleware.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/config/config.php, .planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
+ Implement the production tracer and the complete Plan 01 scope.
+
+Create the Go 1.27 module using only existing GORM/gormigrate/framework dependencies. Register the compiled plugin in `init`, expose config/models/migrations, and publish a framework-owned `surf.LocaleResolver` in Boot. Keep `Requires()` empty so user-preference lookup is optional.
+
+Implement exact squashed DDL for `winter_translate_locales`, `winter_translate_attributes`, `winter_translate_indexes`, and `winter_translate_messages`, including all final columns and PHP-indexed columns from RESEARCH. Messages is DDL only and remains empty. Use explicit gormigrate up/down; never AutoMigrate and never create rainlab or provisional-prefixed tables. Seed only en/pl idempotently with D-08 values.
+
+Implement `Locale` with no timestamps, table name, code/name rules, and Fillable limited to code/name/is_enabled. Implement Translator/Resolver with the exact D-07 order: enabled URL first segment; valid principal preferred_locale; remembered `golem15.translate.locale`; Accept-Language only when browser detection is enabled and `locale_manually_set` is absent; enabled default. The manual cookie is a flag with value 1, never a locale. Validate every candidate against enabled rows. URL-prefix handling strips only a valid enabled prefix before routing and sets the manual flag using configured expiry. Provide the API resolver variant preferred → Accept-Language → default without persistence. All current-locale access takes context.
+
+Add the surf interface and optional lookup without importing the plugin. When no resolver is published, retain existing Accept-Language behavior and principal overlay so hosts without translate do not regress. Update surf README for the exported seam and the new plugin README using only neutral host-application examples.
+
+ go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1 && go vet ./modules/surf/... && go test ./modules/surf -short -count=1
+ Non-zero exit, any package reports build failed, or either test command reports FAIL.
+
+
+ - All four migration files contain their exact `winter_translate_*` table name; no source file contains `golem15_translate_` or `rainlab_translate_`.
+ - Seed source contains en and pl with en default/enabled and pl enabled/non-default, and contains no Deutsch seed.
+ - Plugin module/ID/package match D-15 and plugin Boot publishes the framework resolver interface.
+ - Resolver order is URL → principal → remembered locale → cookie-gated Accept-Language → default.
+ - `locale_manually_set` is treated only as a flag; locale candidates are checked against enabled rows.
+ - There is no mutable package-level request locale and surf writes the selected code with `towel.WithLocale`.
+ - PHP source tree has no diff.
+
+ A production URL-prefix request resolves through a mounted compiled plugin to a validated locale on context, while the schema and en/pl rows are ready for later model/admin slices.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| HTTP URL/header/cookies → Translator | Untrusted locale candidates enter request context |
+| Plugin → surf service registry | Optional compiled plugin supplies a framework interface |
+| gormigrate → Postgres | Persistent import-compatible schema is created |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-14.2.1-01 | Tampering | Translator locale inputs | high | mitigate | Validate every URL, principal, remembered, and header candidate against enabled Locale rows |
+| T-14.2.1-02 | Information Disclosure | request locale state | high | mitigate | Context-only locale; no process singleton; concurrent-request test in Plan 04 |
+| T-14.2.1-03 | Tampering | manual-selection cookie | medium | mitigate | Treat `locale_manually_set` as flag-only and revalidate remembered locale |
+| T-14.2.1-04 | Tampering | schema source | high | mitigate | Explicit gormigrate DDL from frozen SHA; no AutoMigrate or alternate prefixes |
+| T-14.2.1-SC | Tampering | package installs | high | mitigate | No new external package installation in this plan |
+
+ASVS L1: all high threats are blocked by implementation requirements and become fail-closed tests in Plan 04.
+
+
+
+Run the Task 3 command. Confirm `git diff -- /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate` is empty and `git ls-files` in the new plugin lists every created source file.
+
+
+
+- The plugin and surf packages compile and pass short tests.
+- Exact final tables and en/pl seed are implemented through gormigrate.
+- Full resolver order is implemented without process-wide locale state.
+- No deferred Messages UI, CMS components, AI/theme commands, import/export, or PHP edits are introduced.
+
+
+
diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md
new file mode 100644
index 0000000..9ec78c1
--- /dev/null
+++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md
@@ -0,0 +1,211 @@
+---
+phase: 14.2.1-translate-plugin
+plan: 02
+type: execute
+wave: 2
+depends_on: ["14.2.1-01"]
+files_modified:
+ - ../sm-translate-plugin/classes/translatable.go
+ - ../sm-translate-plugin/classes/translatable_smoke_test.go
+ - ../sm-translate-plugin/models/attribute.go
+ - ../sm-translate-plugin/models/index.go
+ - ../sm-translate-plugin/admin.go
+ - ../sm-translate-plugin/admin_permissions.go
+ - ../sm-translate-plugin/admin_navigation.go
+ - ../sm-translate-plugin/controllers/admin_registry.go
+ - ../sm-translate-plugin/controllers/locales.go
+ - ../sm-translate-plugin/controllers/locales/config_list.yaml
+ - ../sm-translate-plugin/controllers/locales/config_form.yaml
+ - ../sm-translate-plugin/models/locale/fields.yaml
+ - ../sm-translate-plugin/models/locale/columns.yaml
+ - ../sm-translate-plugin/lang/en/lang.yaml
+ - ../sm-translate-plugin/lang/pl/lang.yaml
+ - ../sm-translate-plugin/locales_admin_smoke_test.go
+ - ../sm-translate-plugin/README.md
+autonomous: true
+requirements: [D-05, D-08, D-09, D-10, D-11, D-12, D-17]
+must_haves:
+ truths:
+ - "D-09/D-12: a host model declares `Translatable() []string`, `TranslatableIndexes() []string`, and `MorphName() string`; callers use exported get/set and `WithLocale` APIs."
+ - "D-10: default-locale values remain in host columns; each non-default locale is one JSON object row in `winter_translate_attributes`, and indexed values are mirrored to `winter_translate_indexes`."
+ - "D-11: a missing non-default value reads the host model's default-locale field."
+ - "D-05: Locales is ordinary cabana CRUD, permission-gated by exactly `golem15.translate.manage_locales`; no Messages controller, permission, or navigation exists."
+ - "Locale form writes only code/name/is_enabled; is_default and sort_order cannot be mass-assigned, while a permissioned make-default action preserves disabled/default lifecycle guards."
+ - "D-17: a neutral fixture saves and reads en/pl and performs an indexed locale lookup through the production APIs."
+ artifacts:
+ - path: "../sm-translate-plugin/classes/translatable.go"
+ provides: "Translatable contracts, WithLocale, Translated, SetTranslated, indexed lookup"
+ contains: "type Translatable interface"
+ - path: "../sm-translate-plugin/controllers/locales.go"
+ provides: "permissioned Locale admin controller and default guards"
+ contains: "golem15.translate.manage_locales"
+ - path: "../sm-translate-plugin/controllers/locales/config_list.yaml"
+ provides: "sort_order asc Locales list"
+ contains: "sort_order"
+ - path: "../sm-translate-plugin/lang/pl/lang.yaml"
+ provides: "Polish Locales admin phrasebook"
+ contains: "locale:"
+ key_links:
+ - from: "../sm-translate-plugin/classes/translatable.go"
+ to: "winter_translate_attributes"
+ via: "non-default SetTranslated upserts one locale/model row"
+ pattern: "attribute_data"
+ - from: "../sm-translate-plugin/classes/translatable.go"
+ to: "winter_translate_indexes"
+ via: "indexed fields upsert locale/model/item/value"
+ pattern: "TranslatableIndexes"
+ - from: "../sm-translate-plugin/controllers/locales.go"
+ to: "../sm-translate-plugin/models/locale.go"
+ via: "cabana CRUD hooks enforce PHP default-locale invariants"
+ pattern: "RequiredPermissions"
+---
+
+
+Ship the explicit Translatable model API and the permissioned Locales admin, proven by an en/pl fixture through real Postgres storage.
+
+Purpose: give Journal a stable compiled API and let operators manage enabled/default locales without exposing raw attribute rows or Messages.
+Output: translatable contracts/helpers/index lookup, Locales YAML/controller/navigation/permissions/phrasebook, smoke fixture.
+
+
+
+@~/.codex/gsd-core/workflows/execute-plan.md
+@~/.codex/gsd-core/templates/summary.md
+
+
+
+@.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
+@../fonoteka.go/plugins/golem15/user/admin.go
+@../fonoteka.go/plugins/golem15/user/controllers/usergroups_admin_controller.go
+@../fonoteka.go/plugins/golem15/user/admin_harness_test.go
+
+
+## Spec-less probe fallback
+
+No REQUIREMENTS.md IDs are mapped to this phase. Probe predicates are intentionally omitted; D-05/D-08/D-09/D-10/D-11/D-12/D-17 and RESEARCH's validation rows are the visible source contract.
+
+## Artifacts this phase produces
+
+- Exported `classes.Translatable`, optional indexed-field contract, `WithLocale`, `Translated`, `SetTranslated`, and locale-aware indexed lookup.
+- Internal `models.Attribute` and index record mapping with no public CRUD controller.
+- `golem15.translate.manage_locales`, Locales navigation/controller, embedded list/form/fields/columns YAML.
+- English and Polish phrasebook catalogs for plugin/locale admin strings, separate from model translation JSON.
+- Neutral fixture smoke proving en/pl save/read, fallback, and indexed lookup.
+
+
+
+
+ Task 1: Save one fixture title in en and pl, then read pl through the exported API
+ ../sm-translate-plugin/classes/translatable.go, ../sm-translate-plugin/classes/translatable_smoke_test.go, ../sm-translate-plugin/models/attribute.go, ../sm-translate-plugin/models/index.go, ../sm-translate-plugin/models/registry.go
+ /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/behaviors/TranslatableModel.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/TranslatableBehavior.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/tests/unit/behaviors/TranslatableModelTest.php, ../sm-translate-plugin/models/locale.go, ../sm-translate-plugin/updates/202610060002_create_winter_translate_attributes.go, ../sm-translate-plugin/updates/202610060003_create_winter_translate_indexes.go, ../fonoteka.go/plugins/golem15/user/models/user.go (MorphName), .planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md (Translatable assignment)
+ Implement the minimum stable D-09/D-12 API without Eloquent-style magic. Define a host-model contract with `Translatable() []string` and `MorphName() string`, plus an explicit indexed-field contract `TranslatableIndexes() []string`. Reject fields not declared by the model and locale codes not enabled in `winter_translate_locales`.
+
+`SetTranslated` writes the default locale directly to the host model field/column and writes non-default values to one `(locale, model_id, model_type)` `winter_translate_attributes` row as a JSON object keyed by field. Use explicit model primary-key extraction and explicit exported-field/GORM-column mapping; do not use `reflect.Type.String()` as morph identity. Attribute rows have no public controller.
+
+`Translated` returns the host field for default locale; for another enabled locale it reads the JSON key and, when absent, returns the default host field per D-11. Keep an explicit locale argument even when context has a UI locale.
+
+Create a neutral fixture model only in the smoke test, migrate its host table explicitly, seed en/pl through Plan 01 migrations, save English and Polish title values, and read Polish back. This smoke is the production tracer, not the full test matrix reserved for Plan 04.
+
+ go -C ../sm-translate-plugin test ./classes -count=1 -v -run '^(TestFixtureTranslatableSaveRead)$'
+ Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestFixtureTranslatableSaveRead".
+
+
+ - Exported interfaces and functions compile from an external test package.
+ - The default English value is in the fixture host row, not duplicated into `winter_translate_attributes`.
+ - The Polish value is present under `attribute_data.title` in exactly one pl/model row.
+ - Undeclared fields and unenabled locale codes return errors without database writes.
+ - Test fixture is test-only and no production fixture plugin is globally registered.
+
+ A neutral model persists its default title on its own row, persists Polish in Winter storage, and reads Polish through the exported API.
+
+
+
+ Task 2: Add indexed locale lookup, WithLocale query scope, and default fallback
+ ../sm-translate-plugin/classes/translatable.go, ../sm-translate-plugin/classes/translatable_smoke_test.go, ../sm-translate-plugin/README.md
+ ../sm-translate-plugin/classes/translatable.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/behaviors/TranslatableModel.php (scopeTransWhere and index writes), /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/Post.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/Category.php, .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Minimum exported Go API)
+ Expand from the proven save/read path. `WithLocale(ctx, db, locale)` validates the explicit locale and carries it on the GORM statement context; it must not mutate global state. Add an indexed equality helper/scope that joins `winter_translate_indexes` by locale, morph name, model id, and item, while default-locale lookup uses the host column and non-default lookup falls back to that host column only when no translated index match exists, matching the frozen PHP behavior.
+
+When SetTranslated writes a declared indexed field, upsert the corresponding index row atomically with the attribute JSON update. Rewriting one locale/field must preserve other translated fields in the same JSON object. Empty translated values stay explicit empty values rather than silently borrowing fallback during admin editing; ordinary `Translated` still applies D-11 fallback.
+
+Document only the exported identifiers that exist, MorphName's import-stability requirement, and the default-row/non-default-attribute storage model. Use neutral `blog`/`acme` examples and do not mention a consuming application.
+
+ go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./classes -count=1 -v -run '^(TestFixtureTranslatableSaveRead|TestFixtureTranslatedIndexSmoke)$'
+ Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.
+
+
+ - `WithLocale` returns a GORM handle carrying an explicit validated locale on context and has no singleton mutation.
+ - Setting an indexed Polish slug writes one matching `winter_translate_indexes` row.
+ - Polish indexed lookup finds the fixture; default-locale lookup uses the host slug.
+ - Missing Polish title returns English under normal reads per D-11.
+ - README names every exported API identifier accurately and remains application-neutral.
+
+ Journal can query and mutate indexed translated slugs and ordinary translated fields through the minimum public contract.
+
+
+
+ Task 3: Manage Locales through permissioned YAML CRUD and separate phrasebook catalogs
+ ../sm-translate-plugin/admin.go, ../sm-translate-plugin/admin_permissions.go, ../sm-translate-plugin/admin_navigation.go, ../sm-translate-plugin/controllers/admin_registry.go, ../sm-translate-plugin/controllers/locales.go, ../sm-translate-plugin/controllers/locales/config_list.yaml, ../sm-translate-plugin/controllers/locales/config_form.yaml, ../sm-translate-plugin/models/locale/fields.yaml, ../sm-translate-plugin/models/locale/columns.yaml, ../sm-translate-plugin/lang/en/lang.yaml, ../sm-translate-plugin/lang/pl/lang.yaml, ../sm-translate-plugin/locales_admin_smoke_test.go, ../sm-translate-plugin/README.md
+ ../fonoteka.go/plugins/golem15/user/admin.go, ../fonoteka.go/plugins/golem15/user/admin_permissions.go, ../fonoteka.go/plugins/golem15/user/admin_navigation.go, ../fonoteka.go/plugins/golem15/user/controllers/admin_registry.go, ../fonoteka.go/plugins/golem15/user/controllers/usergroups_admin_controller.go, ../fonoteka.go/plugins/golem15/user/controllers/usergroups/config_list.yaml, ../fonoteka.go/plugins/golem15/user/admin_harness_test.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/controllers/Locales.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/controllers/locales/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/locale/fields.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/locale/columns.yaml
+ Implement Locales as `HasAdminControllers` plus `HasNavigation`, never as singleton settings. Register only `golem15.translate.manage_locales` with developer role and require it on controller `golem15.translate.locales`. Do not register manage_messages.
+
+Embed exact YAML paths. Port name/code/enabled/default form fields and searchable list columns; map unsupported invisible number display safely, keep default sort by sort_order ascending and 20 records per page, and use cabana recordUrl/create/update conventions. Do not invent ReorderController.
+
+Protect server-owned state: Locale Fillable remains code/name/is_enabled, so request bodies cannot set is_default or sort_order. Controller/model hooks refuse deleting the default, unsetting the default, and making a disabled locale default with cabana validation errors. Provide an explicit `golem15.translate.manage_locales`-permissioned make-default controller action that atomically clears the previous default and sets the selected enabled locale while preserving those guards; keep direct `is_default` form input read-only.
+
+Port the plugin.* and locale.* UI phrases to `lang/en/lang.yaml` and `lang/pl/lang.yaml` via HasLang. These catalogs are UI phrasebook data and must not read or write attribute JSON. Add smoke coverage for permitted list access and a forbidden request; Plan 04 expands the matrix.
+
+ go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1 -v -run '^(TestLocalesAdminSmoke|TestLocalesAdminForbiddenSmoke)$'
+ Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.
+
+
+ - Controller ID is `golem15.translate.locales` and RequiredPermissions contains only `golem15.translate.manage_locales`.
+ - No production source contains `manage_messages`, a Messages controller, ReorderController, or Messages navigation.
+ - List defaults to sort_order asc and returns en before pl from the seed.
+ - Unknown/unprivileged admin gets 403; privileged admin gets the Locales schema/list.
+ - A crafted body cannot persist is_default or sort_order.
+ - The permissioned make-default action rejects disabled locales and atomically leaves exactly one enabled default locale.
+ - English/Polish phrasebook files are independent from `winter_translate_attributes`.
+
+ Authorized administrators can manage the lean Locale surface while default-state and phrasebook/model-translation boundaries remain enforced.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Host model → Translatable helpers | Shared plugin trusts only declared fields and explicit morph names |
+| Admin JSON → Locale CRUD | Untrusted nested/scalar writes cross permission and fillable boundaries |
+| Translation JSON → Postgres | Field maps and indexes must stay scoped to one model/locale |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-14.2.1-05 | Elevation of Privilege | Locales admin | high | mitigate | RequiredPermissions and 403 smoke; full tests in Plan 04 |
+| T-14.2.1-06 | Tampering | Locale writes | high | mitigate | Fillable excludes is_default/sort_order and hooks protect default lifecycle |
+| T-14.2.1-07 | Tampering | SetTranslated | high | mitigate | Allow only declared fields and enabled locale codes; atomic scoped upserts |
+| T-14.2.1-08 | Information Disclosure | Morph/index queries | medium | mitigate | Explicit MorphName plus model id/locale/item predicates; no broad attribute endpoint |
+| T-14.2.1-SC | Tampering | package installs | high | mitigate | No new packages |
+
+ASVS L1: all high threats are mitigated in production paths and receive removal/failure tests in Plan 04.
+
+
+
+Run all three task commands, then `go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1`.
+
+
+
+- Fixture en/pl save/read and indexed lookup pass.
+- Missing translations fall back to the default host column.
+- Locales admin is permissioned, YAML-driven, and protects default/server-owned fields.
+- Messages admin, CMS components, AI/theme commands, import/export, ReorderController, and extra locale seeds remain absent.
+
+
+
diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md
new file mode 100644
index 0000000..041e54e
--- /dev/null
+++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md
@@ -0,0 +1,242 @@
+---
+phase: 14.2.1-translate-plugin
+plan: 03
+type: execute
+wave: 3
+depends_on: ["14.2.1-02"]
+files_modified:
+ - modules/cabana/form_schema.go
+ - modules/cabana/crud.go
+ - modules/cabana/field_ml.go
+ - modules/cabana/field_markdown.go
+ - modules/cabana/ml_smoke_test.go
+ - modules/cabana/README.md
+ - docs/backend/forms.md
+ - docs/backend/admin-controllers.md
+ - admin/src/components/form/registry.ts
+ - admin/src/components/form/formState.ts
+ - admin/src/components/form/fields/MarkdownField.vue
+ - admin/src/components/form/fields/MLTextField.vue
+ - admin/src/components/form/fields/MLMarkdownField.vue
+ - admin/tests/form/registry.test.ts
+ - admin/tests/form/MLFields.test.ts
+ - admin/openapi/admin.json
+ - admin/src/api/schema.d.ts
+ - modules/boardwalk/dist/
+ - ../sm-translate-plugin/classes/admin_writer.go
+ - ../sm-translate-plugin/plugin.go
+ - ../sm-grzybyfunkcjonalne-app/go.mod
+ - ../sm-grzybyfunkcjonalne-app/go.work
+ - ../sm-grzybyfunkcjonalne-app/summer.yaml
+ - ../sm-grzybyfunkcjonalne-app/.gitmodules
+ - ../sm-grzybyfunkcjonalne-app/main.go
+ - ../sm-grzybyfunkcjonalne-app/plugins.gen.go
+ - ../sm-grzybyfunkcjonalne-app/boot_test.go
+autonomous: false
+requirements: [D-06, D-09, D-12, D-13, D-14, D-15, D-16, D-17]
+must_haves:
+ truths:
+ - "D-06: cabana accepts `markdown`, `mltext`, and `mlmarkdown`; `mlmarkdown` composes the same markdown primitive with locale switching."
+ - "Nested locale maps are lifted before `ProjectWritableFields` drops maps; default locale fills the host field and non-default locales reach the translate writer inside the host save transaction."
+ - "The SPA exposes one locale selector per ML field, switches all ML controls together, supports copy-from-locale, and sends every locale as `Record`."
+ - "Markdown preview uses goldmark without unsafe HTML and cannot execute translated raw HTML/script/event-handler/javascript content."
+ - "Cabana README, forms/admin docs, OpenAPI, generated TS types, and committed `modules/boardwalk/dist/` are regenerated in the same change."
+ - "D-13/D-14/D-15/D-16/D-17: `sm-grzybyfunkcjonalne-app` mounts user and translate as submodules, uses go.work/local replaces, and boots both compiled plugins."
+ artifacts:
+ - path: "modules/cabana/field_ml.go"
+ provides: "ML nested-value lift and TranslationWriter bridge"
+ contains: "TranslationWriter"
+ - path: "admin/src/components/form/fields/MLMarkdownField.vue"
+ provides: "locale-aware markdown source editor"
+ contains: "modelValue"
+ - path: "docs/backend/forms.md"
+ provides: "verified field-type documentation"
+ contains: "mlmarkdown"
+ - path: "../sm-grzybyfunkcjonalne-app/summer.yaml"
+ provides: "compiled user+translate plugin list"
+ contains: "golem15.translate"
+ - path: "../sm-grzybyfunkcjonalne-app/boot_test.go"
+ provides: "proof-host boot smoke"
+ contains: "TestBootUserTranslate"
+ key_links:
+ - from: "modules/cabana/crud.go"
+ to: "modules/cabana/field_ml.go"
+ via: "ML values are lifted before ProjectWritableFields"
+ pattern: "liftMLValues"
+ - from: "../sm-translate-plugin/classes/admin_writer.go"
+ to: "../sm-translate-plugin/classes/translatable.go"
+ via: "published cabana TranslationWriter delegates each locale to SetTranslated"
+ pattern: "SetTranslated"
+ - from: "../sm-grzybyfunkcjonalne-app/summer.yaml"
+ to: "../sm-grzybyfunkcjonalne-app/plugins.gen.go"
+ via: "summer build generates blank imports for both submodules"
+ pattern: "sm-translate-plugin"
+---
+
+
+Add cabana's markdown and multilingual fields end to end, preserve nested locale writes, regenerate every public/generated artifact, and boot the user+translate proof host.
+
+Purpose: prove a Journal-shaped form can submit localized text/markdown through the generic admin stack without a Node extension or dropped nested JSON.
+Output: framework schema/save/UI/docs/OpenAPI/dist changes, translate writer adapter, and the D-13 proof-host layout.
+
+
+
+@~/.codex/gsd-core/workflows/execute-plan.md
+@~/.codex/gsd-core/templates/summary.md
+
+
+
+@.planning/phases/14.2.1-translate-plugin/14.2.1-02-SUMMARY.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
+@modules/cabana/form_schema.go
+@modules/cabana/crud.go
+@modules/cabana/field_permission.go
+@admin/src/components/form/registry.ts
+@admin/src/components/form/formState.ts
+@../sm-bm-app/summer.yaml
+@../sm-bm-app/go.work
+
+
+## Spec-less probe fallback
+
+The phase has no mapped requirement IDs, so no speculative requirement probes are generated. D-06/D-09/D-12/D-13/D-14/D-15/D-16/D-17 and RESEARCH's cabana/host validation rows are the acceptance source.
+
+## Artifacts this phase produces
+
+- Cabana field types `markdown`, `mltext`, `mlmarkdown`, `TranslationWriter`, nested-map lifting, and safe `RenderMarkdown`.
+- Vue `MarkdownField`, `MLTextField`, `MLMarkdownField`, synchronized selector/copy behavior, and registry/form-state support.
+- Updated cabana README and backend docs; regenerated `admin/openapi/admin.json`, `admin/src/api/schema.d.ts`, and `modules/boardwalk/dist/`.
+- Translate plugin's cabana writer adapter published at Boot.
+- Proof host source layout with user and translate submodules, local workspace/replaces, generated plugin list, and `TestBootUserTranslate`.
+
+
+
+
+ Task 1: Submit one mltext map through cabana and persist default plus Polish values
+ modules/cabana/form_schema.go, modules/cabana/crud.go, modules/cabana/field_ml.go, modules/cabana/field_markdown.go, modules/cabana/ml_smoke_test.go, admin/src/components/form/registry.ts, admin/src/components/form/formState.ts, admin/src/components/form/fields/MarkdownField.vue, admin/src/components/form/fields/MLTextField.vue, admin/src/components/form/fields/MLMarkdownField.vue, admin/tests/form/MLFields.test.ts, ../sm-translate-plugin/classes/admin_writer.go, ../sm-translate-plugin/plugin.go
+ modules/cabana/form_schema.go (formFieldTypes and compileFieldNode), modules/cabana/crud.go (save, ProjectWritableFields, BindWritableFields, scalarFormField), modules/cabana/field_permission.go (liftPermissionValues), modules/postcard/templates.go (goldmark safe path), admin/src/components/form/registry.ts, admin/src/components/form/formState.ts, admin/src/components/form/fields/TextField.vue, admin/src/components/form/fields/TextareaField.vue, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/traits/MLControl.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/traits/mlcontrol/partials/_locale_selector.htm, ../sm-translate-plugin/classes/translatable.go
+ Register exactly `markdown`, `mltext`, and `mlmarkdown` as fail-loud schema types without adding unnecessary YAML keys. Define a framework-owned cabana `TranslationWriter` service contract so cabana never imports the translate plugin; implement and publish its adapter from sm-translate-plugin.
+
+In CRUD save, identify declared ML fields and lift each `map[locale]text` before `ProjectWritableFields` evaluates nested values. Reject non-string values, undeclared locales, and extra field names with validation errors. Fill only the default-locale scalar into the host model, then call the writer for non-default values inside the same lagoon/cabana transaction and after controller permission/query scoping has succeeded. Never expose a public translate-write endpoint.
+
+Implement `RenderMarkdown` using the already-pinned goldmark without unsafe HTML; reject unsafe output patterns consistently with postcard. `MarkdownField` edits source and may preview only sanitized output. Build ML components around a `Record` value: active locale editor, selector, copy-from-locale, and a shared form event so changing one selector changes all ML controls. `mlmarkdown` composes `MarkdownField`, not a duplicate parser/editor. Ensure formState sends nested ML records.
+
+Create one focused Go smoke fixture using the Plan 02 writer adapter and a cabana save body `{title:{en,pl}}`. It must prove the nested map survives projection, English reaches the host field, and Polish reaches attribute JSON. Add a focused SPA smoke asserting registry resolution and emitted nested values; full edge/coverage tests remain Plan 04.
+
+ go test ./modules/cabana -count=1 -v -run '^(TestMLNestedSaveSmoke)$' && npm --prefix admin test -- --run admin/tests/form/MLFields.test.ts
+ Non-zero exit; Go output contains "--- FAIL", "--- SKIP", or "no tests to run", lacks "--- PASS: TestMLNestedSaveSmoke", or Vitest reports no test files/tests or any failed test.
+
+
+ - A compiled schema accepts all three exact field types and still rejects unknown types.
+ - ML map lifting occurs before `ProjectWritableFields`; generic nested maps remain rejected/dropped by existing rules.
+ - Default locale fills the host scalar and is not duplicated in attributes; Polish persists through TranslationWriter.
+ - Writer execution occurs only after host-controller authorization/scoping and inside the save transaction.
+ - Raw script/iframe/event-handler/javascript markdown cannot become executable preview HTML.
+ - SPA payload contains all locale keys, and `mlmarkdown` reuses `MarkdownField`.
+
+ A real cabana save carries one multilingual title from Vue-shaped JSON through projection and transactional persistence without dropping or broadening the write.
+
+
+
+ Task 2: Confirm the one-way proof-host submodule layout
+ ../sm-grzybyfunkcjonalne-app/.gitmodules, ../sm-grzybyfunkcjonalne-app/go.work, ../sm-grzybyfunkcjonalne-app/go.mod, ../sm-grzybyfunkcjonalne-app/summer.yaml
+ .planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md (D-13 through D-17), .planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md (Proof host), ../sm-bm-app/.gitmodules, ../sm-bm-app/go.work, ../sm-bm-app/go.mod, ../sm-bm-app/summer.yaml
+ Record the D-13 host layout before adding gitlinks. The selected contract is the existing `sm-grzybyfunkcjonalne-app` repository with `plugins/golem15/user` and `plugins/golem15/translate` submodules plus go.work and local replace entries, following sm-bm-app. Journal is deliberately absent until Phase 15.
+ Which durable proof-host layout should receive the translate gitlink?
+ Changing submodule paths after downstream clones and Phase 15 Journal mounts requires coordinated gitlink, workspace, replace, and deployment changes.
+
+
+
+
+
+
+ git -C ../sm-grzybyfunkcjonalne-app rev-parse --is-inside-work-tree && git -C ../sm-grzybyfunkcjonalne-app remote get-url origin
+ Non-zero exit or origin is not the existing sm-grzybyfunkcjonalne-app remote.
+
+
+ - The selection is recorded in the summary.
+ - Task 3 proceeds only with `grzyby-submodules`; another selection stops as a D-13/D-16 conflict.
+
+ Select `grzyby-submodules` to implement the locked host, or an alternative to stop.
+
+
+
+ Task 3: Complete docs/OpenAPI/TS/dist and boot the user+translate proof host
+ The committed user/translate gitlink paths become the Phase 15 host layout; moving them later requires coordinated submodule and workspace migration.
+ Task 2 selected `grzyby-submodules`, and the Plan 02 translate commit is reachable from its Gitea remote.
+ modules/cabana/README.md, docs/backend/forms.md, docs/backend/admin-controllers.md, admin/tests/form/registry.test.ts, admin/tests/form/MLFields.test.ts, admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/boardwalk/dist/, ../sm-grzybyfunkcjonalne-app/go.mod, ../sm-grzybyfunkcjonalne-app/go.work, ../sm-grzybyfunkcjonalne-app/summer.yaml, ../sm-grzybyfunkcjonalne-app/.gitmodules, ../sm-grzybyfunkcjonalne-app/main.go, ../sm-grzybyfunkcjonalne-app/plugins.gen.go, ../sm-grzybyfunkcjonalne-app/boot_test.go
+ modules/cabana/README.md, docs/backend/forms.md, docs/backend/admin-controllers.md, modules/cabana/openapi_conformance_test.go, admin/package.json, admin/tests/form/registry.test.ts, ../sm-bm-app/go.mod, ../sm-bm-app/go.work, ../sm-bm-app/summer.yaml, ../sm-bm-app/.gitmodules, ../sm-bm-app/main.go, ../sm-bm-app/plugins.gen.go, .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Docs/OpenAPI/TS/dist and Proof-host layout)
+ Update cabana README and backend forms documentation for `markdown`, `mltext`, `mlmarkdown`, nested locale payloads, TranslationWriter, safe markdown, and fail-loud behavior. Update admin-controller docs only where activation/save semantics changed. All examples use neutral blog/acme names. Remove the stale claim that markdown is unavailable.
+
+Regenerate admin OpenAPI from the established command, regenerate TypeScript schema with `npm --prefix admin run gen:api`, run the admin build, and copy the resulting Vite output to committed `modules/boardwalk/dist/` using the existing boardwalk build workflow. Do not hand-edit generated JSON, d.ts, or dist assets. Extend registry tests for all three field types.
+
+In the already-cloned D-14 host, use the sm-bm-app layout: module `git.golem15.com/golem15/sm-grzybyfunkcjonalne-app`, framework replace `../summercms.go`, submodules at `plugins/golem15/user` and `plugins/golem15/translate`, workspace uses and host replaces for each, and summer.yaml entries for only `golem15.user` and `golem15.translate`. Add submodules from their Gitea remotes, never copy runtime mirrors. Generate main.go/plugins.gen.go with `summer build`. Add `TestBootUserTranslate` that activates the real two plugins and verifies the Locales admin controller is registered; the fixture save/read proof remains the plugin integration smoke from Plans 02/03 rather than a third production plugin.
+
+ go test ./cmd/summer -count=1 -run 'TestDocsTree' && go run ./cmd/summer docs:build --check && npm --prefix admin run typecheck && npm --prefix admin test -- --run admin/tests/form/registry.test.ts admin/tests/form/MLFields.test.ts && npm --prefix admin run build && go -C ../sm-grzybyfunkcjonalne-app vet ./... && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslate)$'
+ Non-zero exit; docs checker reports stale identifiers/links/forbidden names; Vitest reports no tests or failures; build omits index.html/assets; host output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestBootUserTranslate".
+
+
+ - README/docs identifiers resolve and contain no consuming-application name.
+ - OpenAPI, schema.d.ts, and boardwalk dist are regenerated and tracked; no generated file is hand-edited.
+ - Registry resolves markdown/mltext/mlmarkdown and unknown ml types still fail boot.
+ - Host `.gitmodules`, `go.work`, `go.mod`, and `summer.yaml` use exact D-16 paths/module IDs.
+ - Generated plugin list imports sm-user-plugin and sm-translate-plugin; Journal is absent.
+ - Host smoke activates both plugins and sees the Locales admin controller.
+
+ The framework ships typed multilingual controls and the intended host boots user+translate from durable submodules with generated admin artifacts in sync.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Admin browser → cabana CRUD | Untrusted nested multilingual JSON crosses authenticated controller boundary |
+| Translated markdown → preview DOM | Stored content can carry active HTML payloads |
+| Cabana → plugin TranslationWriter | Framework delegates scoped writes to an optional plugin service |
+| Host gitlinks → compiled binary | Remote plugin commits become trusted build inputs |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-14.2.1-09 | Tampering | ML nested request | high | mitigate | Lift only declared ML fields; require string locale map; validate locales; execute after host authorization |
+| T-14.2.1-10 | Elevation of Privilege | TranslationWriter | high | mitigate | No standalone endpoint; writer runs inside permissioned/scoped host save transaction |
+| T-14.2.1-11 | Tampering | mass assignment | high | mitigate | Default scalar only enters writable host field; locale map never reaches generic Fill |
+| T-14.2.1-12 | Tampering | markdown preview | high | mitigate | Goldmark safe mode plus unsafe output rejection and CSP-compatible rendering |
+| T-14.2.1-13 | Tampering | host submodule provenance | medium | mitigate | Exact Gitea remotes and committed gitlinks; generated plugin imports |
+| T-14.2.1-SC | Tampering | npm packages | high | mitigate | No package installation or version change; existing exact pins only |
+
+ASVS L1: all high threats are mitigated; Plan 04 adds fail-when-broken evidence.
+
+
+
+Run the Task 3 combined gate. Confirm `git status --short` in all three repositories contains only intended tracked source/generated artifacts and gitlink updates.
+
+
+
+- ML nested saves persist both locales without widening mass assignment.
+- Markdown and multilingual controls compile, test, and appear in generated API/types/dist.
+- Docs checks pass with application-neutral examples.
+- Proof host boots real user+translate plugins and exposes Locales admin.
+
+
+
diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-04-PLAN.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-04-PLAN.md
new file mode 100644
index 0000000..793f985
--- /dev/null
+++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-04-PLAN.md
@@ -0,0 +1,246 @@
+---
+phase: 14.2.1-translate-plugin
+plan: 04
+type: execute
+wave: 4
+depends_on: ["14.2.1-03"]
+files_modified:
+ - ../sm-translate-plugin/updates/postgres_test.go
+ - ../sm-translate-plugin/updates/migrations_test.go
+ - ../sm-translate-plugin/classes/translator_test.go
+ - ../sm-translate-plugin/classes/translatable_test.go
+ - ../sm-translate-plugin/admin_harness_test.go
+ - ../sm-translate-plugin/locales_admin_test.go
+ - ../sm-translate-plugin/integration_test.go
+ - modules/surf/locale_resolver_test.go
+ - modules/cabana/ml_test.go
+ - modules/cabana/markdown_test.go
+ - modules/cabana/openapi_conformance_test.go
+ - admin/tests/form/MLFields.test.ts
+ - admin/tests/form/MarkdownField.test.ts
+ - ../sm-grzybyfunkcjonalne-app/boot_test.go
+ - scripts/check-phase14.2.1.sh
+ - .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md
+ - .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md
+autonomous: true
+requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11, D-12, D-13, D-14, D-15, D-16, D-17]
+must_haves:
+ truths:
+ - "A real-Postgres integration test migrates all four winter_translate tables, activates user+translate+fixture, saves en/pl through cabana ML fields, reads via WithLocale, and reaches Locales admin."
+ - "Migration up/down verifies every final column/index, empty Messages table, idempotent en/pl seed, absence of de, and complete rollback."
+ - "Translator tests prove URL → preferred_locale → remembered locale → cookie-gated Accept-Language → default, invalid-code rejection, API order, plugin-absent surf behavior, and concurrent request isolation."
+ - "Translatable tests prove default-row storage, non-default JSON, default fallback, indexed lookup, undeclared-field rejection, invalid-locale rejection, and atomic preservation of sibling fields."
+ - "Admin/ML tests prove manage_locales authorization, default-locale lifecycle guards, mass-assignment resistance, nested-map preservation, synchronized locale controls, and stored-XSS rejection."
+ - "All three repositories pass vet/tests; docs/OpenAPI/types/dist consistency checks pass; a security review closes every high threat."
+ artifacts:
+ - path: "../sm-translate-plugin/integration_test.go"
+ provides: "full production-path integration proof"
+ contains: "TestTranslateEndToEnd"
+ - path: "../sm-translate-plugin/updates/migrations_test.go"
+ provides: "real Postgres migration up/down and seed proof"
+ contains: "winter_translate_messages"
+ - path: "modules/surf/locale_resolver_test.go"
+ provides: "resolver-present/absent and request-isolation tests"
+ contains: "TestLocaleResolver"
+ - path: "modules/cabana/ml_test.go"
+ provides: "nested ML write and mass-assignment tests"
+ contains: "TestML"
+ - path: "scripts/check-phase14.2.1.sh"
+ provides: "fail-closed phase gate"
+ contains: "sm-translate-plugin"
+ - path: ".planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md"
+ provides: "ASVS L1 threat evidence"
+ contains: "T-14.2.1-01"
+ key_links:
+ - from: "../sm-translate-plugin/integration_test.go"
+ to: "modules/cabana/field_ml.go"
+ via: "fixture admin save uses real TranslationWriter"
+ pattern: "TestTranslateEndToEnd"
+ - from: "scripts/check-phase14.2.1.sh"
+ to: "../sm-translate-plugin/updates/migrations_test.go"
+ via: "full plugin test suite runs with Docker/Postgres, not -short"
+ pattern: "go -C"
+ - from: ".planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md"
+ to: "modules/cabana/ml_test.go"
+ via: "each mitigated high threat cites executed failure evidence"
+ pattern: "T-14.2.1"
+---
+
+
+Finish Phase 14.2.1 with the dedicated unit/integration/security test plan and one fail-closed gate across plugin, framework, admin SPA, and proof host.
+
+Purpose: make every locked decision and high-risk locale/admin write behavior observably fail when broken.
+Output: full test matrix, migration rollback proof, phase gate, security review, and validated validation map.
+
+
+
+@~/.codex/gsd-core/workflows/execute-plan.md
+@~/.codex/gsd-core/templates/summary.md
+
+
+
+@.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-02-SUMMARY.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-03-SUMMARY.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
+@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
+@../fonoteka.go/plugins/golem15/user/updates/postgres_test.go
+@../fonoteka.go/plugins/golem15/user/admin_harness_test.go
+@scripts/check-phase14.sh
+
+
+## Spec-less probe fallback
+
+The phase has no mapped REQUIREMENTS.md IDs. This is a visible, intentional skip of spec-less probes. Tests and gate rows map directly to D-01 through D-17, the frozen PHP pin, and the RESEARCH validation/threat tables.
+
+## Artifacts this phase produces
+
+- `TestTranslateEndToEnd` spanning migration, activation, resolver, permissioned admin, nested ML write, en/pl storage, fallback, and indexed query.
+- Real-Postgres migration/seed/rollback suite.
+- Translator, surf, Translatable, Locales admin, cabana ML/markdown, SPA, generated-contract, and proof-host tests.
+- `scripts/check-phase14.2.1.sh` with short/full/docs/admin/host/security stages.
+- `14.2.1-SECURITY-REVIEW.md` and a completed `14.2.1-VALIDATION.md`.
+
+## Multi-source coverage audit
+
+| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
+|---|---|---|---|---|---|
+| GOAL | — | Lean Translate core lets Journal keep translatable fields and boots in proof host | 01-04 | COVERED | Schema, API, admin, ML fields, host, tests |
+| REQ | — | No mapped requirement IDs | — | COVERED | Visible spec-less fallback; D-IDs are used |
+| CONTEXT | D-01..D-04 | Frozen/read-only PHP SHA | 01,04 | COVERED | Pin asserted; PHP tree unchanged |
+| CONTEXT | D-05 | Locale/admin/behavior lean scope; deferred surfaces absent | 02,04 | COVERED | Negative scope checks in gate |
+| CONTEXT | D-06 | markdown/mltext/mlmarkdown compose | 03,04 | COVERED | Go + SPA + generated artifacts |
+| CONTEXT | D-07 | full request locale resolution | 01,04 | COVERED | Ordered and concurrent tests |
+| CONTEXT | D-08 | en/pl only | 01,04 | COVERED | Seed/absence tests |
+| CONTEXT | D-09..D-12 | explicit Translatable APIs/storage/fallback | 02-04 | COVERED | Fixture and full matrix |
+| CONTEXT | D-13..D-17 | proof host, remotes, submodules, boot/proof | 01,03,04 | COVERED | Host smoke and layout checks |
+| RESEARCH | — | Exact four final tables/columns/indexes and migrations up/down | 01,04 | COVERED | Real Postgres |
+| RESEARCH | — | Permission, default-locale guards, phrasebook separation | 02,04 | COVERED | Admin suite |
+| RESEARCH | — | Nested ML map before projection, safe markdown | 03,04 | COVERED | Security tests |
+| RESEARCH | — | README/docs/OpenAPI/TS/dist same change | 03,04 | COVERED | Consistency gate |
+| RESEARCH | — | No external SaaS API integration | 01-04 | COVERED | No COVERAGE matrix or fabricated API tests |
+
+Excluded by explicit scope: Messages catalogue/admin, CMS locale components, locale picker/hreflang/banner, AI/theme commands, message import/export, extra locale seeds, PHP edits, and Phase 15 Journal implementation.
+
+
+
+
+ Task 1: Prove migration → activation → resolver → Locales admin → ML save → WithLocale read end to end
+ ../sm-translate-plugin/updates/postgres_test.go, ../sm-translate-plugin/integration_test.go, ../sm-translate-plugin/admin_harness_test.go, ../sm-grzybyfunkcjonalne-app/boot_test.go
+ ../fonoteka.go/plugins/golem15/user/updates/postgres_test.go, ../fonoteka.go/plugins/golem15/user/admin_harness_test.go, ../sm-translate-plugin/plugin.go, ../sm-translate-plugin/classes/translator.go, ../sm-translate-plugin/classes/translatable.go, ../sm-translate-plugin/controllers/locales.go, modules/cabana/ml_smoke_test.go, ../sm-grzybyfunkcjonalne-app/boot_test.go, .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md
+ Build the final integration harness on testcontainers Postgres, copying the proven user-plugin fail-closed TestMain/dedicated database pattern. Docker unavailability is a failure for full runs; only an explicit `-short` invocation may skip integration.
+
+`TestTranslateEndToEnd` must migrate user and translate plugin sets in dependency order, activate the real user and translate plugins plus a test-only neutral fixture controller/model, assemble surf/cabana, and create backend principals with and without `golem15.translate.manage_locales`. Assert unauthorized Locales access is 403 and authorized schema/list sees en then pl. Send one real fixture create/update body with mltext title and mlmarkdown body maps for en/pl. Assert host columns contain English, only the Polish non-default attribute row exists, safe markdown source round-trips, `WithLocale(...,"pl")` reads Polish, a missing Polish field falls back to English, and indexed Polish slug lookup finds only the fixture.
+
+Exercise a request with `/pl/...` and conflicting preferred/session/header candidates to prove URL precedence reaches `towel.Locale(ctx) == "pl"`. Keep all fixture plugin/model registration process-local to the test.
+
+Expand host `TestBootUserTranslate` to prove both actual gitlink plugins activate, migrations are discoverable, and the Locales controller/permission are registered. It need not duplicate the fixture model.
+
+ go -C ../sm-translate-plugin test ./... -count=1 -v -run '^(TestTranslateEndToEnd)$' && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslate)$'
+ Non-zero exit; either run prints "--- FAIL", "--- SKIP", "no tests to run", container startup failure treated as skip, or lacks its named "--- PASS" line.
+
+
+ - Integration uses real Postgres and actual gormigrate/party/surf/cabana production paths.
+ - Unauthorized Locales is 403; authorized list includes only seeded en/pl in order.
+ - One nested admin save persists English on the host and Polish in attributes/indexes.
+ - WithLocale Polish read, default fallback, and indexed lookup all pass.
+ - URL prefix wins over all conflicting candidates and locale is context-only.
+ - Fixture registration cannot appear in the production plugin list or host binary.
+
+ The entire Phase 14.2.1 user-visible path is proven through production wiring on real Postgres before horizontal test expansion.
+
+
+
+ Task 2: Complete migration, Translator, Translatable, admin, ML, markdown, and SPA test matrices
+ ../sm-translate-plugin/updates/migrations_test.go, ../sm-translate-plugin/classes/translator_test.go, ../sm-translate-plugin/classes/translatable_test.go, ../sm-translate-plugin/locales_admin_test.go, modules/surf/locale_resolver_test.go, modules/cabana/ml_test.go, modules/cabana/markdown_test.go, modules/cabana/openapi_conformance_test.go, admin/tests/form/MLFields.test.ts, admin/tests/form/MarkdownField.test.ts
+ /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/tests/unit/behaviors/TranslatableModelTest.php, ../sm-translate-plugin/integration_test.go, ../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go, ../sm-translate-plugin/classes/translator.go, ../sm-translate-plugin/classes/translatable.go, modules/surf/locale_from_principal_test.go, modules/cabana/form_schema_test.go, modules/cabana/field_permission.go, admin/tests/form/DatepickerField.test.ts, admin/tests/form/registry.test.ts
+ Complete the decision and security matrix without adding unrelated PHP-suite surfaces.
+
+Migration tests: assert every final table, column type/default, PHP-indexed column, empty Messages row count, no rainlab/provisional tables, idempotent seed, en/pl exact flags/order/names, no de, and rollback removes all four tables in reverse-safe order. Re-migrate after rollback.
+
+Translator tests: table-drive URL prefix precedence and stripping; preferred locale; remembered locale; absent/present manual flag behavior; weighted Accept-Language reduced only to enabled codes; default fallback; invalid URL/session/header ignored; API resolver preferred → header → default without persistence; plugin-absent surf retains old behavior. Run parallel requests with conflicting locales under `-race` and assert no cross-request leak.
+
+Translatable tests: default/non-default storage, D-11 fallback, explicit empty translation, invalid locale and undeclared field no-write, sibling JSON field preservation, indexed upsert/update and morph/model isolation, WithLocale context isolation, transaction rollback. Admin tests: exact permission 403/allowed, is_default/sort_order mass-assignment rejection, delete/unset/disabled-default guards, no manage_messages surface, phrasebook keys in en/pl.
+
+Cabana tests: three types compile and unknown `mlunknown` fails; non-ML nested values remain blocked; ML values lift before projection; malformed/extra locale keys fail; writer failure rolls back host write; writer is not invoked before permission/query checks. Markdown tests include script, iframe, event attributes, javascript/vbscript/data schemes. SPA tests cover selector synchronization, per-locale editing, copy, complete nested payload, and markdown composition without raw-HTML sinks. Extend OpenAPI conformance for all types.
+
+ go -C ../sm-translate-plugin test ./... -count=1 -race && go test ./modules/surf ./modules/cabana -count=1 -race && npm --prefix admin test -- --run admin/tests/form/registry.test.ts admin/tests/form/MLFields.test.ts admin/tests/form/MarkdownField.test.ts
+ Non-zero exit; Go race detector reports a race; any package/test reports FAIL; integration tests unexpectedly SKIP in the plugin run; or Vitest reports no tests or failures.
+
+
+ - Every D-01..D-17 behavior assigned to code has at least one named assertion or gate check.
+ - Migrate, rollback, and re-migrate are proven against real Postgres.
+ - Parallel locale tests pass under `-race` with no shared current-locale state.
+ - High threats T-14.2.1-01/02/04/05/06/07/09/10/11/12 have concrete fail-when-broken tests.
+ - No tests require Messages admin, CMS components, AI/theme commands, import/export, or extra locale seeds.
+
+ All production branches introduced by Plans 01-03 have focused unit or integration evidence, including race, rollback, authorization, mass-assignment, and XSS cases.
+
+
+
+ Task 3: Build the fail-closed phase gate, security review, and validation sign-off
+ scripts/check-phase14.2.1.sh, .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md, .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md
+ scripts/check-phase14.sh, scripts/check-phase12.2.sh, .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md, .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Security Domain and Validation Architecture), .planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md (T-14.2.1-01..04), .planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md (T-14.2.1-05..08), .planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md (T-14.2.1-09..13)
+ Create `scripts/check-phase14.2.1.sh` with explicit stages: frozen PHP SHA and no PHP diff; tracked-source/module/layout checks; plugin vet/full tests/race; framework cabana+surf vet/tests/race; docs tree and docs build check; admin typecheck/tests/build plus generated OpenAPI/schema/dist cleanliness; proof-host vet/test/build; forbidden-scope scan; security evidence. Use `go -C ` exactly for sibling repositories. Full mode must run Postgres integration and fail if Docker is unavailable; do not treat `-short` as final evidence. Detect zero-test filters by requiring named PASS lines where a filter is used.
+
+Run the requested security-review lane over the local Phase 14.2.1 changes. Produce `14.2.1-SECURITY-REVIEW.md` at ASVS L1, preserving unique threat IDs T-14.2.1-01 through T-14.2.1-18. For every high threat, cite the exact source control and executed named test; status must be mitigated or the phase gate remains red. Review locale injection, manage_locales privilege, nested ML JSON, stored XSS, mass assignment, process-wide leakage, and submodule provenance. Do not fabricate an external-API matrix: state `No external API integration: this phase ports a compiled plugin and local framework/host contracts only.`
+
+Update VALIDATION frontmatter to validated/nyquist compliant/wave 0 complete only after all mapped commands pass. Replace pending rows with exact test names and threat references, record the proof-host/manual Locales SPA check as end-of-phase UAT, and run the phase gate once in full.
+
+ bash scripts/check-phase14.2.1.sh --all
+ Non-zero exit; any stage is absent/skipped; output contains FAIL, a Go race, no-tests-to-run, unexpected SKIP, stale generated artifacts, forbidden deferred surface, unmitigated high threat, or lacks the final `Phase 14.2.1 gate passed` line.
+
+
+ - Gate uses `go -C ../sm-translate-plugin` and `go -C ../sm-grzybyfunkcjonalne-app`; it does not invent a nested application directory.
+ - Plugin, cabana, surf, admin, docs, generated artifacts, and proof host all have explicit fail-closed stages.
+ - Security review contains every T-14.2.1-NN exactly once and blocks on all high findings.
+ - Validation rows name existing tests/commands and frontmatter is marked validated/nyquist compliant only after green execution.
+ - Gate confirms no Messages admin/manage_messages, CMS locale components, AI/theme commands, import/export, de seed, runtime plugin loading, AutoMigrate, or PHP modifications.
+
+ The full three-repository phase gate is green, all high threats are mitigated with executed evidence, and validation is signed off.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Test/gate → production claims | A no-op, skipped container, or stale generated artifact must not pass |
+| Concurrent requests → context locale | Conflicting request locales must stay isolated |
+| Security review → phase completion | High findings block completion |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-14.2.1-14 | Repudiation | phase gate | high | mitigate | Require named PASS/final marker; reject no-tests and unexpected skips |
+| T-14.2.1-15 | Information Disclosure | concurrent Translator | high | mitigate | Race-enabled conflicting-locale test asserts context isolation |
+| T-14.2.1-16 | Tampering | migration rollback | medium | mitigate | Up/down/re-up on dedicated Postgres with exact schema assertions |
+| T-14.2.1-17 | Tampering | generated admin artifacts | medium | mitigate | Regenerate and require clean OpenAPI/TS/dist diff after build |
+| T-14.2.1-18 | Elevation of Privilege | test fixture | high | mitigate | Fixture plugin is process-local/test-only and absent from generated production imports |
+| T-14.2.1-SC | Tampering | package installs | high | mitigate | No dependency installation; existing exact pins and lockfile only |
+
+ASVS L1: phase completion is blocked on every high finding.
+
+
+
+`bash scripts/check-phase14.2.1.sh --all` is the authoritative final command and must end with `Phase 14.2.1 gate passed`.
+
+
+With the executor-started proof host and admin SPA, sign in as an administrator holding `golem15.translate.manage_locales`; open Locales, confirm English and Polski appear in order, edit the permitted name/enabled fields, and verify a user without the permission cannot open the controller.
+
+
+
+
+- Full unit, integration, race, migration rollback, SPA, docs, generated-artifact, and host gates pass.
+- Every locked D-01..D-17 decision is covered.
+- Every high STRIDE threat is mitigated with source and named-test evidence.
+- No deferred surface or new external dependency entered the phase.
+
+
+
diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
new file mode 100644
index 0000000..af6850a
--- /dev/null
+++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
@@ -0,0 +1,629 @@
+# Phase 14.2.1: Translate plugin - Pattern Map
+
+**Mapped:** 2026-10-06
+**Files analyzed:** 42 new or modified files (plugin + framework + host + tests)
+**Analogs found:** 40 / 42
+
+Path roots:
+
+- `FW/` = `/media/nvme/dev/golem15/summercms.io/summercms/summercms.go` (working directory; relative paths below are from here unless prefixed).
+- `USR/` = `../fonoteka.go/plugins/golem15/user/` — the `sm-user-plugin` submodule. This is the plugin-mount analog RESEARCH named. `/media/nvme/dev/golem15/fonoteka.go/plugins/golem15/user/` does **not** exist.
+- `BM/` = `../sm-bm-app/` — proof-host analog (`summer.yaml` + `go.work` + `.gitmodules` + `replace`). Copy this layout, not `fonoteka.go`.
+- `PHP/` = `/media/nvme/dev/golem15/fonoteka/plugins/golem15/translate` at SHA `725d547ec839f02b5fdc0f0a6faaed601a414d50` (verified `git rev-parse HEAD` this session; 2026-08-26). Read-only contract. Do not edit.
+- `TR/` = `../sm-translate-plugin/` — **does not exist yet** (D-16). Plan 01 creates it.
+- `APP/` = `../sm-grzybyfunkcjonalne-app/` — **does not exist yet** (D-14). Plan 01 clones the empty remote.
+
+All analog paths below are git-tracked (`git ls-files` in `summercms.go`, inside the user submodule, inside `sm-bm-app`, and inside the PHP plugin). No gitignored mirror is named. `modules/boardwalk/dist`, `admin/openapi/admin.json` and `admin/src/api/schema.d.ts` are generated outputs: regenerate them, never hand-edit.
+
+**Table names (locked by RESEARCH against the frozen PHP models):** `winter_translate_locales`, `winter_translate_attributes`, `winter_translate_indexes`, `winter_translate_messages`. CONTEXT D-10's `golem15_translate_*` placeholder is wrong. Do not invent `golem15_translate_*`.
+
+Suggested plan split from RESEARCH (present at the plan-count checkpoint; unit tests last): **14.2.1-01** plugin repo + squashed schema + Locale + Translator + surf Resolver seam; **14.2.1-02** Translatable API + Locales admin + fixture; **14.2.1-03** cabana `markdown`/`mltext`/`mlmarkdown` + SPA + proof host boot; **14.2.1-04** unit/integration tests last.
+
+## File Classification
+
+### New plugin (`TR/` = `sm-translate-plugin`)
+
+| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
+|-------------------|------|-----------|----------------|---------------|
+| `TR/go.mod` | config | — | `USR/go.mod` | exact |
+| `TR/plugin.go` | provider | request-response | `USR/plugin.go` | exact |
+| `TR/README.md` | docs | — | `USR/README.md` | exact |
+| `TR/admin.go` | provider | — | `USR/admin.go` | exact |
+| `TR/admin_permissions.go` | config | — | `USR/admin_permissions.go` | exact |
+| `TR/admin_navigation.go` | config | — | `USR/admin_navigation.go` | exact |
+| `TR/config/config.yaml` | config | — | `USR/config/config.yaml` + `PHP/config/config.php` | exact |
+| `TR/lang/en/lang.yaml`, `TR/lang/pl/lang.yaml` | config | transform | `USR/lang/{en,pl}/lang.yaml` + `PHP/lang/en/lang.php` keys `plugin.*` / `locale.*` | exact |
+| `TR/models/registry.go` | utility | — | `USR/models/registry.go` | exact |
+| `TR/models/locale.go` | model | CRUD | `USR/models/user_group.go` (Fillable/Rules/TableName) + `PHP/models/Locale.php` (guards) | exact |
+| `TR/models/attribute.go` | model | CRUD | `USR/models/api_token.go` shape + `PHP/models/Attribute.php` fillable | exact |
+| `TR/updates/registry.go` | utility | — | `USR/updates/registry.go` | exact |
+| `TR/updates/202610060001_create_winter_translate_locales.go` | migration | CRUD | `USR/updates/00_base.go` + `USR/updates/202610020001_create_user_groups.go` (CREATE + seed style) | exact |
+| `TR/updates/202610060002_create_winter_translate_attributes.go` | migration | CRUD | `USR/updates/00_base.go` | exact |
+| `TR/updates/202610060003_create_winter_translate_indexes.go` | migration | CRUD | `USR/updates/00_base.go` | exact |
+| `TR/updates/202610060004_create_winter_translate_messages.go` | migration | CRUD | `USR/updates/00_base.go` (DDL only; no Message admin) | exact |
+| `TR/updates/202610060005_seed_en_pl_locales.go` | migration | CRUD | `USR/updates/202610020001_create_user_groups.go` INSERT + `PHP/updates/v1.3.1/seed_all_tables.php` and `v2.4.0/seed_additional_locales.php` | exact |
+| `TR/classes/translator.go` | service | request-response | `PHP/classes/Translator.php` + `LocaleMiddleware.php` + `ApiLocaleMiddleware.php` (contract) and `USR/plugin.go` Boot `app.Publish` | role-match |
+| `TR/classes/translatable.go` | service | CRUD | `PHP/behaviors/TranslatableModel.php` + `classes/TranslatableBehavior.php` (no Go translatable analog) | partial |
+| `TR/controllers/admin_registry.go` | config | — | `USR/controllers/admin_registry.go` | exact |
+| `TR/controllers/locales.go` | controller | CRUD | `USR/controllers/usergroups_admin_controller.go` | exact |
+| `TR/controllers/locales/config_list.yaml` | config | file-I/O | `USR/controllers/usergroups/config_list.yaml` + `PHP/controllers/locales/config_list.yaml` | exact |
+| `TR/controllers/locales/config_form.yaml` | config | file-I/O | `USR/controllers/usergroups/config_form.yaml` + `PHP/controllers/locales/config_form.yaml` | exact |
+| `TR/models/locale/fields.yaml` | config | file-I/O | `USR/models/usergroup/fields.yaml` + `PHP/models/locale/fields.yaml` | exact |
+| `TR/models/locale/columns.yaml` | config | file-I/O | `USR/models/usergroup/columns.yaml` + `PHP/models/locale/columns.yaml` | exact |
+
+### Framework (`FW/`)
+
+| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
+|-------------------|------|-----------|----------------|---------------|
+| `modules/surf/router.go` (`locale()` seam) | middleware | request-response | same file `locale` 707-710 and `wrap` 439; `backpack.Lookup` from `USR/plugin.go` Boot | exact |
+| `modules/surf/locale_resolver.go` (new interface, if extracted) | middleware | request-response | `modules/surf/locale_from_principal.go` + `modules/pact/capabilities.go` `HasHouseMiddleware` (do **not** use house-MW to replace `locale()`) | role-match |
+| `modules/cabana/form_schema.go` | config compiler | transform | same file `formFieldTypes` 24-29, `compileFieldNode` 454-478 | exact |
+| `modules/cabana/crud.go` | service | CRUD | same file `ProjectWritableFields` 154-176, `scalarFormField` 1203-1209, `save` lifts 575-590 | exact |
+| `modules/cabana/field_ml.go` (new: `markdown` / `mltext` / `mlmarkdown`) | service | transform + CRUD | `modules/cabana/field_permission.go` (`liftPermissionValues` nested object) + `field_date.go` (`compileDatepickerKeys`) | role-match |
+| `modules/cabana/README.md` | docs | — | same file Features list | exact |
+| `docs/backend/forms.md` | docs | — | same file Field types table 86-101 | exact |
+| `docs/backend/admin-controllers.md` | docs | — | same file Compilation at boot 159-161 (only if activation rules change) | exact |
+| `admin/src/components/form/registry.ts` | config (registry) | transform | same file 49-63 | exact |
+| `admin/src/components/form/fields/MarkdownField.vue` | component | request-response | `admin/src/components/form/fields/TextareaField.vue` | exact |
+| `admin/src/components/form/fields/MLTextField.vue` | component | request-response | `TextField.vue` (editor) + `PHP/formwidgets/mltext/partials/_mltext.htm` (chrome) | role-match |
+| `admin/src/components/form/fields/MLMarkdownField.vue` | component | request-response | MarkdownField + ML chrome (compose; do not duplicate markdown) | role-match |
+| `admin/src/components/form/formState.ts` | utility | transform | same file `editablePayload` 50-69 (permissioneditor nested object already sent) | exact |
+| `admin/openapi/admin.json`, `admin/src/api/schema.d.ts`, `modules/boardwalk/dist/` | generated | — | regenerate; never analog-copy | — |
+
+### Proof host (`APP/` = `sm-grzybyfunkcjonalne-app`)
+
+| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
+|-------------------|------|-----------|----------------|---------------|
+| `APP/go.mod` | config | — | `BM/go.mod` | exact |
+| `APP/go.work` | config | — | `BM/go.work` | exact |
+| `APP/summer.yaml` | config | — | `BM/summer.yaml` | exact |
+| `APP/.gitmodules` | config | — | `BM/.gitmodules` | exact |
+| `APP/main.go`, `APP/plugins.gen.go` | route | — | `BM/main.go`, `BM/plugins.gen.go` (`summer build` emits these; do not hand-author after first boot) | exact |
+
+### Tests (plan 04 last)
+
+| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
+|-------------------|------|-----------|----------------|---------------|
+| `TR/admin_harness_test.go` + Locales admin tests | test | request-response | `USR/admin_harness_test.go` `newAdminEnv` | exact |
+| `TR/updates/postgres_test.go` | test | CRUD | `USR/updates/postgres_test.go` TestMain + testcontainers | exact |
+| `TR/classes/*_test.go` Translator + Translatable | test | request-response / CRUD | PHP `tests/unit/behaviors/TranslatableModelTest.php` landmines (lean subset) | partial |
+| `modules/cabana/*_test.go` ML compile/save | test | transform | `modules/cabana/form_schema_test.go` `TestFormSchemaRejects` | exact |
+| `modules/surf/*_test.go` Resolver present vs absent | test | request-response | `modules/surf/locale_from_principal_test.go` | exact |
+| `admin/tests/form/registry.test.ts` + ML field tests | test | — | `admin/tests/form/registry.test.ts`, `admin/tests/form/DatepickerField.test.ts` | exact |
+
+## Pattern Assignments
+
+### `TR/go.mod` (config) — plan 01
+
+**Analog:** `USR/go.mod` lines 1-14, 120
+
+```
+module git.golem15.com/golem15/sm-user-plugin
+
+go 1.27.0
+
+require (
+ git.golem15.com/golem15/summercms v0.0.0
+ github.com/go-gormigrate/gormigrate/v2 v2.1.7
+ ...
+ gorm.io/gorm v1.31.2
+)
+
+replace git.golem15.com/golem15/summercms => ../../../../summercms.go
+```
+
+Copy: module `git.golem15.com/golem15/sm-translate-plugin`, Go 1.27.0, require GORM + gormigrate + summercms, **identical** `replace … => ../../../../summercms.go` when mounted at `plugins/golem15/translate`. Do not add goldmark here unless the plugin itself parses markdown; goldmark stays a framework dep.
+
+Decision note: `.planning/notes/core-plugins-own-repos.md` lines 12-15 — module path equals repo path; package `translate`; plugin ID `golem15.translate`. README never names a consuming app.
+
+---
+
+### `TR/plugin.go` (provider) — plan 01
+
+**Analog:** `USR/plugin.go` lines 28-64, 178-180, 207-209, 236-238
+
+```go
+var (
+ _ party.Plugin = (*Plugin)(nil)
+ _ pact.HasConfig = (*Plugin)(nil)
+ _ pact.HasMigrations = (*Plugin)(nil)
+ _ pact.HasModels = (*Plugin)(nil)
+ _ pact.HasLang = (*Plugin)(nil)
+)
+
+//go:embed config
+var configFS embed.FS
+//go:embed lang
+var langFS embed.FS
+
+func (p *Plugin) ID() string { return "golem15.user" }
+func (p *Plugin) Requires() []string { return nil }
+func (p *Plugin) Register(app *backpack.App) error { p.app = app; return nil }
+func (p *Plugin) ConfigFS() fs.FS { return configFS }
+func (p *Plugin) LangFS() fs.FS { return langFS }
+func (p *Plugin) Migrations() []*gormigrate.Migration { return updates.All() }
+func (p *Plugin) Models() []any { return models.All() }
+
+func init() { party.Register(&Plugin{}) }
+```
+
+Copy: `ID() "golem15.translate"`, `Requires()` empty (Translator works without user; locale-from-user is optional). **Do not** copy JWT/bouncer/mail/commands/middleware from user. **Do** `app.Publish` a Resolver in `Boot` (see Translator assignment). Admin capability assertions live in `admin.go`, not here.
+
+PHP contract (do **not** port): `PHP/Plugin.php` `registerComponents`, `manage_messages`, `registerFormWidgets`, console commands, CMS extend — all deferred. Port only `manage_locales` permission (lines 71-75) and Locales as admin CRUD, not `HasSettings` (PHP `registerSettings` points at a list controller; cabana `HasSettings` is a singleton — RESEARCH §2).
+
+---
+
+### `TR/admin.go` + permissions + navigation (provider / config) — plan 02
+
+**Analog:** `USR/admin.go` lines 12-38, `USR/admin_permissions.go` 14-21, `USR/admin_navigation.go` 11-19
+
+```go
+//go:embed controllers/usergroups/config_list.yaml controllers/usergroups/config_form.yaml models/usergroup/fields.yaml models/usergroup/columns.yaml
+var adminFS embed.FS
+
+func (p *Plugin) AdminFS() fs.FS { return adminFS }
+func (p *Plugin) AdminControllers() []pact.AdminController {
+ return controllers.AdminControllers(func() *backpack.App { return p.app })
+}
+var (
+ _ pact.AdminAssets = (*Plugin)(nil)
+ _ pact.HasAdminControllers = (*Plugin)(nil)
+ _ pact.HasPermissions = (*Plugin)(nil)
+ _ pact.HasNavigation = (*Plugin)(nil)
+)
+```
+
+Permissions analog (`USR/admin_permissions.go`):
+
+```go
+{
+ Code: "golem15.users.access_users",
+ Tab: userPermissionTab,
+ Label: "golem15.user::lang.plugin.access_users",
+ Roles: []string{"developer"},
+},
+```
+
+Translate: `Code: "golem15.translate.manage_locales"`, tab `golem15.translate::lang.plugin.tab`, label `golem15.translate::lang.plugin.manage_locales`, `Roles: []string{"developer"}`. **Do not** register `golem15.translate.manage_messages` this phase.
+
+Navigation analog: main item `Code: "translate"`, `Icon` lucide (`languages` or similar; PHP was `icon-language`), `Permissions: []string{"golem15.translate.manage_locales"}`, `Controller: "golem15.translate.locales"`, `Order` ~550 (PHP settings order 550). Locales is CRUD list+form, not a settings singleton.
+
+---
+
+### `TR/models/locale.go` (model, CRUD) — plan 01/02
+
+**Analog (Go struct + Fillable + Rules):** `USR/models/user_group.go` lines 9-50
+
+```go
+type UserGroup struct {
+ ID uint `gorm:"column:id;primaryKey"`
+ Name string `gorm:"column:name"`
+ Code *string `gorm:"column:code"`
+ // ...
+}
+func (UserGroup) TableName() string { return "user_groups" }
+func (UserGroup) Fillable() []string { return []string{"name", "code", "description"} }
+func (UserGroup) Rules() map[string]string {
+ return map[string]string{"name": "required|between:3,64", "code": "required|unique:user_groups"}
+}
+func init() { Register(UserGroup{}) }
+```
+
+**Contract (PHP):** `PHP/models/Locale.php` lines 24-43, 77-109
+
+```php
+public $table = 'winter_translate_locales';
+public $rules = ['code' => 'required', 'name' => 'required'];
+public $fillable = ['code', 'name', 'is_enabled'];
+public $timestamps = false;
+// beforeDelete: cannot delete default
+// beforeUpdate: cannot unset default; makeDefault() writes is_default
+// makeDefault: cannot make a disabled locale default
+```
+
+Copy: `TableName() "winter_translate_locales"`, no `CreatedAt`/`UpdatedAt`, Fillable **only** `code`, `name`, `is_enabled`. `is_default` and `sort_order` are **not** fillable (PHP). Rules: `code` required, `name` required. Port delete/unset/disabled-default as `FormBeforeDelete` / `FormBeforeUpdate` returning `&cabana.ValidationError{Details: ...}` (422) — analog `USR/controllers/usergroups_admin_controller.go` 162-175.
+
+`isValid` = code in enabled list (`PHP/models/Locale.php` 228-232). Default locale: `is_default` true row; seed `en` as default.
+
+---
+
+### `TR/models/attribute.go` (model, CRUD) — plan 02
+
+**Contract:** `PHP/models/Attribute.php` lines 15-34 — table `winter_translate_attributes`, fillable `locale`, `model_type`, `model_id`, `attribute_data`, `$guarded = ['*']`.
+
+**Go analog:** `USR/models/user_group.go` TableName + Register. No public Attribute controller. Writes only through Translatable helpers (RESEARCH T-SEC). Optional: omit a Message model entirely and only DDL `winter_translate_messages` (RESEARCH §9). If Attribute exists, Fillable matches PHP; never expose an admin controller.
+
+---
+
+### `TR/updates/*` (migration, CRUD) — plan 01
+
+**Analog (CREATE + Register):** `USR/updates/00_base.go` lines 1-33
+
+```go
+var migrations = []*gormigrate.Migration{
+ {
+ ID: "202609170001_create_users",
+ Migrate: func(tx *gorm.DB) error {
+ return tx.Exec(`
+CREATE TABLE users (
+ id SERIAL PRIMARY KEY,
+ ...
+)`).Error
+ },
+ Rollback: func(tx *gorm.DB) error {
+ return tx.Exec(`DROP TABLE IF EXISTS users`).Error
+ },
+ },
+}
+func init() { Register(migrations...) }
+```
+
+**Analog (indexes + idempotent seed):** `USR/updates/202610020001_create_user_groups.go` lines 17-45 — `CREATE INDEX …`, `INSERT INTO … VALUES`.
+
+**Contract columns (squash to final names; do not emit `rainlab_translate_*`):**
+
+| Table | Columns (from PHP create + later updates) |
+|-------|-------------------------------------------|
+| `winter_translate_locales` | `id SERIAL PK`, `code TEXT` indexed, `name TEXT` indexed nullable, `is_default BOOLEAN DEFAULT FALSE`, `is_enabled BOOLEAN DEFAULT FALSE`, `sort_order INTEGER DEFAULT 0`. No timestamps. |
+| `winter_translate_attributes` | `id`, `locale` indexed, `model_id` indexed nullable, `model_type` indexed nullable, `attribute_data TEXT` nullable |
+| `winter_translate_indexes` | `id`, `locale` indexed, `model_id` indexed nullable, `model_type` indexed nullable, `item` indexed nullable, `value TEXT` nullable |
+| `winter_translate_messages` | `id`, `code` indexed nullable, `message_data TEXT` nullable, `found BOOLEAN DEFAULT TRUE`, `code_pre_2_1_0 TEXT` indexed nullable — create empty; no Messages admin |
+
+Suggested IDs (planner may adjust date prefix, not table names): `202610060001_create_winter_translate_locales` … `202610060005_seed_en_pl_locales`.
+
+**Seed contract:** `PHP/updates/v1.3.1/seed_all_tables.php` 16-22 (`en` / English / default / enabled) and `PHP/updates/v2.4.0/seed_additional_locales.php` 13-20 (`pl` / Polski / not default / enabled / `sort_order` 2). Set `en.sort_order = 1`. **Do not seed `de`.** Idempotent: insert by `code` if missing (`$exists` check lines 33-36).
+
+---
+
+### `TR/classes/translator.go` (service, request-response) — plan 01
+
+**Contract order (do not reduce):** `PHP/classes/LocaleMiddleware.php` 24-41
+
+1. URL prefix (`Translator::loadLocaleFromRequest` — first path segment, only if `Locale::isValid`) — `PHP/classes/Translator.php` 129-138
+2. Else authenticated user's `preferred_locale` if valid — middleware 54-83
+3. Else session `golem15.translate.locale` — Translator 204-213
+4. Else Accept-Language **only if** `browserDetection.enabled` **and** cookie `locale_manually_set` is absent — middleware 33-36, 96-100, 214-228. Parse q-values; take first two letters; allow-list against enabled codes (126-132). **Do not** pass the raw header into `towel.WithLocale`.
+5. Else default locale.
+
+API chain (ship Translator so Phase 15 can call it): `PHP/classes/ApiLocaleMiddleware.php` 40-59 — user preferred → Accept-Language → default; `setLocale($locale, false)` no session.
+
+Keys (`PHP/classes/Translator.php` 23-25, `config/config.php` 77-86):
+
+- session/cookie locale: `golem15.translate.locale`
+- configured flag: `golem15.translate.configured`
+- manual-selection cookie **flag only**: `locale_manually_set` = `'1'`, expiry 525600 minutes. Does **not** contain a locale code. URL-prefix visit queues it (`PHP/routes.php` 29-35).
+
+`setLocale` requires `Locale::isValid`; invalid codes return false and are never persisted (`Translator.php` 58-72).
+
+**Go seam analog:** `USR/plugin.go` Boot Publish (lines 77-95) + `FW/modules/backpack/app.go` 59-73
+
+```go
+func (a *App) Publish[T any](value T) error { return a.Services.Publish(value) }
+func (a *App) Lookup[T any]() (T, bool) { ... }
+```
+
+User plugin publishes `*bouncer.Registry` / `bouncer.BlacklistStore`. Translate Boot publishes a `surf.LocaleResolver` (or equivalent interface **in the framework**, because `surf` must not import the plugin). Methods take `ctx` and `*http.Request`; they must not store the active locale on a process-wide struct (KERN-07). Write the resolved code with `towel.WithLocale`.
+
+**Do not** implement PHP Singleton `Translator::instance()`. **Do not** use `pact.HasHouseMiddleware` to replace house `locale()`: house-MW is a named middleware table (`FW/modules/pact/capabilities.go` 47-59); `locale()` is hardcoded in `wrap` after named MW (`router.go` 439).
+
+Config keys under plugin namespace (`PHP/config/config.php`): `forceDefaultLocale`, `prefixDefaultLocale` (default true), `disableLocalePrefixRoutes` (default false), `browserDetection.enabled` (default true), `browserDetection.manualSelectionCookie`, `browserDetection.manualSelectionExpiry`. Port into `TR/config/config.yaml` via `HasConfig` like `USR/config/config.yaml`.
+
+---
+
+### `modules/surf/router.go` locale seam (middleware) — plan 01
+
+**Analog (current, to wrap):** `FW/modules/surf/router.go` 439 and 707-710
+
+```go
+h = locale(h)
+
+func locale(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ next.ServeHTTP(w, r.WithContext(towel.WithLocale(r.Context(), r.Header.Get("Accept-Language"))))
+ })
+}
+```
+
+`wrap` currently has `*Router` but **no** `*backpack.App`. `BuildRouter(app, plugins)` has `app` (459-464). Prescription: store `app` on `Router` during `BuildRouter`, then `locale()` Lookup Resolver; if published, call it and write a **validated** code; else keep today's Accept-Language behavior so `fonoteka.go` (no translate plugin) does not change.
+
+**Existing overlay (keep when no Resolver):** `FW/modules/surf/locale_from_principal.go` 10-18 — `PreferredLocale` on the principal. With Resolver, user preferred is step 2 of the PHP chain (after URL prefix), not a post-hoc overlay of the raw header.
+
+**Context bag:** `FW/modules/towel/context.go` 55-62
+
+```go
+func WithLocale(ctx context.Context, locale string) context.Context {
+ return withValue(ctx, localeKey{}, locale)
+}
+func Locale(ctx context.Context) (string, bool) {
+ return stringValue(ctx, localeKey{})
+}
+```
+
+Phrasebook UI locale and model content locale usually match after Translator runs; Translatable helpers still take an **explicit** locale argument (admin SPA UI can stay `en` while content is `pl`).
+
+**Cookie attributes analog (not JWT):** `FW/modules/cabana/auth.go` 243-252 — `HttpOnly`, `Secure`, `SameSite`. Locale cookies are not admin session tokens; re-validate enabled codes every request. RESEARCH A3: signed cookie `golem15.translate.locale` is the Go stand-in for Laravel session; do not invent a second JWT library.
+
+---
+
+### `TR/classes/translatable.go` (service, CRUD) — plan 02
+
+**No Go translatable analog.** Copy PHP storage, not Eloquent magic.
+
+**Contract:** `PHP/classes/TranslatableBehavior.php` 141-216 and `PHP/behaviors/TranslatableModel.php` 89-108, 224-294, 345-348
+
+- Default locale values live on the **host model's own columns**. `isTranslatable` is false when context equals default.
+- Other locales: JSON object in `winter_translate_attributes.attribute_data`, one row per `(locale, model_id, model_type)`.
+- Missing key + fallback on (default): return default-locale column.
+- Indexed attributes (`['slug', 'index' => true]`) also write `winter_translate_indexes` (`item` = attribute, `value` = translated string).
+- `model_type` = `getMorphClass()` → Go `MorphName() string`.
+- `scopeTransWhere`: look up index table; if no rows, `where` on the host column (104-108).
+- Do **not** port Redis `translation:%s:%s:%s` cache this phase.
+
+**MorphName analog:** `USR/models/user.go` 59-60 and `FW/modules/lagoon/attach/example_test.go` 21
+
+```go
+func (User) MorphName() string { return `Golem15\User\Models\User` }
+func (Post) MorphName() string { return `Acme\Blog\Models\Post` }
+```
+
+Fixture models may use a Go type string. Document that Phase 15 Journal import must pin PHP class strings (`Golem15\Journal\Models\Post`).
+
+Minimum exported API (RESEARCH §3; identifiers prescribed there):
+
+```go
+type Translatable interface {
+ Translatable() []string
+ MorphName() string
+}
+func WithLocale(ctx context.Context, db *gorm.DB, locale string) *gorm.DB
+func Translated(...) (any, error)
+func SetTranslated(...) error
+```
+
+Plus `TranslatableIndexes() []string` (or options) for Journal slugs. Do not 1:1 every PHP method. Do not export deprecated `noFallbackLocale`.
+
+---
+
+### `TR/controllers/locales.go` (controller, CRUD) — plan 02
+
+**Analog:** `USR/controllers/usergroups_admin_controller.go` 41-53, 123-131, 162-175 + `USR/controllers/admin_registry.go` 12-18, 42-50
+
+```go
+func (usergroupsAdminController) ID() string { return "golem15.user.usergroups" }
+func (usergroupsAdminController) ModelName() string { return `Golem15\User\Models\UserGroup` }
+func (usergroupsAdminController) ConfigDir() string { return "controllers/usergroups" }
+func (usergroupsAdminController) RequiredPermissions() []string {
+ return []string{PermissionAccessGroups}
+}
+func (usergroupsAdminController) NewRecord() any { return &models.UserGroup{} }
+```
+
+Translate: `ID() "golem15.translate.locales"`, `ModelName() \`Golem15\Translate\Models\Locale\``, `ConfigDir() "controllers/locales"`, `RequiredPermissions() []string{"golem15.translate.manage_locales"}`, `NewRecord() &models.Locale{}`.
+
+PHP `Locales.php` 21: `$requiredPermissions = ['golem15.translate.manage_locales']`. Implement `pact.AdminPermissioned`. 403 without it.
+
+Hooks: `FormBeforeDelete` refuse default locale; `FormBeforeUpdate` refuse unsetting default / making disabled default — return `&cabana.ValidationError` (422) with phrase keys from `PHP/lang/en/lang.php` 27-29 (`unset_default`, `delete_default`, `disabled_default`). Analog Forbidden vs Validation: usergroups uses `ForbiddenError` for permission (403) and `ValidationError` for code format (422). Locale guards are validation, not 403.
+
+PHP ReorderController has **no cabana analog**. Keep `sort_order`, seed `en=1` `pl=2`, list `defaultSort` `sort_order` asc. Do not invent drag-reorder.
+
+**Error types:** `FW/modules/cabana/crud.go` 62-81 `ValidationError` / `ForbiddenError`.
+
+---
+
+### Admin YAML (config, file-I/O) — plan 02
+
+**Go analog:** `USR/controllers/usergroups/config_list.yaml` (recordUrl, recordsPerPage 20, toolbar create, search) and `config_form.yaml` (form path, modelClass, redirects).
+
+**PHP contract fields:** `PHP/models/locale/fields.yaml` — `name`, `code`, `is_enabled` checkbox, `is_default` checkbox. `PHP/models/locale/columns.yaml` — name/code searchable, is_default switch, is_enabled switch invisible, sort_order number invisible.
+
+Cabana list types (`FW/modules/cabana/list_schema.go` 22-24): `"text"`, `"datetime"`, `"switch"`, `"date"`, `"time"`. Map PHP `type: number` on invisible `sort_order` → omit type (text) or drop from visible columns.
+
+`config_list.yaml`: PHP `recordOnClick` is Winter AJAX; Go uses `recordUrl: golem15/translate/locales/update/:id` like usergroups. `title: golem15.translate::lang.locale.label_plural`. `defaultSort.column: sort_order`, `direction: asc`.
+
+Embed YAML file-by-file in `admin.go` (`//go:embed controllers/locales/... models/locale/...`), same as user — do not embed the whole `controllers/` tree (it will hold `.go` files).
+
+---
+
+### Phrasebook lang YAML (config) — plan 02
+
+**Analog:** `USR/lang/en/lang.yaml` `plugin:` block lines 4-10 and `USR/lang/pl/lang.yaml` same keys.
+
+**Contract keys to port (UI only):** `PHP/lang/en/lang.php` 4-41 `plugin.name/description/tab/manage_locales` and `locale.*` (label, label_plural, title, create/update titles, name, code, is_default, is_enabled, help, delete/unset/disabled_default, sort_order, hint). Do **not** load PHP `unsupported_lang/` or `messages.*` this phase.
+
+Do not conflate phrasebook files with Locale seed rows (D-08 is two Locale rows; two YAML trees for plugin UI).
+
+---
+
+### Cabana `markdown` / `mltext` / `mlmarkdown` — plan 03
+
+**Registry analog:** `FW/modules/cabana/form_schema.go` 24-47 and 465-478
+
+```go
+formFieldTypes = map[string]struct{}{
+ "text": {}, "textarea": {}, ... "datepicker": {}, "password": {}, "permissioneditor": {},
+}
+// unknown key → "unknown field %s"
+// unknown type → "unsupported type %s"
+```
+
+Add `"markdown"`, `"mltext"`, `"mlmarkdown"` to `formFieldTypes`. Prefer **no extra YAML keys** (reuse `label`, `comment`, `span`, `size`, `required`, `tab`, `context`). If a key is added, it must go in `formFieldKeys` or boot fails.
+
+**Compile analog:** `FW/modules/cabana/field_date.go` 42-52 `compileDatepickerKeys` — refuse type-specific keys on other types; call from `compileFieldNode` next to `compilePermissionKeys` / `compileDatepickerKeys` (form_schema.go 551-558).
+
+**Nested save analog:** `FW/modules/cabana/field_permission.go` 177-210 `liftPermissionValues` — read `body[name]` as `map[string]any` **before** scalar projection. Hook the lift from `CRUDService.save` (`crud.go` 575-590) the same way relations/virtual/permissions are lifted.
+
+**The landmine:** `ProjectWritableFields` (`crud.go` 154-176) drops `nestedValue` (maps/slices, 1224-1230). `scalarFormField` (1203-1209) is only `text/textarea/number/checkbox/switch/dropdown/datepicker` — `mltext`/`mlmarkdown`/`markdown` are skipped by `BindWritableFields` (201) unless treated as scalar **or** lifted like permissioneditor.
+
+**PHP save contract:** `PHP/traits/MLControl.php` 186-216 — POST all locales; `setAttributeTranslated` per locale; return value is the **default locale** entry only. `getLocaleValue` uses `setTranslatableUseFallback(false)` so empty translations stay empty in hidden fields (158-159). Host column = default locale; attribute rows = other locales only (do not duplicate default into `winter_translate_attributes`).
+
+**SPA registry analog:** `FW/admin/src/components/form/registry.ts` 49-63 — add `markdown`, `mltext`, `mlmarkdown`. `isRegistered` must stay true so `formState.editablePayload` (50-69) includes them. permissioneditor already sends a nested object whenever shown (60-63) — ML fields should send `Record` the same way.
+
+**Chrome contract:** `PHP/formwidgets/mltext/partials/_mltext.htm` — wrapper `data-default-locale`, locale selector, hidden inputs per locale, copy-from-locale optional. One switcher per ML field; switching one switches all (PHP Ctrl/Cmd-click). Visible editor shows the active locale.
+
+**Markdown primitive analog:** `FW/modules/postcard/templates.go` 24-29 `goldmark.New()` already in framework `go.mod` v1.8.6. Land shared `markdown` **this phase** (`mlmarkdown` = markdown editor + ML chrome). Safe mode: no `html.WithUnsafe`; reject leftover script/iframe (postcard `rawUnsafeTag`). Minimum this phase is edit+save of markdown source per locale, not WYSIWYG.
+
+**Docs analog:** `FW/docs/backend/forms.md` 86-101 — add the three types; **replace** the sentence that the markdown editor is not provided (line 101). Neutral names only (`acme`, `blog`). Same change: `modules/cabana/README.md` Features, OpenAPI, openapi-typescript, committed `boardwalk/dist/`. `go test ./cmd/summer -run TestDocsTree` and `summer docs:build --check`.
+
+**Fail-loud test analog:** `FW/modules/cabana/form_schema_test.go` `TestFormSchemaRejects` 184-197 (`unknown key` / `unknown type`). Add `type: mlunknown` fails boot.
+
+---
+
+### SPA field components — plan 03
+
+**Text analog:** `FW/admin/src/components/form/fields/TextField.vue` 1-26 — `FieldControlProps`, `update:modelValue`, `controlAttributes` / `controlClass`.
+
+**Textarea analog for markdown source:** `FW/admin/src/components/form/fields/TextareaField.vue` 1-30 — size → rows.
+
+**Value-field test analog:** `FW/admin/tests/form/DatepickerField.test.ts` mount + emit; `FW/admin/tests/form/registry.test.ts` `it.each` renderer map (31-42).
+
+No existing multilingual control. Compose: inner TextField/MarkdownField + locale switcher chrome. `modelValue` is `Record` (locale → text), not a scalar.
+
+---
+
+### Proof host (`APP/`) — plan 03 (clone in plan 01)
+
+**Analog:** `BM/go.mod` 1-20, `BM/go.work` 5-10, `BM/summer.yaml` 1-9, `BM/.gitmodules` 1-3, `BM/main.go` 22-38, `BM/plugins.gen.go` 1-16
+
+```
+module git.golem15.com/jakub/sm-bm-app
+replace git.golem15.com/golem15/summercms => ../summercms.go
+replace git.golem15.com/golem15/sm-user-plugin => ./plugins/golem15/user
+```
+
+```yaml
+plugins:
+ - id: golem15.user
+ module: git.golem15.com/golem15/sm-user-plugin
+```
+
+```
+[submodule "plugins/golem15/user"]
+ path = plugins/golem15/user
+ url = git@git.golem15.com:golem15/sm-user-plugin.git
+```
+
+```go
+plugins, err := party.Activate(app, PluginIDs)
+```
+
+Proof host: module `git.golem15.com/golem15/sm-grzybyfunkcjonalne-app` (or whatever the empty remote uses), plugins **only** `golem15.user` + `golem15.translate`, submodule paths `plugins/golem15/user` and `plugins/golem15/translate`, `go.work` use both, `replace` both to `./plugins/...`, framework replace `../summercms.go`. `main.go` / `plugins.gen.go` come from `summer build` after `summer.yaml` exists (`FW/examples/hello/plugins.gen.go` same stamp `// Code generated by summer build. DO NOT EDIT.`).
+
+D-17 fixture: RESEARCH A1 — prefer plugin integration test (`party.Activate([]{user, translate, in-process fixture})`) plus host smoke `migrate`/`serve` without a third production plugin. Do not block on a host demo model.
+
+Manual Wave 0: user creates `git@git.golem15.com:golem15/sm-translate-plugin.git` (does not exist). Host remote already exists.
+
+---
+
+### Tests — plan 04
+
+**Admin boot analog:** `USR/admin_harness_test.go` 129-191 `newAdminEnv` — `party.Activate`, `lagoon.Migrate`, `surf.Assemble`, mint backend admin with a permission set. Locales 403 without `golem15.translate.manage_locales`.
+
+**Postgres analog:** `USR/updates/postgres_test.go` 25-36 TestMain + testcontainers `postgres:16-alpine`, `-short` skip.
+
+**Surf analog:** `FW/modules/surf/locale_from_principal_test.go` — table of Resolver-absent (raw Accept-Language still set, fonoteka) vs Resolver-present (URL wins; invalid prefix ignored; cookie flag skips Accept-Language; unlisted code rejected).
+
+Lean PHP landmines (do not 1:1 the suite): fallback default; set `pl` does not change default column; `WithLocale` + indexed slug; skip morphMap, `addTranslatableAttributes`, Messages, CMS page/url.
+
+## Shared Patterns
+
+### Plugin mount (submodule + go.work + replace)
+
+**Source:** `USR/go.mod`, `BM/go.mod` / `go.work` / `summer.yaml` / `.gitmodules`, `.planning/notes/core-plugins-own-repos.md`
+**Apply to:** `TR/` repo creation and `APP/` first mount
+
+Module `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, ID `golem15.translate`, `party.Register` in `init`, `summer.yaml` lists the id+module, submodule `plugins/golem15/translate`, plugin `replace` framework `../../../../summercms.go`, host `replace` plugin `./plugins/golem15/translate`.
+
+### Backpack Publish / Lookup (optional Translator)
+
+**Source:** `FW/modules/backpack/app.go` 59-73; `USR/plugin.go` Boot 77-95
+**Apply to:** Translator Resolver; surf `locale()` Lookup
+
+Interface lives in **framework** (`surf` or a tiny contract next to `locale()`). Plugin Boot publishes. Surf looks up; if missing, today's Accept-Language. No process-wide locale (KERN-07).
+
+### Request locale bag
+
+**Source:** `FW/modules/towel/context.go` 55-62
+**Apply to:** Translator and phrasebook
+
+Always `towel.WithLocale(ctx, validatedCode)`. Never `var currentLocale`. Never stuff the raw `Accept-Language` header into context when Resolver is present.
+
+### Admin CRUD + permissions
+
+**Source:** `USR/admin.go`, `usergroups_admin_controller.go`, `pact.HasAdminControllers` / `AdminPermissioned` (`FW/modules/pact/capabilities.go` 182-195)
+**Apply to:** Locales controller
+
+YAML + `CRUDService`. `RequiredPermissions` `golem15.translate.manage_locales`. Fillable allow-list. Lifecycle hooks return `ValidationError` (422) or `ForbiddenError` (403). Fail-loud YAML at `cabana.Activate` (`docs/backend/admin-controllers.md` 161).
+
+### Nested form values (ML save)
+
+**Source:** `FW/modules/cabana/field_permission.go` `liftPermissionValues`; `crud.go` `save` lifts before `ProjectWritableFields`
+**Apply to:** `mltext` / `mlmarkdown` locale maps
+
+Lift `map[string]string` per ML field before nested drop. Default locale → host column; other locales → `SetTranslated`. Do not bind ML maps as extra model columns.
+
+### Morph type strings
+
+**Source:** `USR/models/user.go` `MorphName`; `FW/modules/lagoon/attach/example_test.go`
+**Apply to:** `winter_translate_attributes.model_type` / indexes
+
+Explicit `MorphName() string`. Do not use `reflect.TypeOf(x).String()`.
+
+### Phrasebook vs model translations
+
+**Source:** `USR/lang/{en,pl}/lang.yaml`; `FW/modules/phrasebook` (HasLang)
+**Apply to:** Locales UI strings only
+
+`I18N-01` is phrasebook. Attribute JSON is the model layer. Seed Locale **rows** independently of YAML files.
+
+### Markdown
+
+**Source:** `FW/modules/postcard/templates.go` goldmark pipeline
+**Apply to:** cabana `markdown` / `mlmarkdown` preview if any
+
+One library (`github.com/yuin/goldmark` v1.8.6). Safe HTML. No second markdown parser.
+
+### Cookie flags
+
+**Source:** `FW/modules/cabana/auth.go` `sessionCookie` 243-252
+**Apply to:** `locale_manually_set` and locale cookie
+
+HttpOnly + Secure + SameSite. Manual cookie is flag `"1"`, not a locale code. Re-validate locale cookies against enabled list every request.
+
+### Test harness
+
+**Source:** `USR/admin_harness_test.go` `newAdminEnv`; `USR/updates/postgres_test.go`
+**Apply to:** plugin admin + migration tests last
+
+`party.Activate` + `lagoon.Migrate` + `surf.Assemble`. testcontainers behind `-short`.
+
+## No Analog Found
+
+| File | Role | Data Flow | Reason |
+|------|------|-----------|--------|
+| `TR/classes/translatable.go` (attribute JSON + `WithLocale` index lookup) | service | CRUD | No Go model currently stores translations in `winter_translate_*`. Copy PHP `TranslatableModel` / `TranslatableBehavior` storage; use `MorphName` + GORM only as structural analogs. |
+| `admin/.../MLTextField.vue` / `MLMarkdownField.vue` locale switcher | component | request-response | No multilingual SPA control exists. Compose TextField/TextareaField + PHP `_mltext.htm` chrome; nested save follows permissioneditor. |
+
+Planner should use RESEARCH.md §3–§5 for those two, not invent JSON columns or `type: widget` ML controls.
+
+## Do not copy / anti-patterns
+
+- **`golem15_translate_*` table names** — frozen PHP uses `winter_translate_*`.
+- **JSON column on host models** — D-10.
+- **Translator singleton / package-level request locale** — KERN-07.
+- **Accept-Language as the only resolver** — D-07; also stop stuffing the raw header into context when Resolver runs.
+- **Plugin-owned `type: widget` ML controls** — field types belong in cabana (`formFieldTypes`).
+- **`HasSettings` for Locales** — it is CRUD; use `HasAdminControllers` + `HasNavigation`.
+- **`HasHouseMiddleware` as the locale seam** — house-MW is named; `locale()` is hardcoded in `wrap`.
+- **Messages admin, locale picker, AI/theme commands, message import** — deferred.
+- **Seeding `de`** — D-08 is `en`+`pl` only.
+- **Editing `wn-translate-plugin` / PHP tree**.
+- **AutoMigrate as schema** — gormigrate squash only.
+- **Hand-editing `boardwalk/dist` or OpenAPI JSON**.
+- **Naming a consuming app in the plugin README**.
+
+## Metadata
+
+**Analog search scope:** `USR/` (sm-user-plugin), `BM/` (sm-bm-app), `FW/modules/{cabana,surf,towel,backpack,pact,postcard,lagoon}`, `FW/admin/src/components/form`, `FW/docs/backend`, `PHP/` translate plugin at `725d547ec839f02b5fdc0f0a6faaed601a414d50`
+**Files scanned:** ~90 analog files read or grepped; 3–5 strong matches per new file; PHP pin SHA verified
+**Pattern extraction date:** 2026-10-06
+**Tracked-source gate:** every analog path printed by `git ls-files` in its own repository
diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
index 97db288..3da09dc 100644
--- a/.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
+++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
@@ -699,21 +699,18 @@ DATA_r3t6y0ab_END
A1–A3 are execution details, not stack choices. A4 follows locked D-08.
-## Open Questions
+## Open Questions (RESOLVED)
-1. **Proof fixture location**
+1. **Proof fixture location — RESOLVED**
- What we know: D-17 wants a fixture model; host checkout is empty/missing.
- - What's unclear: host app plugin vs test-only fixture.
- - Recommendation: plugin integration test + host boot without Journal; no third production plugin.
+ - Resolution: use a test-only fixture plugin/model in the translate plugin integration harness, plus a real `sm-grzybyfunkcjonalne-app` host boot with user+translate and no third production plugin. Plans 02–04 implement this split.
-2. **Admin session for Translator**
+2. **Admin session for Translator — RESOLVED**
- What we know: PHP uses Laravel `Session::put(SESSION_LOCALE)`.
- - What's unclear: SummerCMS public requests may not have a session store yet (admin uses JWT cookie).
- - Recommendation: signed cookie `golem15.translate.locale` plus `locale_manually_set`; document as the Go analog of session+cookie.
+ - Resolution: use a signed remembered-locale cookie named `golem15.translate.locale`, with `locale_manually_set` remaining a separate flag-only cookie; validate the remembered code against enabled locales on every request. Plan 01 implements this Go session analog.
-3. **Phase 15 ROADMAP depends_on**
- - Deferred idea: `$gsd-phase --edit 15` not done in discuss.
- - Recommendation: planner notes it; do not block this phase.
+3. **Phase 15 ROADMAP depends_on — RESOLVED**
+ - Resolution: keep the ROADMAP dependency edit deferred and out of Phase 14.2.1. The plans document Phase 15 as the consumer without mutating its roadmap metadata or blocking this phase.
## Environment Availability