Lightweight Accept-Language header parsing using golang.org/x/text/language for RFC 5646 language matching with quality weighting. Provides GetLocale() and GetMessage() context helpers for handlers. Bridges to go-i18n grammar engine later — no heavy file-based i18n dependencies needed. Co-Authored-By: Virgil <virgil@lethean.io>
121 lines
3.7 KiB
Go
121 lines
3.7 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package api
|
|
|
|
import (
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
// i18nContextKey is the Gin context key for the detected locale string.
|
|
const i18nContextKey = "i18n.locale"
|
|
|
|
// i18nMessagesKey is the Gin context key for the message lookup map.
|
|
const i18nMessagesKey = "i18n.messages"
|
|
|
|
// I18nConfig configures the internationalisation middleware.
|
|
type I18nConfig struct {
|
|
// DefaultLocale is the fallback locale when the Accept-Language header
|
|
// is absent or does not match any supported locale. Defaults to "en".
|
|
DefaultLocale string
|
|
|
|
// Supported lists the locale tags the application supports.
|
|
// Each entry should be a BCP 47 language tag (e.g. "en", "fr", "de").
|
|
// If empty, only the default locale is supported.
|
|
Supported []string
|
|
|
|
// Messages maps locale tags to key-value message pairs.
|
|
// For example: {"en": {"greeting": "Hello"}, "fr": {"greeting": "Bonjour"}}
|
|
// This is optional — handlers can use GetLocale() alone for custom logic.
|
|
Messages map[string]map[string]string
|
|
}
|
|
|
|
// 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
|
|
// context and can be retrieved by handlers via GetLocale().
|
|
//
|
|
// If messages are configured, handlers can look up localised strings via
|
|
// GetMessage(). This is a lightweight bridge — the go-i18n grammar engine
|
|
// can replace the message map later.
|
|
func WithI18n(cfg ...I18nConfig) Option {
|
|
return func(e *Engine) {
|
|
var config I18nConfig
|
|
if len(cfg) > 0 {
|
|
config = cfg[0]
|
|
}
|
|
if config.DefaultLocale == "" {
|
|
config.DefaultLocale = "en"
|
|
}
|
|
|
|
// Build the language.Matcher from supported locales.
|
|
tags := []language.Tag{language.Make(config.DefaultLocale)}
|
|
for _, s := range config.Supported {
|
|
tag := language.Make(s)
|
|
// Avoid duplicating the default if it also appears in Supported.
|
|
if tag != tags[0] {
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
matcher := language.NewMatcher(tags)
|
|
|
|
e.middlewares = append(e.middlewares, i18nMiddleware(matcher, config))
|
|
}
|
|
}
|
|
|
|
// i18nMiddleware returns Gin middleware that parses Accept-Language, matches
|
|
// it against supported locales, and stores the result in the context.
|
|
func i18nMiddleware(matcher language.Matcher, cfg I18nConfig) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
accept := c.GetHeader("Accept-Language")
|
|
|
|
var locale string
|
|
if accept == "" {
|
|
locale = cfg.DefaultLocale
|
|
} else {
|
|
tags, _, _ := language.ParseAcceptLanguage(accept)
|
|
tag, _, _ := matcher.Match(tags...)
|
|
base, _ := tag.Base()
|
|
locale = base.String()
|
|
}
|
|
|
|
c.Set(i18nContextKey, locale)
|
|
|
|
// Attach the message map for this locale if messages are configured.
|
|
if cfg.Messages != nil {
|
|
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)
|
|
}
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// GetLocale returns the detected locale for the current request.
|
|
// Returns "en" if the i18n middleware was not applied.
|
|
func GetLocale(c *gin.Context) string {
|
|
if v, ok := c.Get(i18nContextKey); ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return "en"
|
|
}
|
|
|
|
// GetMessage looks up a localised message by key for the current request.
|
|
// Returns the message string and true if found, or empty string and false
|
|
// if the key does not exist or the i18n middleware was not applied.
|
|
func GetMessage(c *gin.Context, key string) (string, bool) {
|
|
if v, ok := c.Get(i18nMessagesKey); ok {
|
|
if msgs, ok := v.(map[string]string); ok {
|
|
if msg, ok := msgs[key]; ok {
|
|
return msg, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|