--- 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" --- 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 `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. 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 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. 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. 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 reads `winter_translate_*`; the Go plugin deliberately uses `golem15_translate_*`. 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 `golem15-prefix`; `host-json` stops as a locked-decision conflict. `winter-final` is the discarded research recommendation. Select `golem15-prefix` (locked). `winter-final` is no longer the implementation path. 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 `golem15-prefix`. ../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 ../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 `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. 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 `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. 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 `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. Create `.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md` when done.