mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:25:00 +00:00
The i18n middleware runs before UserAuth, so user settings weren't available when language was detected. Now GetLangFromContext checks user settings first (set by UserAuth) before falling back to the language set by middleware or Accept-Language header.
228 lines
5.7 KiB
Go
228 lines
5.7 KiB
Go
package i18n
|
|
|
|
import (
|
|
"embed"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"golang.org/x/text/language"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/dto"
|
|
)
|
|
|
|
const (
|
|
LangZh = "zh"
|
|
LangEn = "en"
|
|
DefaultLang = LangEn // Fallback to English if language not supported
|
|
)
|
|
|
|
//go:embed locales/*.yaml
|
|
var localeFS embed.FS
|
|
|
|
var (
|
|
bundle *i18n.Bundle
|
|
localizers = make(map[string]*i18n.Localizer)
|
|
mu sync.RWMutex
|
|
initOnce sync.Once
|
|
)
|
|
|
|
// Init initializes the i18n bundle and loads all translation files
|
|
func Init() error {
|
|
var initErr error
|
|
initOnce.Do(func() {
|
|
bundle = i18n.NewBundle(language.Chinese)
|
|
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
|
|
|
// Load embedded translation files
|
|
files := []string{"locales/zh.yaml", "locales/en.yaml"}
|
|
for _, file := range files {
|
|
_, err := bundle.LoadMessageFileFS(localeFS, file)
|
|
if err != nil {
|
|
initErr = err
|
|
return
|
|
}
|
|
}
|
|
|
|
// Pre-create localizers for supported languages
|
|
localizers[LangZh] = i18n.NewLocalizer(bundle, LangZh)
|
|
localizers[LangEn] = i18n.NewLocalizer(bundle, LangEn)
|
|
|
|
// Set the TranslateMessage function in common package
|
|
common.TranslateMessage = T
|
|
})
|
|
return initErr
|
|
}
|
|
|
|
// GetLocalizer returns a localizer for the specified language
|
|
func GetLocalizer(lang string) *i18n.Localizer {
|
|
lang = normalizeLang(lang)
|
|
|
|
mu.RLock()
|
|
loc, ok := localizers[lang]
|
|
mu.RUnlock()
|
|
|
|
if ok {
|
|
return loc
|
|
}
|
|
|
|
// Create new localizer for unknown language (fallback to default)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
// Double-check after acquiring write lock
|
|
if loc, ok = localizers[lang]; ok {
|
|
return loc
|
|
}
|
|
|
|
loc = i18n.NewLocalizer(bundle, lang, DefaultLang)
|
|
localizers[lang] = loc
|
|
return loc
|
|
}
|
|
|
|
// T translates a message key using the language from gin context
|
|
func T(c *gin.Context, key string, args ...map[string]any) string {
|
|
lang := GetLangFromContext(c)
|
|
return Translate(lang, key, args...)
|
|
}
|
|
|
|
// Translate translates a message key for the specified language
|
|
func Translate(lang, key string, args ...map[string]any) string {
|
|
loc := GetLocalizer(lang)
|
|
|
|
config := &i18n.LocalizeConfig{
|
|
MessageID: key,
|
|
}
|
|
|
|
if len(args) > 0 && args[0] != nil {
|
|
config.TemplateData = args[0]
|
|
}
|
|
|
|
msg, err := loc.Localize(config)
|
|
if err != nil {
|
|
// Return key as fallback if translation not found
|
|
return key
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// userLangLoaderFunc is a function that loads user language from database/cache
|
|
// It's set by the model package to avoid circular imports
|
|
var userLangLoaderFunc func(userId int) string
|
|
|
|
// SetUserLangLoader sets the function to load user language (called from model package)
|
|
func SetUserLangLoader(loader func(userId int) string) {
|
|
userLangLoaderFunc = loader
|
|
}
|
|
|
|
// GetLangFromContext extracts the language setting from gin context
|
|
// It checks multiple sources in priority order:
|
|
// 1. User settings (ContextKeyUserSetting) - if already loaded (e.g., by TokenAuth)
|
|
// 2. Lazy load user language from cache/DB using user ID
|
|
// 3. Language set by middleware (ContextKeyLanguage) - from Accept-Language header
|
|
// 4. Default language (English)
|
|
func GetLangFromContext(c *gin.Context) string {
|
|
if c == nil {
|
|
return DefaultLang
|
|
}
|
|
|
|
// 1. Try to get language from user settings (if already loaded by TokenAuth or other middleware)
|
|
if userSetting, ok := common.GetContextKeyType[dto.UserSetting](c, constant.ContextKeyUserSetting); ok {
|
|
if userSetting.Language != "" {
|
|
normalized := normalizeLang(userSetting.Language)
|
|
if IsSupported(normalized) {
|
|
return normalized
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Lazy load user language using user ID (for session-based auth where full settings aren't loaded)
|
|
if userLangLoaderFunc != nil {
|
|
if userId, exists := c.Get("id"); exists {
|
|
if uid, ok := userId.(int); ok && uid > 0 {
|
|
lang := userLangLoaderFunc(uid)
|
|
if lang != "" {
|
|
normalized := normalizeLang(lang)
|
|
if IsSupported(normalized) {
|
|
return normalized
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Try to get language from context (set by I18n middleware from Accept-Language)
|
|
if lang := c.GetString(string(constant.ContextKeyLanguage)); lang != "" {
|
|
normalized := normalizeLang(lang)
|
|
if IsSupported(normalized) {
|
|
return normalized
|
|
}
|
|
}
|
|
|
|
// 4. Try Accept-Language header directly (fallback if middleware didn't run)
|
|
if acceptLang := c.GetHeader("Accept-Language"); acceptLang != "" {
|
|
lang := ParseAcceptLanguage(acceptLang)
|
|
if IsSupported(lang) {
|
|
return lang
|
|
}
|
|
}
|
|
|
|
return DefaultLang
|
|
}
|
|
|
|
// ParseAcceptLanguage parses the Accept-Language header and returns the preferred language
|
|
func ParseAcceptLanguage(header string) string {
|
|
if header == "" {
|
|
return DefaultLang
|
|
}
|
|
|
|
// Simple parsing: take the first language tag
|
|
parts := strings.Split(header, ",")
|
|
if len(parts) == 0 {
|
|
return DefaultLang
|
|
}
|
|
|
|
// Get the first language and remove quality value
|
|
firstLang := strings.TrimSpace(parts[0])
|
|
if idx := strings.Index(firstLang, ";"); idx > 0 {
|
|
firstLang = firstLang[:idx]
|
|
}
|
|
|
|
return normalizeLang(firstLang)
|
|
}
|
|
|
|
// normalizeLang normalizes language code to supported format
|
|
func normalizeLang(lang string) string {
|
|
lang = strings.ToLower(strings.TrimSpace(lang))
|
|
|
|
// Handle common variations
|
|
switch {
|
|
case strings.HasPrefix(lang, "zh"):
|
|
return LangZh
|
|
case strings.HasPrefix(lang, "en"):
|
|
return LangEn
|
|
default:
|
|
return DefaultLang
|
|
}
|
|
}
|
|
|
|
// SupportedLanguages returns a list of supported language codes
|
|
func SupportedLanguages() []string {
|
|
return []string{LangZh, LangEn}
|
|
}
|
|
|
|
// IsSupported checks if a language code is supported
|
|
func IsSupported(lang string) bool {
|
|
lang = normalizeLang(lang)
|
|
for _, supported := range SupportedLanguages() {
|
|
if lang == supported {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|