Files
summercms/.planning/phases/04-cli-scaffolding-i18n-and-mail/04-CONTEXT.md
2026-09-18 02:14:25 +02:00

142 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 4: CLI scaffolding, i18n and mail - Context
**Gathered:** 2026-09-18
**Status:** Ready for planning
<domain>
## Phase Boundary
Framework repo (`summercms.go`) only. Three deliverables:
1. **Scaffolding** (CLI-02): `summer make:plugin`, `make:model`, `make:migration`, `make:command`, `make:job`, `make:admin-controller` generate stubs that compile and pass `go vet`, laid out in the WinterCMS directory shape and auto-wired through a generated per-plugin registry.
2. **i18n** (`phrasebook`, I18N-01): namespaced keys `vendor.plugin::group.key` loaded from per-plugin per-locale YAML, `:name` parameter substitution, CLDR plurals for pl and en (go-i18n for category selection), locale fallback chain.
3. **Mail** (`postcard`, I18N-03): plugins register mail templates and layouts by dotted name with the WinterCMS per-locale suffix convention; templates are WinterCMS-shaped files (INI header, `==`, Markdown body) rendered through `html/template` plus a Markdown renderer, sent through a driver interface (smtp via go-mail, log, memory).
Not in this phase: per-request locale resolution (Phase 7, I18N-02), translated validation messages (`lifeguard`), queued mail and River (Phase 11), the admin schema pipeline (Phase 9), restructuring the Phase 3 `fonoteka.go` plugins (Phase 5), any writes to `fonoteka.go`.
</domain>
<decisions>
## Implementation Decisions
### Lang files and plurals (phrasebook)
- **D-01:** Lang files are `lang/<locale>/<group>.yaml` inside the plugin module, embedded through the `HasLang` capability; keys are `vendor.plugin::group.dot.path`, group = filename. Nested YAML maps flatten to dot paths, as the summer-phrasebook design did for HOCON.
- **D-02:** Two plural syntaxes are both accepted. A YAML **map** under a key is a CLDR category map (`one`, `few`, `many`, `other`, plus `zero`/`two` where a locale has them) and is selected through go-i18n's plural rules. A **string containing `|`** is parsed Laravel-style: pipe-separated forms in CLDR category order, with optional explicit `{0}` / `[2,*]` conditions, as in the summer-phrasebook design. Fonoteka's PHP lang files contain no pipe plurals, so the map form is the primary target; the pipe form exists for line-by-line ports of other WinterCMS plugins.
- **D-03:** Parameters use `:name` placeholders with the `:Name` (ucfirst) and `:NAME` (upper) case variants. phrasebook performs substitution itself; go-i18n is used only for CLDR plural category selection, not for message templating. PHP lang values copy over unchanged.
- **D-04:** Missing keys walk the fallback chain (requested `pl-PL` → parent `pl` → configured fallback) and then return the raw key string, as WinterCMS does. In non-production environments a missing key is logged once per key.
- **D-05:** Default and fallback locale come from config: `app.locale` and `app.fallback_locale`, framework default `en` for both. Płytarium sets `app.locale: pl` in its own config; the framework ships no Polish default.
### Mail template format (postcard)
- **D-06:** Template files keep the WinterCMS shape exactly: an INI-style header (`subject = "..."`, `description = "..."`, `layout = "..."`), a `==` separator line, then the body. Files live at `views/mail/<name>.htm` inside the plugin, embedded through `HasMailTemplates`, and are registered by dotted name (`golem15.fonoteka::mail.collection_invitation`). The subject line is itself a template so `{{ .CollectionName }}` works there. postcard ships a small header parser; no INI library.
- **D-07:** Bodies are Markdown. Rendering order: `html/template` for variable substitution on the Markdown source, then Markdown → HTML for the HTML part; the substituted Markdown source is the plain-text part, as WinterCMS does. This adds one dependency to name in research (goldmark is the expected pick; research confirms version and HTML-escaping behavior of substituted values).
- **D-08:** Per-locale templates are the PHP convention with no framework magic: the unsuffixed name is the app-default-locale version, `-en` (etc.) suffixed names are explicit siblings, and **the caller picks the full name**. postcard does no locale lookup or fallback at send time.
- **D-09:** Layouts are registered by short name (`plytarium` → `golem15.fonoteka::mail.layouts.plytarium`), mirroring `registerMailLayouts()`. A layout file has the same INI + `==` shape with a text wrapper section and an HTML wrapper section, each rendering `{{ .Content }}`; shared variables (`css`, brand css) come from config or the registering plugin. The framework ships one neutral `default` layout.
### Scaffolding (make:*)
- **D-10:** Generated plugins follow the WinterCMS directory layout as Go subpackages, decided in `/gsd-explore` and recorded in `.planning/notes/plugin-layout-winter-directories.md`: `models/`, `classes/`, `controllers/`, `console/`, `jobs/`, `middleware/`, `updates/` are packages; `lang/`, `views/mail/`, `config/` are embedded asset directories; `plugin.go` and `routes.go` sit in the root package and import the leaves. Chosen because agents converting golem15, jz and pxpx plugins lean on the PHP directory: `models/Album.php` → `models/album.go` must be a mechanical move.
- **D-11:** `models/` is a leaf package: it never imports a sibling package of its own plugin. Casts move into `models/` (or `lagoon` when generic); lifecycle hooks that call services become GORM callbacks registered on the model from `classes/` or `plugin.go` — the same callback-registry mechanism Phase 5 uses for cross-plugin extension. Phase 4 ships the enforcement: `summer build` (or a framework `go test` over the workspace) fails naming the plugin and the offending import when a `models/` package imports a sibling.
- **D-12:** Auto-wiring through a generated per-plugin `registry.gen.go` in the root package: each `make:*` appends the new artifact to the generated slices (migrations, commands, jobs, admin controllers, models) that the scaffolded `plugin.go`'s capability methods return. Hand-written `plugin.go` is never rewritten. Existing hand-written plugins opt in by calling the same generated accessors (`append(handWritten, generatedMigrations()...)`); running `make:*` on a plugin without a registry creates it and prints the one-time line to add. The Phase 3 `fonoteka.go` plugins are not touched in this phase.
- **D-13:** `make:model <vendor.plugin> <Name>` generates the model struct in `models/` (table `vendor_plugin_names`, timestamps) **and** a create-table migration appended to the plugin's gormigrate set in `updates/`; `--no-migration` skips the migration. `make:migration` appends a migration alone.
- **D-14:** `make:admin-controller` generates a stub satisfying a minimal `pact` admin-controller interface defined in this phase (ID, model name, config directory) plus `fields.yaml` and `columns.yaml` placeholders in the WinterCMS shape Fonoteka already uses (`controllers/<name>/fields.yaml`). Phase 9 grows the interface and the template; the stub only has to compile and vet now.
- **D-15:** `make:job` generates a stub satisfying a framework job interface modeled on River's worker (a `Kind() string` on an args struct, `Work(ctx, args) error`) **without importing River**. Phase 11 adapts that interface onto River; jobs are unit-testable now by calling `Work` directly.
- **D-16:** Stub templates are embedded `text/template` files under `internal/build/stubs/*.tmpl`, rendered and passed through `go/format`. `make:plugin`'s existing Go string builders migrate to the same mechanism. A framework test generates every stub type into a temporary plugin inside `examples/hello` and runs `go build` and `go vet` on it: that test is success criterion 1.
- **D-17:** Argument shape is `make:<kind> <vendor.plugin> <Name>`. When run inside a plugin directory the plugin ID may be omitted and is read from `plugin.go` (`pluginIDFromGo` in `internal/build/scaffold.go` already does this).
### Mail drivers and test send
- **D-18:** Three drivers ship behind the `postcard` driver interface: `smtp` (go-mail), `log` (writes the rendered headers and text part to the app logger, for dev), `memory` (keeps sent messages in a slice for tests). Driver and connection settings come from a `mail.*` config section (`config/mail.yaml`, `SUMMER_MAIL__…` overrides per Phase 1 D-08).
- **D-19:** Success criterion 3 is proven twice: a unit test renders a registered template + layout through the `memory` driver and asserts subject, HTML part and text part; an integration test, gated the same way as the testcontainers Postgres tests (fails when Docker is unavailable, skipped under `-short`), starts a Mailpit container and asserts through its API that the message arrived over SMTP via go-mail.
- **D-20:** Sending is a `postcard.Mailer` service published on the `backpack` container: `Send(ctx, postcard.Message{Template, To, Cc, Bcc, ReplyTo, Vars}) error`. This is the port shape for `Mail::send('vendor.plugin::mail.x', $vars, fn)`: the PHP closure's recipient/subject calls become struct fields. No per-template Go types.
- **D-21:** `Send` returns the driver error to the caller; no swallowing, no retries (retries belong to Phase 11's queue). Registering a template whose file is missing, or a template naming an unregistered layout, fails `Boot` with the plugin ID and the name, consistent with Phase 1 D-10.
### Claude's Discretion
- Exact `phrasebook` API surface (`Get`, `Choice`, locale on context via an unexported key with exported accessors per ARCHITECTURE.md), namespace aliases and runtime overrides from the phrasebook design if cheap; the request-context seam for Phase 7.
- Which Markdown renderer (research names it; goldmark expected) and how substituted values are escaped before Markdown rendering.
- Registry file name and the exact accessor names in `registry.gen.go`; how `summer build` locates each plugin's `models/` package for the leaf check.
- What `make:plugin` scaffolds beyond Phase 1's minimum now that the directory layout is fixed (empty `lang/en/lang.yaml`, `views/mail/`, `updates/` package with an empty set are reasonable).
- Whether `make:command` stubs land in `console/` with a colon-style name derived from the plugin ID (`fonoteka:<name>`), following Phase 1 D-15.
- Mailpit image and API client (a few `net/http` calls; no SDK).
- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan" rules.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Plugin layout (binding for all make:* output)
- `.planning/notes/plugin-layout-winter-directories.md` — the directory-as-subpackage layout, the `models`-is-a-leaf rule, the Fonoteka dependency evidence and the accepted trade-off
- `.planning/todos/pending/verify-models-leaf-rule.md` — follow-up validation on other plugins (not Phase 4 work)
### Design carry-overs
- `../modules/summer-phrasebook/README.md` — key format, `:name` case variants, pipe-string plural syntax with `{n}`/`[a,b]` conditions, CLDR form ordering, fallback chain, aliases and overrides
- `.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md` — D-03 (examples/hello as testbed and template source), D-06–D-09 (config layout, `SUMMER_` env mapping), D-10 (fail boot naming plugin + missing thing), D-14–D-17 (bonfire output, colon command names, `bonfire.Command`)
- `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md` — D-16–D-19 (squashed, never-edited per-plugin gormigrate sets; `HasMigrations` capability) which `make:migration` appends to
### Research
- `.planning/research/ARCHITECTURE.md` — Pattern 1 capability interfaces (`HasLang`, `HasMailTemplates` as `fs.FS`), package table (`phrasebook`, `postcard`), Anti-Pattern on context keys
- `.planning/research/STACK.md` — go-i18n/v2 v2.6.1 (CLDR v48), goccy/go-yaml, testcontainers-go; go-mail is named in ARCHITECTURE.md's package table
- `.planning/research/FEATURES.md` §Console / i18n — mail-template-per-locale-by-suffix convention, `registerMailTemplates` pairs
- `.planning/research/PITFALLS.md` §dev loop (line ~309) — scaffolding quality is what makes the compiled-plugin loop viable for agents
### PHP reference (file shapes to mirror, read-only)
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/views/mail/collection_invitation-en.htm` — INI header + `==` + Markdown body
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/views/mail/layouts/plytarium.htm` — three-section layout (name header, text wrapper, HTML wrapper with `css`/`brandCss`)
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php` lines 333–351 — `registerMailTemplates()` / `registerMailLayouts()`
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/lang/{pl,en}/lang.php` — key structure to mirror in YAML (no plural pipes present)
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/` directory tree — the layout D-10 mirrors
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `internal/build/scaffold.go`: `MakePlugin`, `AddPlugin`, `pluginIDFromGo`, go.mod/go.work editing, toolchain pinning — `make:*` commands build on these; the Go-string source builders migrate to embedded `.tmpl` files (D-16)
- `cmd/summer/main.go`: `make:plugin`, `dev`, `plugin:add` as `bonfire.Command` values — new `make:*` commands register the same way
- `pact/capabilities.go`: `HasLang` and `HasMailTemplates` are reserved names in the comment block; both were sketched as `fs.FS` returners in ARCHITECTURE.md
- `examples/hello` (plugins `base`, `greeter`, `optional`): permanent testbed; the stub-compile test (D-16) and a lang + mail smoke can live here
- `bonfire` output widgets and non-TTY degradation for `make:*` feedback
- `compass` typed section loading for `app.locale` / `mail.*`
### Established Patterns
- Capability discovery by type assertion at a fixed kernel point (`HasConfig` before Register, `HasMigrations` in lagoon migrate, `HasRoutes` in surf assemble) — `HasLang` and `HasMailTemplates` get their own assertion points in phrasebook/postcard boot
- Fail-boot-with-names for missing dependencies, middleware and now templates/layouts
- Per-plugin ordered slices (migrations) composed at boot in `Requires()` order — lang namespaces and mail template registries compose the same way
- testcontainers gating: fail when Docker is unavailable, skip under `-short` (Phase 2/3) — reused for Mailpit
- Colon-style command names; `bonfire.Command` with injected `Output`
### Integration Points
- `pact`: new minimal interfaces for admin controllers (D-14) and jobs (D-15), plus `HasLang` / `HasMailTemplates`
- `backpack`: `postcard.Mailer` and the phrasebook translator published as services
- `summer build` / `internal/build`: the `models`-leaf check (D-11) and `registry.gen.go` generation (D-12)
- `surf` locale middleware (Phase 3 D-12 reads `Accept-Language` into context): phrasebook exposes the context accessor it will use; Phase 7 fills in user-preferred locale
</code_context>
<specifics>
## Specific Ideas
- The user consistently chose the WinterCMS-literal option over the "Go-native" one when the two diverged (INI + `==` mail files, caller-picked `-en` names, both plural syntaxes, directory-as-subpackage layout). Downstream: when a WinterCMS shape and a Go-idiomatic shape compete and neither is decided here, prefer the WinterCMS shape unless it fights the compiler.
- The conversion audience is agents porting many plugins (golem15, jz, pxpx projects), so rules must be mechanical and tool-enforced (D-11's build check), not documented conventions.
- Fonoteka mail bodies use `**bold**` and `[link](url)` Markdown with Twig variables; the Go port keeps them recognisably the same file with `{{ .Var }}` instead of `{{ var }}`.
</specifics>
<deferred>
## Deferred Ideas
- Restructuring the flat Phase 3 `fonoteka.go` plugins into the D-10 layout — Phase 5, when they are widened anyway.
- Validating the `models`-leaf rule on keios.eu / jz / pxpx plugins — todo `verify-models-leaf-rule`, before Phase 5's bulk port.
- Per-request locale from `preferred_locale` — Phase 7 (I18N-02).
- Queued sends, retries, River adapter for the Phase 4 job interface — Phase 11.
- `summer mail:test` CLI command for production smoke sends — not needed for the criterion; capture if a later phase wants it.
- Attachments on `postcard.Message` — not required by any Fonoteka template; add when a port needs it.
</deferred>
---
*Phase: 04-cli-scaffolding-i18n-and-mail*
*Context gathered: 2026-09-18*