i18n.go 5.9 KB

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