locale.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package locale
  2. import (
  3. "embed"
  4. "io/fs"
  5. "os"
  6. "strings"
  7. "x-ui/logger"
  8. "github.com/gin-gonic/gin"
  9. "github.com/nicksnyder/go-i18n/v2/i18n"
  10. "github.com/pelletier/go-toml/v2"
  11. "golang.org/x/text/language"
  12. )
  13. var (
  14. i18nBundle *i18n.Bundle
  15. LocalizerWeb *i18n.Localizer
  16. LocalizerBot *i18n.Localizer
  17. )
  18. type I18nType string
  19. const (
  20. Bot I18nType = "bot"
  21. Web I18nType = "web"
  22. )
  23. type SettingService interface {
  24. GetTgLang() (string, error)
  25. }
  26. func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
  27. // set default bundle to english
  28. i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
  29. i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
  30. // parse files
  31. if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
  32. return err
  33. }
  34. // setup bot locale
  35. if err := initTGBotLocalizer(settingService); err != nil {
  36. return err
  37. }
  38. return nil
  39. }
  40. func createTemplateData(params []string, seperator ...string) map[string]any {
  41. var sep string = "=="
  42. if len(seperator) > 0 {
  43. sep = seperator[0]
  44. }
  45. templateData := make(map[string]any)
  46. for _, param := range params {
  47. parts := strings.SplitN(param, sep, 2)
  48. templateData[parts[0]] = parts[1]
  49. }
  50. return templateData
  51. }
  52. func I18n(i18nType I18nType, key string, params ...string) string {
  53. var localizer *i18n.Localizer
  54. switch i18nType {
  55. case "bot":
  56. localizer = LocalizerBot
  57. case "web":
  58. localizer = LocalizerWeb
  59. default:
  60. logger.Errorf("Invalid type for I18n: %s", i18nType)
  61. return ""
  62. }
  63. templateData := createTemplateData(params)
  64. if localizer == nil {
  65. // Fallback to key if localizer not ready; prevents nil panic on pages like sub
  66. return key
  67. }
  68. msg, err := localizer.Localize(&i18n.LocalizeConfig{
  69. MessageID: key,
  70. TemplateData: templateData,
  71. })
  72. if err != nil {
  73. logger.Errorf("Failed to localize message: %v", err)
  74. return ""
  75. }
  76. return msg
  77. }
  78. func initTGBotLocalizer(settingService SettingService) error {
  79. botLang, err := settingService.GetTgLang()
  80. if err != nil {
  81. return err
  82. }
  83. LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
  84. return nil
  85. }
  86. func LocalizerMiddleware() gin.HandlerFunc {
  87. return func(c *gin.Context) {
  88. // Ensure bundle is initialized so creating a Localizer won't panic
  89. if i18nBundle == nil {
  90. i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
  91. i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
  92. // Try lazy-load from disk when running sub server without InitLocalizer
  93. if err := loadTranslationsFromDisk(i18nBundle); err != nil {
  94. logger.Warning("i18n lazy load failed:", err)
  95. }
  96. }
  97. var lang string
  98. if cookie, err := c.Request.Cookie("lang"); err == nil {
  99. lang = cookie.Value
  100. } else {
  101. lang = c.GetHeader("Accept-Language")
  102. }
  103. LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
  104. c.Set("localizer", LocalizerWeb)
  105. c.Set("I18n", I18n)
  106. c.Next()
  107. }
  108. }
  109. // loadTranslationsFromDisk attempts to load translation files from "web/translation" using the local filesystem.
  110. func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
  111. root := os.DirFS("web")
  112. return fs.WalkDir(root, "translation", func(path string, d fs.DirEntry, err error) error {
  113. if err != nil {
  114. return err
  115. }
  116. if d.IsDir() {
  117. return nil
  118. }
  119. data, err := fs.ReadFile(root, path)
  120. if err != nil {
  121. return err
  122. }
  123. _, err = bundle.ParseMessageFileBytes(data, path)
  124. return err
  125. })
  126. }
  127. func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
  128. err := fs.WalkDir(i18nFS, "translation",
  129. func(path string, d fs.DirEntry, err error) error {
  130. if err != nil {
  131. return err
  132. }
  133. if d.IsDir() {
  134. return nil
  135. }
  136. data, err := i18nFS.ReadFile(path)
  137. if err != nil {
  138. return err
  139. }
  140. _, err = i18nBundle.ParseMessageFileBytes(data, path)
  141. return err
  142. })
  143. if err != nil {
  144. return err
  145. }
  146. return nil
  147. }