feat(api): add i18n config snapshot
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f760ab6c72
commit
5de64a0a75
3 changed files with 132 additions and 1 deletions
1
api.go
1
api.go
|
|
@ -63,6 +63,7 @@ type Engine struct {
|
|||
expvarEnabled bool
|
||||
ssePath string
|
||||
graphql *graphqlConfig
|
||||
i18nConfig I18nConfig
|
||||
}
|
||||
|
||||
// New creates an Engine with the given options.
|
||||
|
|
|
|||
49
i18n.go
49
i18n.go
|
|
@ -3,6 +3,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -46,6 +47,23 @@ type I18nConfig struct {
|
|||
Messages map[string]map[string]string
|
||||
}
|
||||
|
||||
// I18nConfig returns the configured locale and message catalogue settings for
|
||||
// the engine.
|
||||
//
|
||||
// The result snapshots the Engine state at call time and clones slices/maps so
|
||||
// callers can safely reuse or modify the returned value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// cfg := engine.I18nConfig()
|
||||
func (e *Engine) I18nConfig() I18nConfig {
|
||||
if e == nil {
|
||||
return I18nConfig{}
|
||||
}
|
||||
|
||||
return cloneI18nConfig(e.i18nConfig)
|
||||
}
|
||||
|
||||
// WithI18n adds Accept-Language header parsing and locale detection middleware.
|
||||
// The middleware uses golang.org/x/text/language for RFC 5646 language matching
|
||||
// with quality weighting support. The detected locale is stored in the Gin
|
||||
|
|
@ -77,9 +95,11 @@ func WithI18n(cfg ...I18nConfig) Option {
|
|||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
snapshot := cloneI18nConfig(config)
|
||||
e.i18nConfig = snapshot
|
||||
matcher := language.NewMatcher(tags)
|
||||
|
||||
e.middlewares = append(e.middlewares, i18nMiddleware(matcher, config))
|
||||
e.middlewares = append(e.middlewares, i18nMiddleware(matcher, snapshot))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,3 +217,30 @@ func localeFallbacks(locale string) []string {
|
|||
|
||||
return fallbacks
|
||||
}
|
||||
|
||||
func cloneI18nConfig(cfg I18nConfig) I18nConfig {
|
||||
out := cfg
|
||||
out.Supported = slices.Clone(cfg.Supported)
|
||||
out.Messages = cloneI18nMessages(cfg.Messages)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneI18nMessages(messages map[string]map[string]string) map[string]map[string]string {
|
||||
if len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make(map[string]map[string]string, len(messages))
|
||||
for locale, msgs := range messages {
|
||||
if len(msgs) == 0 {
|
||||
out[locale] = nil
|
||||
continue
|
||||
}
|
||||
cloned := make(map[string]string, len(msgs))
|
||||
for key, value := range msgs {
|
||||
cloned[key] = value
|
||||
}
|
||||
out[locale] = cloned
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
83
i18n_test.go
83
i18n_test.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -288,3 +289,85 @@ func TestWithI18n_Good_FallsBackToParentLocaleMessage(t *testing.T) {
|
|||
t.Fatal("expected found=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_I18nConfig_Good_SnapshotsCurrentSettings(t *testing.T) {
|
||||
e, _ := api.New(api.WithI18n(api.I18nConfig{
|
||||
DefaultLocale: "en",
|
||||
Supported: []string{"en", "fr"},
|
||||
Messages: map[string]map[string]string{
|
||||
"en": {"greeting": "Hello"},
|
||||
"fr": {"greeting": "Bonjour"},
|
||||
},
|
||||
}))
|
||||
|
||||
snap := e.I18nConfig()
|
||||
if snap.DefaultLocale != "en" {
|
||||
t.Fatalf("expected default locale en, got %q", snap.DefaultLocale)
|
||||
}
|
||||
if !slices.Equal(snap.Supported, []string{"en", "fr"}) {
|
||||
t.Fatalf("expected supported locales [en fr], got %v", snap.Supported)
|
||||
}
|
||||
if snap.Messages["fr"]["greeting"] != "Bonjour" {
|
||||
t.Fatalf("expected cloned French greeting, got %q", snap.Messages["fr"]["greeting"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_I18nConfig_Good_ClonesMutableInputs(t *testing.T) {
|
||||
supported := []string{"en", "fr"}
|
||||
messages := map[string]map[string]string{
|
||||
"en": {"greeting": "Hello"},
|
||||
"fr": {"greeting": "Bonjour"},
|
||||
}
|
||||
|
||||
e, _ := api.New(api.WithI18n(api.I18nConfig{
|
||||
DefaultLocale: "en",
|
||||
Supported: supported,
|
||||
Messages: messages,
|
||||
}))
|
||||
|
||||
supported[0] = "de"
|
||||
messages["fr"]["greeting"] = "Salut"
|
||||
|
||||
snap := e.I18nConfig()
|
||||
if !slices.Equal(snap.Supported, []string{"en", "fr"}) {
|
||||
t.Fatalf("expected engine supported locales to be cloned, got %v", snap.Supported)
|
||||
}
|
||||
if snap.Messages["fr"]["greeting"] != "Bonjour" {
|
||||
t.Fatalf("expected engine message catalogue to be cloned, got %q", snap.Messages["fr"]["greeting"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithI18n_Good_SnapshotsMutableInputs(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
messages := map[string]map[string]string{
|
||||
"en": {"greeting": "Hello"},
|
||||
"fr": {"greeting": "Bonjour"},
|
||||
}
|
||||
|
||||
e, _ := api.New(api.WithI18n(api.I18nConfig{
|
||||
DefaultLocale: "en",
|
||||
Supported: []string{"en", "fr"},
|
||||
Messages: messages,
|
||||
}))
|
||||
e.Register(&i18nTestGroup{})
|
||||
|
||||
messages["fr"]["greeting"] = "Salut"
|
||||
|
||||
h := e.Handler()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/i18n/greeting", nil)
|
||||
req.Header.Set("Accept-Language", "fr")
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp i18nMessageResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if resp.Data.Message != "Bonjour" {
|
||||
t.Fatalf("expected cloned greeting %q, got %q", "Bonjour", resp.Data.Message)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue