# 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-` 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 ` | 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`