From b9e2630da3dc5cbb2fab33f52fe9c097ac2d1e57 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 20:44:15 +0000 Subject: [PATCH] feat(html): allow swapping context translators Co-Authored-By: Virgil --- context.go | 43 ++++++++++++++++++++++++++++++------------- context_test.go | 21 +++++++++++++++++++++ docs/development.md | 2 +- docs/history.md | 2 +- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/context.go b/context.go index 110757a..1710b2f 100644 --- a/context.go +++ b/context.go @@ -19,6 +19,23 @@ type Context struct { service Translator } +func applyLocaleToService(svc Translator, locale string) { + if svc == nil || locale == "" { + return + } + + if setter, ok := svc.(interface{ SetLanguage(string) error }); ok { + base := locale + for i := 0; i < len(base); i++ { + if base[i] == '-' || base[i] == '_' { + base = base[:i] + break + } + } + _ = setter.SetLanguage(base) + } +} + // NewContext creates a new rendering context with sensible defaults. // Usage example: html := Render(Text("welcome"), NewContext("en-GB")) func NewContext(locale ...string) *Context { @@ -35,18 +52,18 @@ func NewContext(locale ...string) *Context { // Usage example: ctx := NewContextWithService(myTranslator, "en-GB") func NewContextWithService(svc Translator, locale ...string) *Context { ctx := NewContext(locale...) - ctx.service = svc - if len(locale) > 0 { - if setter, ok := svc.(interface{ SetLanguage(string) error }); ok { - base := locale[0] - for i := 0; i < len(base); i++ { - if base[i] == '-' || base[i] == '_' { - base = base[:i] - break - } - } - _ = setter.SetLanguage(base) - } - } + ctx.SetService(svc) + return ctx +} + +// SetService swaps the translator used by the context. +// Usage example: ctx.SetService(myTranslator) +func (ctx *Context) SetService(svc Translator) *Context { + if ctx == nil { + return nil + } + + ctx.service = svc + applyLocaleToService(svc, ctx.Locale) return ctx } diff --git a/context_test.go b/context_test.go index 091785c..7c86db7 100644 --- a/context_test.go +++ b/context_test.go @@ -46,3 +46,24 @@ func TestNewContextWithService_AppliesLocaleToService_Good(t *testing.T) { t.Fatalf("NewContextWithService locale translation = %q, want %q", got, "o") } } + +func TestContext_SetService_AppliesLocale_Good(t *testing.T) { + svc, _ := i18n.New() + ctx := NewContext("fr-FR") + + if got := ctx.SetService(svc); got != ctx { + t.Fatal("SetService should return the same context for chaining") + } + + got := Text("prompt.yes").Render(ctx) + if got != "o" { + t.Fatalf("SetService locale translation = %q, want %q", got, "o") + } +} + +func TestContext_SetService_NilContext_Ugly(t *testing.T) { + var ctx *Context + if got := ctx.SetService(nil); got != nil { + t.Fatal("SetService on nil context should return nil") + } +} diff --git a/docs/development.md b/docs/development.md index ba2761d..46cbcc1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -309,6 +309,6 @@ func TestGenerateClass_ValidTag(t *testing.T) { - `NewLayout("XYZ")` silently produces empty output for unrecognised slot letters. Valid letters are `H`, `L`, `C`, `R`, `F`. There is no error or warning. - `Responsive.Variant()` accepts only `*Layout`, not arbitrary `Node` values. Arbitrary subtrees must be wrapped in a single-slot layout first. -- `Context.service` is unexported. Custom translation injection requires `NewContextWithService()`. There is no way to swap the translator after construction. +- `Context.service` is unexported. Custom translation injection uses `NewContextWithService()`, and `Context.SetService()` can swap the translator later while preserving locale-aware services. - The WASM module has no integration test for the JavaScript exports. `size_test.go` tests binary size only; it does not exercise `renderToString` behaviour from JavaScript. - `codegen.GenerateBundle()` now renders output classes in sorted slot-key order so generated bundles are stable between runs. diff --git a/docs/history.md b/docs/history.md index 24e284a..fc6ff0b 100644 --- a/docs/history.md +++ b/docs/history.md @@ -101,7 +101,7 @@ These are not regressions; they are design choices or deferred work recorded for 3. **Responsive accepts only Layout.** `Responsive.Variant()` takes `*Layout` rather than `Node`. The rationale is that `CompareVariants` in the pipeline needs access to the slot structure. Accepting `Node` would require a different approach to variant analysis. -4. **Context.service is private.** The i18n service cannot be set after construction or swapped. This is a conservative choice; relaxing it requires deciding whether mutation should be safe for concurrent use. +4. **Context.service is private.** The i18n service is still unexported, but callers can now swap it explicitly with `Context.SetService()`. This keeps the field encapsulated while allowing controlled mutation. 5. **TypeScript definitions not generated.** `codegen.GenerateBundle()` produces JS only. A `.d.ts` companion would benefit TypeScript consumers of the generated Web Components.