Files
summercms/.planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md
2026-10-06 11:22:45 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
14.2.1-translate-plugin 02 execute 2
14.2.1-01
../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
true
D-05
D-08
D-09
D-10
D-11
D-12
D-17
truths artifacts key_links
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.
path provides contains
../sm-translate-plugin/classes/translatable.go Translatable contracts, WithLocale, Translated, SetTranslated, indexed lookup type Translatable interface
path provides contains
../sm-translate-plugin/controllers/locales.go permissioned Locale admin controller and default guards golem15.translate.manage_locales
path provides contains
../sm-translate-plugin/controllers/locales/config_list.yaml sort_order asc Locales list sort_order
path provides contains
../sm-translate-plugin/lang/pl/lang.yaml Polish Locales admin phrasebook locale:
from to via pattern
../sm-translate-plugin/classes/translatable.go winter_translate_attributes non-default SetTranslated upserts one locale/model row attribute_data
from to via pattern
../sm-translate-plugin/classes/translatable.go winter_translate_indexes indexed fields upsert locale/model/item/value TranslatableIndexes
from to via pattern
../sm-translate-plugin/controllers/locales.go ../sm-translate-plugin/models/locale.go cabana CRUD hooks enforce PHP default-locale invariants 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.

<execution_context> @/.codex/gsd-core/workflows/execute-plan.md @/.codex/gsd-core/templates/summary.md </execution_context>

@.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)$' <fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestFixtureTranslatableSaveRead".</fails_when> <acceptance_criteria> - 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. </acceptance_criteria> 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)$' <fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.</fails_when> <acceptance_criteria> - 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. </acceptance_criteria> 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)$' <fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.</fails_when> <acceptance_criteria> - 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. </acceptance_criteria> Authorized administrators can manage the lean Locale surface while default-state and phrasebook/model-translation boundaries remain enforced.

<threat_model>

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. </threat_model>

Run all three task commands, then `go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1`.

<success_criteria>

  • 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. </success_criteria>
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-02-SUMMARY.md` when done.