feat(html): restore context translator swapping
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
436bd3716f
commit
b3b44ae432
4 changed files with 80 additions and 3 deletions
27
context.go
27
context.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue