--- phase: 04-cli-scaffolding-i18n-and-mail plan: "02" type: execute wave: 2 depends_on: ["04-01"] files_modified: - phrasebook/loader.go - phrasebook/translator.go - phrasebook/translator_test.go - party/registry.go - examples/hello/plugins/base/plugin.go - examples/hello/plugins/base/lang/en/lang.yaml - examples/hello/plugins/base/lang/pl/lang.yaml - examples/hello/hello_test.go - go.mod - go.sum autonomous: true requirements: [I18N-01] must_haves: truths: - "D-01: A plugin's HasLang FS loads lang//.yaml, flattens nested keys, and resolves vendor.plugin::group.dot.path." - "D-02: YAML CLDR category maps and Laravel pipe strings both choose correct pl/en plural forms, including {0} and [2,*] explicit conditions." - "D-03: :name, :Name, and :NAME are substituted after plural selection without changing PHP-style source values." - "D-04: pl-PL lookup checks pl-PL, then pl, then configured fallback, then returns the raw key; a missing key logs once per key outside production." - "D-05: app.locale and app.fallback_locale select default/fallback locale, each defaulting to en in the framework; no Polish framework default is installed." artifacts: - path: phrasebook/loader.go provides: embedded YAML catalog loading and namespace validation - path: phrasebook/translator.go provides: app-scoped Get and Choice lookup, CLDR and pipe selection, fallback and parameters - path: party/registry.go provides: HasLang activation and translator publication before plugin Boot key_links: - from: examples/hello/plugins/base/plugin.go to: pact.HasLang via: LangFS embedded assets - from: party/registry.go to: phrasebook/loader.go via: ordered plugin capability scan before Boot - from: phrasebook/translator.go to: towel/context.go via: towel.Locale accessor for the request-context seam --- ## Phase Goal **As a** plugin developer, **I want to** generate compiling plugin artifacts, resolve translated strings, and send registered mail, **so that** I can port WinterCMS plugins into one SummerCMS binary. Resolve plugin-owned Polish and English translation keys, plural forms, parameters, and locale fallbacks. Purpose: A ported plugin can retain WinterCMS key names and message values while SummerCMS chooses the requested language. Output: phrasebook service, HasLang boot registration, and a working hello plugin catalog. @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md @CLAUDE.md @.planning/ROADMAP.md @.planning/REQUIREMENTS.md @.planning/phases/04-cli-scaffolding-i18n-and-mail/04-CONTEXT.md @.planning/phases/04-cli-scaffolding-i18n-and-mail/04-RESEARCH.md @.planning/phases/04-cli-scaffolding-i18n-and-mail/04-PATTERNS.md @.planning/phases/04-cli-scaffolding-i18n-and-mail/04-01-SUMMARY.md @../modules/summer-phrasebook/README.md From Plan 01: pact.HasLang.LangFS() fs.FS. party.Activate(app, ids) resolves plugins in Requires order, merges HasConfig, then runs all Register methods before Boot. backpack.App.Publish[T] and Lookup[T] are app-scoped. compass.Config.String reads app.locale and app.fallback_locale; towel.Locale(ctx) reads the existing Accept-Language context value from surf. Define phrasebook.Translator with Get(ctx, key, params) string and Choice(ctx, key, count, params) string, plus explicit-locale variants for callers without a request context. Use go-i18n/v2 v2.6.1 only to choose CLDR category labels; phrasebook owns source string and placeholder replacement. Task 1: Load a namespaced translation from an embedded plugin phrasebook/loader.go, phrasebook/translator.go, phrasebook/translator_test.go, party/registry.go, examples/hello/plugins/base/plugin.go, examples/hello/plugins/base/lang/en/lang.yaml, examples/hello/plugins/base/lang/pl/lang.yaml, examples/hello/hello_test.go phrasebook/loader.go (create); phrasebook/translator.go (create); phrasebook/translator_test.go (create); party/registry.go; pact/capabilities.go; backpack/app.go; compass/config.go; examples/hello/plugins/base/plugin.go; examples/hello/hello_test.go; examples/hello/config/app.yaml; 04-CONTEXT.md D-01, D-05; 04-RESEARCH.md Translations Write an end-to-end hello smoke case first: Activate the base plugin, look up a Polish and English vendor.plugin::group.key, and observe the initial failure. In the same task, implement fs.WalkDir catalog loading from HasLang.LangFS, sorted file processing, nested YAML flattening and full namespace construction. Reject malformed paths, non-string leaves, duplicate keys and duplicate namespace owners with plugin/file/key context. Add real en/pl hello YAML and embed it in base.Plugin. Construct and publish one phrasebook.Translator on the app before Boot, using app.locale and app.fallback_locale with framework defaults en/en. Make the smoke green before committing; keep root vet/test green at the commit. go test ./phrasebook -run TestTranslationSmoke -short -count=1 && go -C examples/hello test ./... && go vet ./... && go test ./... A hello plugin can resolve its own dotted key in en and pl after party.Activate; malformed and colliding catalogs fail activation with context. A caller retrieves a string through the published translator from embedded YAML. Task 2: Select CLDR and Laravel plural forms with PHP parameters phrasebook/loader.go, phrasebook/translator.go, phrasebook/translator_test.go, examples/hello/plugins/base/lang/en/lang.yaml, examples/hello/plugins/base/lang/pl/lang.yaml, go.mod, go.sum phrasebook/loader.go; phrasebook/translator.go; phrasebook/translator_test.go; examples/hello/plugins/base/lang/en/lang.yaml; examples/hello/plugins/base/lang/pl/lang.yaml; go.mod; go.sum; 04-CONTEXT.md D-02, D-03; 04-RESEARCH.md Library findings; ../modules/summer-phrasebook/README.md Add go-i18n/v2 v2.6.1 as the research-named dependency. Keep plural selection and lookup together in translator.go to keep the package focused. Interpret a YAML map as a plural map only when all keys are CLDR category names, require other, and validate categories for the locale; otherwise recurse as a nested key namespace. Use go-i18n solely to select a category label from count, then choose the unmodified YAML message and replace :name, :Name, :NAME within phrasebook. Parse pipe strings in CLDR category order, with exact {n} and inclusive [a,b] or [a,*] conditions taking precedence; reject malformed or incomplete pipes at catalog load. Cover pl counts 1, 2, 5, 22 and en 1, 2 plus zero and fractional values in the focused smoke. Do not let go-i18n evaluate message text as a Go template. go test ./phrasebook -run 'TestTranslationSmoke|TestPluralSmoke' -short -count=1 && go vet ./... && go test ./... Both plural syntaxes select the specified forms; all three parameter case variants work on the selected text; bad plural definitions fail at load time. Polish and English plural examples work through the published translator. Task 3: Apply configured locale fallback and missing-key behavior phrasebook/translator.go, phrasebook/translator_test.go, examples/hello/hello_test.go phrasebook/translator.go; phrasebook/translator_test.go; examples/hello/hello_test.go; towel/context.go; surf/router.go; compass/config.go; 04-CONTEXT.md D-04, D-05 Use towel.Locale(ctx) when present and app.locale otherwise; expose explicit-locale lookup for non-request callers. Resolve requested tag first, then parent tag, then app.fallback_locale, then raw key. Deduplicate locale steps without changing their priority. In non-production mode log a missing key once per key per app-owned translator; production lookup is silent. Add hello smoke cases for pl-PL to pl, fallback en and raw key. Keep this as a context seam only: do not add Phase 7 preferred-locale lookup or per-user state. go test ./phrasebook -run 'TestTranslationSmoke|TestLocaleFallbackSmoke' -short -count=1 && go -C examples/hello test ./... && go vet ./... && go test ./... Requested/parent/fallback/raw order is observable, en/en defaults hold without app config, and missing-key logging occurs once outside production. A plugin resolves a full Polish or English key and a CLDR plural through the app-scoped service. ## Trust Boundaries | Boundary | Description | |---|---| | Embedded plugin YAML to app service | Plugin-owned catalog bytes become lookup data for all callers. | | Request context to lookup | An untrusted locale tag chooses a catalog and fallback sequence. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |---|---|---|---|---| | T-04-04 | Tampering | phrasebook loader | mitigate | Reject malformed paths, YAML leaves, duplicate namespaces, duplicate keys and invalid plural definitions at boot with plugin/file/key context. | | T-04-05 | Denial of service | locale and pipe parsing | mitigate | Bound parser work to loaded catalog size; validate conditions once at boot and use a finite fallback chain per lookup. | | T-04-06 | Information disclosure | missing-key logging | mitigate | Log only the key once per translator in non-production; never log parameters or request data. | | T-04-SC | Tampering | Go module resolution | mitigate | Pin research-named go-i18n/v2 v2.6.1; npm/pip/cargo package gate is inapplicable. | Run focused translation smoke and root go vet ./... plus go test ./... after every task. Run the hello activation smoke from the examples/hello module as a separate command before plan completion. Confirm no package-level locale state or second context key appears. I18N-01 resolves vendor.plugin::group.key from embedded en/pl YAML, handles nested keys, both plural syntaxes, case-sensitive parameter variants, configured locale fallback and raw-key behavior. A malformed catalog fails plugin activation by name. Create .planning/phases/04-cli-scaffolding-i18n-and-mail/04-02-SUMMARY.md when done.