i18n.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package i18n
  2. import (
  3. "embed"
  4. "strings"
  5. "sync"
  6. "github.com/gin-gonic/gin"
  7. "github.com/nicksnyder/go-i18n/v2/i18n"
  8. "golang.org/x/text/language"
  9. "gopkg.in/yaml.v3"
  10. "github.com/QuantumNous/new-api/common"
  11. "github.com/QuantumNous/new-api/constant"
  12. "github.com/QuantumNous/new-api/dto"
  13. )
  14. const (
  15. LangZh = "zh"
  16. LangEn = "en"
  17. DefaultLang = LangEn // Fallback to English if language not supported
  18. )
  19. //go:embed locales/*.yaml
  20. var localeFS embed.FS
  21. var (
  22. bundle *i18n.Bundle
  23. localizers = make(map[string]*i18n.Localizer)
  24. mu sync.RWMutex
  25. initOnce sync.Once
  26. )
  27. // Init initializes the i18n bundle and loads all translation files
  28. func Init() error {
  29. var initErr error
  30. initOnce.Do(func() {
  31. bundle = i18n.NewBundle(language.Chinese)
  32. bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
  33. // Load embedded translation files
  34. files := []string{"locales/zh.yaml", "locales/en.yaml"}
  35. for _, file := range files {
  36. _, err := bundle.LoadMessageFileFS(localeFS, file)
  37. if err != nil {
  38. initErr = err
  39. return
  40. }
  41. }
  42. // Pre-create localizers for supported languages
  43. localizers[LangZh] = i18n.NewLocalizer(bundle, LangZh)
  44. localizers[LangEn] = i18n.NewLocalizer(bundle, LangEn)
  45. // Set the TranslateMessage function in common package
  46. common.TranslateMessage = T
  47. })
  48. return initErr
  49. }
  50. // GetLocalizer returns a localizer for the specified language
  51. func GetLocalizer(lang string) *i18n.Localizer {
  52. lang = normalizeLang(lang)
  53. mu.RLock()
  54. loc, ok := localizers[lang]
  55. mu.RUnlock()
  56. if ok {
  57. return loc
  58. }
  59. // Create new localizer for unknown language (fallback to default)
  60. mu.Lock()
  61. defer mu.Unlock()
  62. // Double-check after acquiring write lock
  63. if loc, ok = localizers[lang]; ok {
  64. return loc
  65. }
  66. loc = i18n.NewLocalizer(bundle, lang, DefaultLang)
  67. localizers[lang] = loc
  68. return loc
  69. }
  70. // T translates a message key using the language from gin context
  71. func T(c *gin.Context, key string, args ...map[string]any) string {
  72. lang := GetLangFromContext(c)
  73. return Translate(lang, key, args...)
  74. }
  75. // Translate translates a message key for the specified language
  76. func Translate(lang, key string, args ...map[string]any) string {
  77. loc := GetLocalizer(lang)
  78. config := &i18n.LocalizeConfig{
  79. MessageID: key,
  80. }
  81. if len(args) > 0 && args[0] != nil {
  82. config.TemplateData = args[0]
  83. }
  84. msg, err := loc.Localize(config)
  85. if err != nil {
  86. // Return key as fallback if translation not found
  87. return key
  88. }
  89. return msg
  90. }
  91. // userLangLoaderFunc is a function that loads user language from database/cache
  92. // It's set by the model package to avoid circular imports
  93. var userLangLoaderFunc func(userId int) string
  94. // SetUserLangLoader sets the function to load user language (called from model package)
  95. func SetUserLangLoader(loader func(userId int) string) {
  96. userLangLoaderFunc = loader
  97. }
  98. // GetLangFromContext extracts the language setting from gin context
  99. // It checks multiple sources in priority order:
  100. // 1. User settings (ContextKeyUserSetting) - if already loaded (e.g., by TokenAuth)
  101. // 2. Lazy load user language from cache/DB using user ID
  102. // 3. Language set by middleware (ContextKeyLanguage) - from Accept-Language header
  103. // 4. Default language (English)
  104. func GetLangFromContext(c *gin.Context) string {
  105. if c == nil {
  106. return DefaultLang
  107. }
  108. // 1. Try to get language from user settings (if already loaded by TokenAuth or other middleware)
  109. if userSetting, ok := common.GetContextKeyType[dto.UserSetting](c, constant.ContextKeyUserSetting); ok {
  110. if userSetting.Language != "" {
  111. normalized := normalizeLang(userSetting.Language)
  112. if IsSupported(normalized) {
  113. return normalized
  114. }
  115. }
  116. }
  117. // 2. Lazy load user language using user ID (for session-based auth where full settings aren't loaded)
  118. if userLangLoaderFunc != nil {
  119. if userId, exists := c.Get("id"); exists {
  120. if uid, ok := userId.(int); ok && uid > 0 {
  121. lang := userLangLoaderFunc(uid)
  122. if lang != "" {
  123. normalized := normalizeLang(lang)
  124. if IsSupported(normalized) {
  125. return normalized
  126. }
  127. }
  128. }
  129. }
  130. }
  131. // 3. Try to get language from context (set by I18n middleware from Accept-Language)
  132. if lang := c.GetString(string(constant.ContextKeyLanguage)); lang != "" {
  133. normalized := normalizeLang(lang)
  134. if IsSupported(normalized) {
  135. return normalized
  136. }
  137. }
  138. // 4. Try Accept-Language header directly (fallback if middleware didn't run)
  139. if acceptLang := c.GetHeader("Accept-Language"); acceptLang != "" {
  140. lang := ParseAcceptLanguage(acceptLang)
  141. if IsSupported(lang) {
  142. return lang
  143. }
  144. }
  145. return DefaultLang
  146. }
  147. // ParseAcceptLanguage parses the Accept-Language header and returns the preferred language
  148. func ParseAcceptLanguage(header string) string {
  149. if header == "" {
  150. return DefaultLang
  151. }
  152. // Simple parsing: take the first language tag
  153. parts := strings.Split(header, ",")
  154. if len(parts) == 0 {
  155. return DefaultLang
  156. }
  157. // Get the first language and remove quality value
  158. firstLang := strings.TrimSpace(parts[0])
  159. if idx := strings.Index(firstLang, ";"); idx > 0 {
  160. firstLang = firstLang[:idx]
  161. }
  162. return normalizeLang(firstLang)
  163. }
  164. // normalizeLang normalizes language code to supported format
  165. func normalizeLang(lang string) string {
  166. lang = strings.ToLower(strings.TrimSpace(lang))
  167. // Handle common variations
  168. switch {
  169. case strings.HasPrefix(lang, "zh"):
  170. return LangZh
  171. case strings.HasPrefix(lang, "en"):
  172. return LangEn
  173. default:
  174. return DefaultLang
  175. }
  176. }
  177. // SupportedLanguages returns a list of supported language codes
  178. func SupportedLanguages() []string {
  179. return []string{LangZh, LangEn}
  180. }
  181. // IsSupported checks if a language code is supported
  182. func IsSupported(lang string) bool {
  183. lang = normalizeLang(lang)
  184. for _, supported := range SupportedLanguages() {
  185. if lang == supported {
  186. return true
  187. }
  188. }
  189. return false
  190. }