feat(i18n): add locale message fallback
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0ed72c4952
commit
a055781d5d
2 changed files with 99 additions and 3 deletions
65
i18n.go
65
i18n.go
|
|
@ -3,6 +3,8 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
|
@ -13,6 +15,12 @@ const i18nContextKey = "i18n.locale"
|
|||
// i18nMessagesKey is the Gin context key for the message lookup map.
|
||||
const i18nMessagesKey = "i18n.messages"
|
||||
|
||||
// i18nCatalogKey is the Gin context key for the full locale->message catalog.
|
||||
const i18nCatalogKey = "i18n.catalog"
|
||||
|
||||
// i18nDefaultLocaleKey stores the configured default locale for fallback lookups.
|
||||
const i18nDefaultLocaleKey = "i18n.default_locale"
|
||||
|
||||
// I18nConfig configures the internationalisation middleware.
|
||||
type I18nConfig struct {
|
||||
// DefaultLocale is the fallback locale when the Accept-Language header
|
||||
|
|
@ -79,14 +87,13 @@ func i18nMiddleware(matcher language.Matcher, cfg I18nConfig) gin.HandlerFunc {
|
|||
}
|
||||
|
||||
c.Set(i18nContextKey, locale)
|
||||
c.Set(i18nDefaultLocaleKey, cfg.DefaultLocale)
|
||||
|
||||
// Attach the message map for this locale if messages are configured.
|
||||
if cfg.Messages != nil {
|
||||
c.Set(i18nCatalogKey, cfg.Messages)
|
||||
if msgs, ok := cfg.Messages[locale]; ok {
|
||||
c.Set(i18nMessagesKey, msgs)
|
||||
} else if msgs, ok := cfg.Messages[cfg.DefaultLocale]; ok {
|
||||
// Fall back to default locale messages.
|
||||
c.Set(i18nMessagesKey, msgs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,5 +123,57 @@ func GetMessage(c *gin.Context, key string) (string, bool) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
catalog, _ := c.Get(i18nCatalogKey)
|
||||
msgsByLocale, _ := catalog.(map[string]map[string]string)
|
||||
if len(msgsByLocale) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
locales := localeFallbacks(GetLocale(c))
|
||||
if defaultLocale, ok := c.Get(i18nDefaultLocaleKey); ok {
|
||||
if fallback, ok := defaultLocale.(string); ok && fallback != "" {
|
||||
locales = append(locales, localeFallbacks(fallback)...)
|
||||
}
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(locales))
|
||||
for _, locale := range locales {
|
||||
if locale == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[locale]; ok {
|
||||
continue
|
||||
}
|
||||
seen[locale] = struct{}{}
|
||||
if msgs, ok := msgsByLocale[locale]; ok {
|
||||
if msg, ok := msgs[key]; ok {
|
||||
return msg, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// localeFallbacks returns the locale and its parent tags in order from
|
||||
// most specific to least specific. For example, "fr-CA" yields
|
||||
// ["fr-CA", "fr"] and "zh-Hant-TW" yields ["zh-Hant-TW", "zh-Hant", "zh"].
|
||||
func localeFallbacks(locale string) []string {
|
||||
locale = strings.TrimSpace(strings.ReplaceAll(locale, "_", "-"))
|
||||
if locale == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(locale, "-")
|
||||
if len(parts) == 0 {
|
||||
return []string{locale}
|
||||
}
|
||||
|
||||
fallbacks := make([]string, 0, len(parts))
|
||||
for i := len(parts); i >= 1; i-- {
|
||||
fallbacks = append(fallbacks, strings.Join(parts[:i], "-"))
|
||||
}
|
||||
|
||||
return fallbacks
|
||||
}
|
||||
|
|
|
|||
37
i18n_test.go
37
i18n_test.go
|
|
@ -251,3 +251,40 @@ func TestWithI18n_Good_LooksUpMessage(t *testing.T) {
|
|||
t.Fatalf("expected message=%q, got %q", "Hello", respEn.Data.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithI18n_Good_FallsBackToParentLocaleMessage(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
e, _ := api.New(api.WithI18n(api.I18nConfig{
|
||||
DefaultLocale: "en",
|
||||
Supported: []string{"en", "fr", "fr-CA"},
|
||||
Messages: map[string]map[string]string{
|
||||
"en": {"greeting": "Hello"},
|
||||
"fr": {"greeting": "Bonjour"},
|
||||
},
|
||||
}))
|
||||
e.Register(&i18nTestGroup{})
|
||||
|
||||
h := e.Handler()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/i18n/greeting", nil)
|
||||
req.Header.Set("Accept-Language", "fr-CA")
|
||||
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.Locale != "fr-CA" {
|
||||
t.Fatalf("expected locale=%q, got %q", "fr-CA", resp.Data.Locale)
|
||||
}
|
||||
if resp.Data.Message != "Bonjour" {
|
||||
t.Fatalf("expected fallback message=%q, got %q", "Bonjour", resp.Data.Message)
|
||||
}
|
||||
if !resp.Data.Found {
|
||||
t.Fatal("expected found=true")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue