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<string,string>`.
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.
published cabana TranslationWriter delegates each locale to SetTranslated
SetTranslated
from
to
via
pattern
../sm-grzybyfunkcjonalne-app/summer.yaml
../sm-grzybyfunkcjonalne-app/plugins.gen.go
summer build generates blank imports for both submodules
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.
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<string,string> 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
<fails_when>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.</fails_when>
<acceptance_criteria>
- 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.
</acceptance_criteria>
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.
Use sm-grzybyfunkcjonalne-app with plugins/golem15/user and plugins/golem15/translate
Matches locked D-13/D-16 and the established sm-bm-app pattern.
The submodule paths become durable host layout.
Use fonoteka.go as the proof host
Already has a large application harness.
Contradicts D-13 and couples the shared plugin to the wrong app.
Copy plugin source into the host
No gitlinks.
Contradicts compiled core-plugin repository ownership and creates a fork.
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)$'
<fails_when>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".</fails_when>
<acceptance_criteria>
- 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.
</acceptance_criteria>
The framework ships typed multilingual controls and the intended host boots user+translate from durable submodules with generated admin artifacts in sync.
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.
</threat_model>
Run the Task 3 combined gate. Confirm `git status --short` in all three repositories contains only intended tracked source/generated artifacts and gitlink updates.
<success_criteria>
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.
</success_criteria>
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-03-SUMMARY.md` when done.