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" ) 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 } // GetLangFromContext extracts the language setting from gin context func GetLangFromContext(c *gin.Context) string { if c == nil { return DefaultLang } // Try to get language from context (set by middleware) if lang := c.GetString(string(constant.ContextKeyLanguage)); lang != "" { return normalizeLang(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 }