D-01: A plugin's HasLang FS loads lang/<locale>/<group>.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.
path
provides
phrasebook/loader.go
embedded YAML catalog loading and namespace validation
path
provides
phrasebook/translator.go
app-scoped Get and Choice lookup, CLDR and pipe selection, fallback and parameters
path
provides
party/registry.go
HasLang activation and translator publication before plugin Boot
from
to
via
examples/hello/plugins/base/plugin.go
pact.HasLang
LangFS embedded assets
from
to
via
party/registry.go
phrasebook/loader.go
ordered plugin capability scan before Boot
from
to
via
phrasebook/translator.go
towel/context.go
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.
@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.
<threat_model>
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.
</threat_model>
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.
<success_criteria>
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.
</success_criteria>
Create .planning/phases/04-cli-scaffolding-i18n-and-mail/04-02-SUMMARY.md when done.