Files
summercms/.planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md
Jakub Zych bd0a27f59f docs(14.2.1): lock Go tables to golem15_translate_*
Execute-phase Task 2 chose golem15-prefix over the researched winter_translate_* names so the plugin ships vendor tables; PHP winter names stay a later import mapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 11:36:28 +02:00

228 lines
17 KiB
Markdown

---
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_golem15_translate_locales.go
- ../sm-translate-plugin/updates/202610060002_create_golem15_translate_attributes.go
- ../sm-translate-plugin/updates/202610060003_create_golem15_translate_indexes.go
- ../sm-translate-plugin/updates/202610060004_create_golem15_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 `golem15_translate_locales`, `golem15_translate_attributes`, `golem15_translate_indexes`, and empty `golem15_translate_messages`; it creates no rainlab-era or `winter_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_golem15_translate_locales.go"
provides: "squashed Locale schema"
contains: "golem15_translate_locales"
- path: "../sm-translate-plugin/updates/202610060004_create_golem15_translate_messages.go"
provides: "empty compatibility Messages table without admin"
contains: "golem15_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"
---
<objective>
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.
</objective>
<execution_context>
@~/.codex/gsd-core/workflows/execute-plan.md
@~/.codex/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
## 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 `golem15_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.
<tasks>
<task type="checkpoint:human-action" gate="blocking-human">
<name>Task 1: Create the public Gitea remote and authorize local repository setup</name>
<files>../sm-translate-plugin/</files>
<read_first>.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</read_first>
<action>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.</action>
<instructions>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.</instructions>
<verification>The executor runs `git ls-remote`, clones/initializes both local checkouts, and verifies their origin URLs before continuing.</verification>
<verify>
<automated>git ls-remote git@git.golem15.com:golem15/sm-translate-plugin.git</automated>
<fails_when>Non-zero exit, authentication denial, or repository-not-found text.</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>The intended tracked plugin repository exists locally and remotely, ready for source commits.</done>
<resume-signal>Reply `remote-created` after the empty public repository exists.</resume-signal>
</task>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 2: Confirm the one-way table contract</name>
<files>../sm-translate-plugin/updates/</files>
<read_first>.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</read_first>
<action>Record the selected schema contract before migrations are implemented. Execute-phase Task 2 locked `golem15-prefix`: Go tables are `golem15_translate_*` with the PHP pin's columns and indexes. PHP still uses `winter_translate_*`; do not copy those names into Go DDL. This gate does not reopen the PHP pin or permit a JSON-column alternative.</action>
<decision>Which persistent table contract should this shared plugin publish?</decision>
<context>Once released and imported by Journal, renaming these tables requires coordinated data migrations in every host. The frozen PHP pin reads `winter_translate_*`; the Go plugin deliberately uses `golem15_translate_*`.</context>
<options>
<option id="winter-final">
<name>Squash directly to the four final winter_translate_* tables</name>
<pros>Matches SHA 725d547, Winter imports, and Journal cutover.</pros>
<cons>Not the locked Go contract.</cons>
</option>
<option id="golem15-prefix">
<name>Use golem15_translate_* names</name>
<pros>Vendor-style naming; locked D-10 execute decision.</pros>
<cons>Winter import needs a later mapping; PHP source still says winter_translate_*.</cons>
</option>
<option id="host-json">
<name>Store translations on host-model JSON columns</name>
<pros>Fewer tables.</pros>
<cons>Contradicts D-10 and PHP storage semantics.</cons>
</option>
</options>
<verify>
<automated>test -f /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/Locale.php &amp;&amp; git -C /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate rev-parse HEAD</automated>
<fails_when>Non-zero exit or the printed SHA is not `725d547ec839f02b5fdc0f0a6faaed601a414d50`.</fails_when>
</verify>
<acceptance_criteria>
- The response is recorded in the summary.
- Task 3 starts only for `golem15-prefix`; `host-json` stops as a locked-decision conflict. `winter-final` is the discarded research recommendation.
</acceptance_criteria>
<resume-signal>Select `golem15-prefix` (locked). `winter-final` is no longer the implementation path.</resume-signal>
</task>
<task type="tracer">
<name>Task 3: Resolve one URL-prefixed request through the plugin, enabled Locale row, surf, and context</name>
<reversibility rating="one-way">The four table names and columns become the import target; changing them after release requires data migrations in every host.</reversibility>
<precondition>Tasks 1 and 2 completed, with schema option `golem15-prefix`.</precondition>
<files>../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_golem15_translate_locales.go, ../sm-translate-plugin/updates/202610060002_create_golem15_translate_attributes.go, ../sm-translate-plugin/updates/202610060003_create_golem15_translate_indexes.go, ../sm-translate-plugin/updates/202610060004_create_golem15_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</files>
<read_first>../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</read_first>
<action>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 `golem15_translate_locales`, `golem15_translate_attributes`, `golem15_translate_indexes`, and `golem15_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 `winter_translate_*` 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.</action>
<verify>
<automated>go -C ../sm-translate-plugin vet ./... &amp;&amp; go -C ../sm-translate-plugin test ./... -short -count=1 &amp;&amp; go vet ./modules/surf/... &amp;&amp; go test ./modules/surf -short -count=1</automated>
<fails_when>Non-zero exit, any package reports build failed, or either test command reports FAIL.</fails_when>
</verify>
<acceptance_criteria>
- All four migration files contain their exact `golem15_translate_*` table name; no source file contains `winter_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.
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
<threat_model>
## 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.
</threat_model>
<verification>
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.
</verification>
<success_criteria>
- The plugin and surf packages compile and pass short tests.
- Exact final `golem15_translate_*` 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.
</success_criteria>
<output>
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md` when done.
</output>