Phase 4: CLI scaffolding, i18n and mail - Discussion Log
Audit trail only. Do not use as input to planning, research, or execution agents.
Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
Date: 2026-09-17 / 2026-09-18
Phase: 04-cli-scaffolding-i18n-and-mail
Areas discussed: Lang file & plural syntax, Mail template format, Scaffold stub behavior, Mail drivers & test send
Lang file & plural syntax
Plural message form
| Option |
Description |
Selected |
| CLDR category map |
{one, few, many, other} YAML map; 1:1 onto go-i18n; covers Polish's 4th category |
|
| Laravel pipe strings |
":count wpis|:count wpisy|:count wpisów" plus {0}/[2,*] ranges, per summer-phrasebook |
|
| Both accepted |
Map = CLDR categories; string with | = Laravel-style |
✓ |
Notes: Fonoteka PHP lang files contain no pipe plurals; the pipe form is for line-by-line ports of other WinterCMS plugins.
Parameter placeholders
| Option |
Description |
Selected |
:name |
WinterCMS/Laravel style with :Name/:NAME variants; phrasebook substitutes, go-i18n only selects plural category |
✓ |
{{.Name}} go-i18n native |
Every ported string rewritten |
|
| You decide |
|
|
Missing key
| Option |
Description |
Selected |
| Fallback chain, then raw key |
requested → parent → fallback → key string; logged once in dev/test |
✓ |
| Fallback chain, then error |
(string, error) or panic in dev |
|
| You decide |
|
|
Default locale
| Option |
Description |
Selected |
| Config-driven, en default |
app.locale / app.fallback_locale, framework default en |
✓ |
| Config-driven, pl default |
|
|
| You decide |
|
|
Mail template format
File shape
| Option |
Description |
Selected |
| YAML front matter + body |
--- header parsed with goccy/go-yaml |
|
Keep WinterCMS INI + == |
Exactly the PHP file shape; small hand-written header parser |
✓ |
| Go registration + body-only files |
Subject/layout declared in Go |
|
Body format
| Option |
Description |
Selected |
| Markdown, add a renderer |
html/template for vars, then Markdown → HTML; raw Markdown is the text part; one new dependency (goldmark expected) |
✓ |
| Plain HTML only |
Zero new deps; six templates hand-converted; no text part |
|
| HTML now, Markdown later |
Body-renderer seam, decide at Phase 12/13 |
|
Per-locale suffix
| Option |
Description |
Selected |
| Suffix lookup with fallback |
postcard tries name-<locale> then name |
|
| Caller picks the full name |
Exactly like PHP; no locale logic in postcard |
✓ |
| You decide |
|
|
Layouts
| Option |
Description |
Selected |
| Registered by short name, HTML + text |
Mirrors registerMailLayouts and the three-section PHP layout; framework ships default |
✓ |
| HTML wrapper only |
Drops the text section |
|
| You decide |
|
|
Scaffold stub behavior
Wiring
| Option |
Description |
Selected |
| Auto-wire via generated registry file |
Per-plugin registry.gen.go; plugin.go never rewritten |
✓ |
| Edit plugin.go in place |
go/ast insertion |
|
| Write file, print the line to add |
|
|
make:model output
| Option |
Description |
Selected |
| Model + migration, flag to skip |
--no-migration |
✓ |
| Model only |
|
|
| Model + migration + fields.yaml/columns.yaml |
No consumer until Phase 9 |
|
make:admin-controller before Phase 9
| Option |
Description |
Selected |
| Minimal interface + placeholder YAML |
Tiny pact interface, WinterCMS-shaped YAML placeholders |
✓ |
| Go stub only, no YAML |
|
|
| You decide |
|
|
Stub templates
| Option |
Description |
Selected |
| Embedded text/template .tmpl files |
internal/build/stubs, go/format, compile test in examples/hello |
✓ |
| Keep Go string builders |
|
|
| You decide |
|
|
make:job before River
| Option |
Description |
Selected |
| Framework job interface, River-shaped |
Kind() on args, Work(ctx, args) error; no River import |
✓ |
| Import River now |
River enters go.mod six phases early |
|
| You decide |
|
|
File layout inside a plugin
| Option |
Description |
Selected |
| Flat, one package, name suffixes |
fonoteka.go Phase 3 style; no intra-plugin imports |
|
| WinterCMS-style subpackages |
models/, controllers/, console/, jobs/, updates/ as packages |
✓ (via /gsd-explore) |
| Flat Go, WinterCMS dirs for non-Go assets |
|
|
User's choice: Deferred to /gsd-explore ("This is a difficult one"), then decided: WinterCMS-style subpackages because agents converting golem15/jz/pxpx plugins lean on the PHP directory, not the class graph. Added rule: models/ is a leaf package (user agreed "first option seems the best"), casts move in, service-calling hooks become callbacks from classes//plugin.go, enforced by summer build. Evidence: Fonoteka has 40 classes→models edges and exactly 1 models→classes edge (a cast and a hook in Album.php). Recorded in .planning/notes/plugin-layout-winter-directories.md.
Coexistence with hand-written plugins
| Option |
Description |
Selected |
| Opt-in, merged by plugin.go |
Hand-written plugins call generated accessors; fonoteka.go untouched |
✓ |
| Migrate existing plugins in this phase |
Touches fonoteka.go, out of phase scope |
|
| You decide |
|
|
Argument shape
| Option |
Description |
Selected |
make:model <vendor.plugin> <Name> |
Plugin ID inferred from plugin.go when inside a plugin dir |
✓ |
| Always explicit, no inference |
|
|
| You decide |
|
|
Mail drivers & test send
Drivers
| Option |
Description |
Selected |
| smtp + log + memory |
go-mail, logger, in-memory slice |
✓ |
| smtp + memory only |
|
|
| smtp only |
|
|
Test send
| Option |
Description |
Selected |
| Unit test + SMTP integration test |
memory driver unit test plus Mailpit container test gated like testcontainers Postgres |
✓ |
| Unit test with memory driver only |
|
|
summer mail:test command |
Manual verification |
|
Send API
| Option |
Description |
Selected |
| Mailer service on the container |
postcard.Mailer.Send(ctx, Message{Template, To, Vars}) |
✓ |
| Typed message per template |
|
|
| You decide |
|
|
Failures
| Option |
Description |
Selected |
| Error returned, unknown template fails at boot |
Consistent with Phase 1 D-10 |
✓ |
| Error returned, lazy template check |
|
|
| You decide |
|
|
Claude's Discretion
- phrasebook API surface, aliases/overrides, request-context seam for Phase 7
- Markdown renderer pick (research) and escaping of substituted values
- registry.gen.go naming and accessor names; how the models-leaf check locates packages
- What make:plugin scaffolds beyond Phase 1's minimum in the new layout
- make:command naming under the plugin's colon prefix
- Mailpit image and API access
- Plan count and split
Deferred Ideas
- Restructure Phase 3 fonoteka.go plugins into the directory layout — Phase 5
- Validate the models-leaf rule on keios.eu / jz / pxpx plugins — todo
verify-models-leaf-rule
summer mail:test production smoke command
- Attachments on
postcard.Message