feat(html): restore context translator swapping
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 02:03:39 +00:00
parent 436bd3716f
commit b3b44ae432
4 changed files with 80 additions and 3 deletions

View file

@ -16,7 +16,7 @@ type translatorCloner interface {
// context.go: Context carries rendering state through the node tree.
// Example: NewContext("en-GB") initialises locale-specific rendering state.
// Locale and translator selection happen at construction time.
// Locale and translator selection are managed through dedicated setters.
type Context struct {
Identity string
Locale string
@ -110,6 +110,20 @@ func NewContextWithService(svc Translator, locale ...string) *Context {
return ctx
}
// SetService swaps the translator used by the context and reapplies the
// current locale to it.
// Example: ctx.SetService(svc).
func (ctx *Context) SetService(svc Translator) *Context {
if ctx == nil {
return nil
}
ensureContextDefaults(ctx)
ctx.service = svc
applyLocaleToService(ctx.service, ctx.Locale)
return ctx
}
// SetEntitlements updates the feature gate callback used by Entitled nodes and
// returns the same context.
// Example: ctx.SetEntitlements(func(feature string) bool { return feature == "premium" }).
@ -181,6 +195,17 @@ func (ctx *Context) WithLocale(locale string) *Context {
return clone
}
// WithService returns a cloned context with a different translator.
// Example: next := ctx.WithService(svc).
func (ctx *Context) WithService(svc Translator) *Context {
clone := ctx.Clone()
if clone == nil {
return nil
}
clone.SetService(svc)
return clone
}
// WithEntitlements returns a cloned context with a different feature gate callback.
// Example: next := ctx.WithEntitlements(func(feature string) bool { return feature == "premium" }).
func (ctx *Context) WithEntitlements(entitlements func(feature string) bool) *Context {

View file

@ -88,6 +88,24 @@ func TestContext_SetLocale_ReappliesToTranslator(t *testing.T) {
}
}
func TestContext_SetService_ReappliesCurrentLocale(t *testing.T) {
ctx := NewContext("fr-FR")
svc := &localeTranslator{}
if got := ctx.SetService(svc); got != ctx {
t.Fatalf("SetService should return the same context")
}
if ctx.service != svc {
t.Fatalf("SetService should replace the active translator")
}
if svc.language != "fr" {
t.Fatalf("SetService should apply the existing locale to the new translator, got %q", svc.language)
}
if got := Text("prompt.yes").Render(ctx); got != "o" {
t.Fatalf("SetService translation = %q, want %q", got, "o")
}
}
func TestContext_SetEntitlements_UpdatesFeatureGate(t *testing.T) {
ctx := NewContext()
@ -282,6 +300,34 @@ func TestContext_WithLocaleReturnsClonedContext(t *testing.T) {
}
}
func TestContext_WithServiceReturnsClonedContext(t *testing.T) {
ctx := NewContext("fr-FR")
ctx.SetIdentity("user-001")
ctx.SetData("theme", "dark")
svc := &localeTranslator{}
next := ctx.WithService(svc)
if next == ctx {
t.Fatal("WithService should return a cloned context")
}
if got := ctx.service; got == svc {
t.Fatal("WithService should not mutate the original context service")
}
if got := next.service; got != svc {
t.Fatalf("WithService should set the requested service on the clone, got %v", got)
}
if svc.language != "fr" {
t.Fatalf("WithService should apply the existing locale to the new translator, got %q", svc.language)
}
if got := next.Data["theme"]; got != "dark" {
t.Fatalf("WithService should preserve existing data on the clone, got %v", got)
}
if got := next.Locale; got != "fr-FR" {
t.Fatalf("WithService should preserve the locale on the clone, got %q", got)
}
}
func TestContext_WithEntitlementsReturnsClonedContext(t *testing.T) {
ctx := NewContext()
ctx.SetIdentity("user-001")
@ -328,12 +374,18 @@ func TestContext_Setters_NilReceiver(t *testing.T) {
if got := ctx.SetLocale("en-GB"); got != nil {
t.Fatalf("nil Context.SetLocale should return nil, got %v", got)
}
if got := ctx.SetService(&localeTranslator{}); got != nil {
t.Fatalf("nil Context.SetService should return nil, got %v", got)
}
if got := ctx.SetEntitlements(func(string) bool { return true }); got != nil {
t.Fatalf("nil Context.SetEntitlements should return nil, got %v", got)
}
if got := ctx.WithLocale("en-GB"); got != nil {
t.Fatalf("nil Context.WithLocale should return nil, got %v", got)
}
if got := ctx.WithService(&localeTranslator{}); got != nil {
t.Fatalf("nil Context.WithService should return nil, got %v", got)
}
if got := ctx.WithEntitlements(func(string) bool { return true }); got != nil {
t.Fatalf("nil Context.WithEntitlements should return nil, got %v", got)
}

View file

@ -101,7 +101,7 @@ Two constructors are provided:
- `NewContext()` creates a context with sensible defaults and an empty `Data` map.
- `NewContextWithService(svc)` creates a context backed by a specific `i18n.Service` instance.
The `service` field is intentionally unexported. When nil, `Text` nodes fall back to the global `i18n.T()` default. This prevents callers from setting the service inconsistently after construction.
The `service` field is intentionally unexported. When nil, `Text` nodes fall back to the global `i18n.T()` default. Callers can replace the active translator with `SetService()` or `WithService()`, which reapply the current locale to the new service.
## HLCRF Layout

View file

@ -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 and immutable.** The i18n service is selected at construction time via `NewContext()` or `NewContextWithService()` and cannot be swapped afterwards. This keeps rendering state stable and avoids accidental cross-request mutation.
4. **Context.service is private, but swappable through setters.** The i18n service remains unexported, but `SetService()` and `WithService()` let callers replace it while keeping the current locale applied.
5. **TypeScript definitions are generated.** `codegen.GenerateTypeDefinitions()` produces a `.d.ts` companion for the generated Web Components.