Files
summercms/.planning/phases/04-cli-scaffolding-i18n-and-mail/04-02-SUMMARY.md
Jakub Zych c203bdfc54 docs(04-02): complete i18n phrasebook plan
Tasks completed: 3/3
- Load a namespaced translation from an embedded plugin
- Select CLDR and Laravel plural forms with PHP parameters
- Apply configured locale fallback and missing-key behavior

SUMMARY: .planning/phases/04-cli-scaffolding-i18n-and-mail/04-02-SUMMARY.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 13:53:31 +02:00

163 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 04-cli-scaffolding-i18n-and-mail
plan: 02
subsystem: i18n
tags: [phrasebook, i18n, cldr, go-i18n, yaml, wintercms]
requires:
- phase: 04-cli-scaffolding-i18n-and-mail
provides: pact.HasLang, party.Activate Register-then-Boot order
- phase: 01-framework-kernel-foundation
provides: backpack.Publish/Lookup, towel.Locale, compass app.locale
provides:
- phrasebook catalog loader for lang/<locale>/<group>.yaml
- app-scoped Translator with Get/Choice and explicit-locale variants
- HasLang activation before plugin Boot
- hello plugin en/pl embedded catalog
affects: [04-03-mail, 04-04-tests, plugin-porting, I18N-02]
tech-stack:
added: [github.com/nicksnyder/go-i18n/v2@v2.6.1]
patterns:
- go-i18n selects only a CLDR category label via IdentityParser
- phrasebook owns YAML source text and :name/:Name/:NAME substitution
- HasLang catalogs load after Register and publish Translator before Boot
key-files:
created:
- 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
modified:
- party/registry.go
- party/registry_test.go
- examples/hello/plugins/base/plugin.go
- examples/hello/hello_test.go
- go.mod
- go.sum
key-decisions:
- "go-i18n is used only to choose a CLDR category label; YAML message text never enters its template engine"
- "A per-locale i18n.Bundle is cached so matcher/plural rules follow the requested tag, not the English default bundle"
- "Fallback order is requested, parent, app.fallback_locale, then the raw key; framework defaults remain en/en"
patterns-established:
- "LangFS paths must be lang/<locale>/<group>.yaml; malformed paths, non-string leaves, duplicate keys and duplicate namespace owners fail boot with plugin/file/key context"
- "YAML maps whose keys are all CLDR categories are plural maps requiring other; otherwise they flatten as nested namespaces"
- "Missing keys log once per translator outside production and never include parameters"
requirements-completed: [I18N-01]
duration: 10min
completed: 2026-09-18
---
# Phase 4 Plan 02: i18n phrasebook Summary
**Plugin-owned English and Polish YAML catalogs resolve `vendor.plugin::group.key` with CLDR and Laravel plurals, `:name` case variants, and pl-PL→pl→fallback→raw lookup**
## Performance
- **Duration:** 10 min
- **Started:** 2026-09-18T11:43:00Z
- **Completed:** 2026-09-18T11:52:55Z
- **Tasks:** 3
- **Files modified:** 11
## Accomplishments
- `phrasebook` loads embedded `lang/<locale>/<group>.yaml`, flattens nested maps to `vendor.plugin::group.dot.path`, and rejects malformed catalogs with plugin/file/key context
- `party.Activate` publishes one `*phrasebook.Translator` after Register and before Boot, using `app.locale` / `app.fallback_locale` with framework defaults `en`/`en`
- Polish and English plural maps and Laravel pipe strings select the specified forms; `:name`, `:Name`, and `:NAME` substitute after selection without sending YAML through go-i18n templates
- Requested locale walks parent then configured fallback then returns the raw key; missing keys log once outside production
## Task Commits
Each task was committed atomically:
1. **Task 1: Load a namespaced translation from an embedded plugin** - `f824e55` (feat)
2. **Task 2: Select CLDR and Laravel plural forms with PHP parameters** - `6d8786a` (feat)
3. **Task 3: Apply configured locale fallback and missing-key behavior** - `8a4e0ef` (feat)
**Plan metadata:** (this commit)
## Files Created/Modified
- `phrasebook/loader.go` - WalkDir catalog load, YAML flatten, plural-map detection, pipe parse
- `phrasebook/translator.go` - Get/Choice, CLDR category selection, fallback chain, missing-key log
- `phrasebook/translator_test.go` - translation, plural, and locale-fallback smokes
- `party/registry.go` - HasLang scan and translator publish before Boot
- `party/registry_test.go` - translator available at Boot; malformed lang fails with plugin id
- `examples/hello/plugins/base/plugin.go` - embeds LangFS and implements HasLang
- `examples/hello/plugins/base/lang/en/lang.yaml` - English nested keys, plurals, pipes, parameters
- `examples/hello/plugins/base/lang/pl/lang.yaml` - Polish nested keys, CLDR map and pipe forms
- `examples/hello/hello_test.go` - Activate + en/pl/pl-PL/fallback/raw-key smoke
- `go.mod` / `go.sum` - pin go-i18n/v2 v2.6.1
## Decisions Made
- go-i18n receives only category-name message variants and `IdentityParser`, so PHP `:name` placeholders and `{{.Count}}` YAML stay literal
- Each locale gets its own `i18n.Bundle` default language so Polish `few`/`many` are real categories, not English `other`
- Fallback stops at configured `app.fallback_locale` then the raw key; there is no extra core-locale step beyond that default
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] phrasebook tests cannot import party**
- **Found during:** Task 1 (TestTranslationSmoke Activate)
- **Issue:** `phrasebook_test` → `party` → `phrasebook` is an import cycle in tests
- **Fix:** Call `phrasebook.Activate` from phrasebook tests; keep `party.Activate` HasLang wiring tests in `party/registry_test.go`
- **Files modified:** `phrasebook/translator_test.go`, `party/registry_test.go`
- **Verification:** `TestTranslationSmoke` and `TestActivatePublishesTranslatorBeforeBoot` pass
- **Committed in:** `f824e55` (Task 1 commit)
**2. [Rule 1 - Bug] go-i18n rejects float PluralCount**
- **Found during:** Task 2 (fractional plural samples)
- **Issue:** `plural.NewOperands` requires integers or decimal strings, not `float64`
- **Fix:** Coerce float counts to decimal strings before category selection
- **Files modified:** `phrasebook/translator.go`
- **Verification:** `TestPluralSmoke` covers `1.5` for pl/en
- **Committed in:** `6d8786a` (Task 2 commit)
**3. [Rule 1 - Bug] English default bundle hid Polish CLDR forms**
- **Found during:** Task 2 (Polish `few` rejected as invalid for `pl`)
- **Issue:** A single `NewBundle(language.English)` made Localizer match English plural rules
- **Fix:** Cache one bundle per locale tag with that tag as default language
- **Files modified:** `phrasebook/translator.go`
- **Verification:** Polish counts 1/2/5/22 select one/few/many/few
- **Committed in:** `6d8786a` (Task 2 commit)
---
**Total deviations:** 3 auto-fixed (1 blocking, 2 bugs)
**Impact on plan:** No behavior divergence from D-01–D-05. Fixes were required for catalog tests to compile and for Polish CLDR selection to be real.
## Issues Encountered
None
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
Ready for 04-03 (postcard / mail). Translator is app-scoped and available at plugin Boot. Unit-test coverage remains the last plan (04-04). Per-request preferred-locale lookup stays Phase 7.
## Verification
- `go test ./phrasebook -run 'TestTranslationSmoke|TestPluralSmoke|TestLocaleFallbackSmoke' -short -count=1` — PASS
- `go -C examples/hello test ./...` — PASS
- `go vet ./... && go test ./...` — PASS
- No second context key; phrasebook reads `towel.Locale` only
- No package-level current-locale variable
## Self-Check: PASSED
---
*Phase: 04-cli-scaffolding-i18n-and-mail*
*Completed: 2026-09-18*