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>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
---
|
||||
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*
|
||||
Reference in New Issue
Block a user