From 7cbf6787382a6eb8e63dacd4f8bb5e16731980b8 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 01:23:40 +0000 Subject: [PATCH] fix(html): clone mutable translators in context copies Co-Authored-By: Virgil --- context.go | 4 +++ context_test.go | 49 ++++++++++++++++++++++++++++++++----- default_translator_js.go | 9 +++++++ go.sum | 4 --- translator_clone_default.go | 6 +++++ translator_clone_js.go | 6 +++++ 6 files changed, 68 insertions(+), 10 deletions(-) diff --git a/context.go b/context.go index 2a82ee2..25b7bdc 100644 --- a/context.go +++ b/context.go @@ -10,6 +10,10 @@ type Translator interface { T(key string, args ...any) string } +type translatorCloner interface { + Clone() Translator +} + // 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. diff --git a/context_test.go b/context_test.go index 8f79b65..44142b8 100644 --- a/context_test.go +++ b/context_test.go @@ -28,6 +28,15 @@ func (t *localeTranslator) SetLanguage(language string) error { return nil } +func (t *localeTranslator) Clone() Translator { + if t == nil { + return (*localeTranslator)(nil) + } + + clone := *t + return &clone +} + func TestContext_NewContextWithService_AppliesLocale(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "fr-FR") @@ -159,8 +168,8 @@ func TestContext_CloneCopiesDataWithoutSharingMap(t *testing.T) { if clone == ctx { t.Fatal("Clone should return a distinct context instance") } - if clone.service != ctx.service { - t.Fatal("Clone should preserve the active translator") + if clone.service == ctx.service { + t.Fatal("Clone should duplicate cloneable translators") } if clone.Locale != ctx.Locale { t.Fatalf("Clone should preserve locale, got %q want %q", clone.Locale, ctx.Locale) @@ -193,6 +202,31 @@ func TestContext_CloneDoesNotShareDefaultTranslator(t *testing.T) { } } +func TestContext_CloneClonesMutableTranslator(t *testing.T) { + svc := &localeTranslator{} + ctx := NewContextWithService(svc, "en-GB") + + clone := ctx.Clone() + if clone == nil { + t.Fatal("Clone should return a context") + } + if clone.service == ctx.service { + t.Fatal("Clone should isolate cloneable translators") + } + + clone.SetLocale("fr-FR") + + if got := svc.language; got != "en" { + t.Fatalf("Clone should not mutate the original translator, got %q", got) + } + if got := Text("prompt.yes").Render(ctx); got != "y" { + t.Fatalf("Clone should leave original context translation unchanged, got %q", got) + } + if got := Text("prompt.yes").Render(clone); got != "o" { + t.Fatalf("Clone should reapply locale to the cloned translator, got %q", got) + } +} + func TestContext_WithDataReturnsClonedContext(t *testing.T) { ctx := NewContext() ctx.SetData("theme", "dark") @@ -251,11 +285,14 @@ func TestContext_WithLocaleReturnsClonedContext(t *testing.T) { if got := next.Locale; got != "fr-FR" { t.Fatalf("WithLocale should set the requested locale on the clone, got %q", got) } - if got := next.service; got != ctx.service { - t.Fatal("WithLocale should preserve the active translator on the clone") + if got := next.service; got == ctx.service { + t.Fatal("WithLocale should duplicate cloneable translators on the clone") } - if svc.language != "fr" { - t.Fatalf("WithLocale should reapply locale to the cloned service, got %q", svc.language) + if svc.language != "en" { + t.Fatalf("WithLocale should not mutate the original translator, got %q", svc.language) + } + if got := Text("prompt.yes").Render(next); got != "o" { + t.Fatalf("WithLocale should reapply locale to the cloned service, got %q", got) } if got := next.Data["theme"]; got != "dark" { t.Fatalf("WithLocale should preserve existing data on the clone, got %v", got) diff --git a/default_translator_js.go b/default_translator_js.go index 5c11f53..3aa8a6c 100644 --- a/default_translator_js.go +++ b/default_translator_js.go @@ -32,6 +32,15 @@ func (t *defaultTranslator) SetLanguage(language string) error { return nil } +func (t *defaultTranslator) Clone() Translator { + if t == nil { + return (*defaultTranslator)(nil) + } + + clone := *t + return &clone +} + func newDefaultTranslator() Translator { return &defaultTranslator{} } diff --git a/go.sum b/go.sum index b1e21b6..5a10c39 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/translator_clone_default.go b/translator_clone_default.go index 8e723cf..517ef7c 100644 --- a/translator_clone_default.go +++ b/translator_clone_default.go @@ -11,6 +11,12 @@ func cloneTranslator(svc Translator, locale string) Translator { return nil } + if cloner, ok := svc.(translatorCloner); ok && cloner != nil { + if clone := cloner.Clone(); clone != nil { + return clone + } + } + if current, ok := svc.(*i18n.Service); ok && current != nil { clone := &i18n.Service{} applyLocaleToService(clone, locale) diff --git a/translator_clone_js.go b/translator_clone_js.go index 531f7db..3c19ac3 100644 --- a/translator_clone_js.go +++ b/translator_clone_js.go @@ -9,6 +9,12 @@ func cloneTranslator(svc Translator, _ string) Translator { return nil } + if cloner, ok := svc.(translatorCloner); ok && cloner != nil { + if clone := cloner.Clone(); clone != nil { + return clone + } + } + if current, ok := svc.(*defaultTranslator); ok && current != nil { clone := *current return &clone