140 lines
11 KiB
Markdown
140 lines
11 KiB
Markdown
---
|
|
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/<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."
|
|
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.
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.codex/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@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
|
|
|
|
<interfaces>
|
|
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.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Load a namespaced translation from an embedded plugin</name>
|
|
<files>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</files>
|
|
<read_first>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</read_first>
|
|
<action>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.</action>
|
|
<verify><automated>go test ./phrasebook -run TestTranslationSmoke -short -count=1 && go -C examples/hello test ./... && go vet ./... && go test ./...</automated></verify>
|
|
<acceptance_criteria>A hello plugin can resolve its own dotted key in en and pl after party.Activate; malformed and colliding catalogs fail activation with context.</acceptance_criteria>
|
|
<done>A caller retrieves a string through the published translator from embedded YAML.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Select CLDR and Laravel plural forms with PHP parameters</name>
|
|
<files>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</files>
|
|
<read_first>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</read_first>
|
|
<action>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.</action>
|
|
<verify><automated>go test ./phrasebook -run 'TestTranslationSmoke|TestPluralSmoke' -short -count=1 && go vet ./... && go test ./...</automated></verify>
|
|
<acceptance_criteria>Both plural syntaxes select the specified forms; all three parameter case variants work on the selected text; bad plural definitions fail at load time.</acceptance_criteria>
|
|
<done>Polish and English plural examples work through the published translator.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Apply configured locale fallback and missing-key behavior</name>
|
|
<files>phrasebook/translator.go, phrasebook/translator_test.go, examples/hello/hello_test.go</files>
|
|
<read_first>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</read_first>
|
|
<action>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.</action>
|
|
<verify><automated>go test ./phrasebook -run 'TestTranslationSmoke|TestLocaleFallbackSmoke' -short -count=1 && go -C examples/hello test ./... && go vet ./... && go test ./...</automated></verify>
|
|
<acceptance_criteria>Requested/parent/fallback/raw order is observable, en/en defaults hold without app config, and missing-key logging occurs once outside production.</acceptance_criteria>
|
|
<done>A plugin resolves a full Polish or English key and a CLDR plural through the app-scoped service.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
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.
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
Create .planning/phases/04-cli-scaffolding-i18n-and-mail/04-02-SUMMARY.md when done.
|
|
</output>
|