localization.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. package localization
  2. import (
  3. "embed"
  4. "encoding/json"
  5. "net/http"
  6. "strings"
  7. "sync"
  8. "github.com/TecharoHQ/anubis"
  9. "github.com/nicksnyder/go-i18n/v2/i18n"
  10. "golang.org/x/text/language"
  11. )
  12. //go:embed locales/*.json
  13. var localeFS embed.FS
  14. type LocalizationService struct {
  15. bundle *i18n.Bundle
  16. }
  17. var (
  18. globalService *LocalizationService
  19. once sync.Once
  20. )
  21. func NewLocalizationService() *LocalizationService {
  22. once.Do(func() {
  23. bundle := i18n.NewBundle(language.English)
  24. bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
  25. // Read all JSON files from the locales directory
  26. entries, err := localeFS.ReadDir("locales")
  27. if err != nil {
  28. // Try fallback - create a minimal service with default messages
  29. globalService = &LocalizationService{bundle: bundle}
  30. return
  31. }
  32. loadedAny := false
  33. for _, entry := range entries {
  34. if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") {
  35. filePath := "locales/" + entry.Name()
  36. _, err := bundle.LoadMessageFileFS(localeFS, filePath)
  37. if err != nil {
  38. // Log error but continue with other files
  39. continue
  40. }
  41. loadedAny = true
  42. }
  43. }
  44. if !loadedAny {
  45. // If no files were loaded successfully, create minimal service
  46. globalService = &LocalizationService{bundle: bundle}
  47. return
  48. }
  49. globalService = &LocalizationService{bundle: bundle}
  50. })
  51. // Safety check - if globalService is still nil, create a minimal one
  52. if globalService == nil {
  53. bundle := i18n.NewBundle(language.English)
  54. bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
  55. globalService = &LocalizationService{bundle: bundle}
  56. }
  57. return globalService
  58. }
  59. func (ls *LocalizationService) GetLocalizer(lang string) *i18n.Localizer {
  60. return i18n.NewLocalizer(ls.bundle, lang)
  61. }
  62. func (ls *LocalizationService) GetLocalizerFromRequest(r *http.Request) *i18n.Localizer {
  63. if ls == nil || ls.bundle == nil {
  64. // Fallback to a basic bundle if service is not properly initialized
  65. bundle := i18n.NewBundle(language.English)
  66. bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
  67. return i18n.NewLocalizer(bundle, "en")
  68. }
  69. acceptLanguage := r.Header.Get("Accept-Language")
  70. // Parse Accept-Language header to properly handle quality factors
  71. // The language.ParseAcceptLanguage function returns tags sorted by quality
  72. tags, _, err := language.ParseAcceptLanguage(acceptLanguage)
  73. if err != nil || len(tags) == 0 {
  74. return i18n.NewLocalizer(ls.bundle, "en")
  75. }
  76. // Convert parsed tags to strings for the localizer
  77. // We include both the full tag and base language to ensure proper matching
  78. langs := make([]string, 0, len(tags)*2+1)
  79. for _, tag := range tags {
  80. langs = append(langs, tag.String())
  81. // Also add base language (e.g., "en" for "en-GB") to help matching
  82. base, _ := tag.Base()
  83. if base.String() != tag.String() {
  84. langs = append(langs, base.String())
  85. }
  86. }
  87. langs = append(langs, "en") // Always include English as fallback
  88. return i18n.NewLocalizer(ls.bundle, langs...)
  89. }
  90. // SimpleLocalizer wraps i18n.Localizer with a more convenient API
  91. type SimpleLocalizer struct {
  92. Localizer *i18n.Localizer
  93. }
  94. // T provides a concise way to localize messages
  95. func (sl *SimpleLocalizer) T(messageID string) string {
  96. return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
  97. }
  98. // Get the language that is used by the localizer by retrieving a well-known string that is required to be present
  99. func (sl *SimpleLocalizer) GetLang() string {
  100. _, tag, err := sl.Localizer.LocalizeWithTag(&i18n.LocalizeConfig{MessageID: "loading"})
  101. if err != nil {
  102. return "en"
  103. }
  104. return tag.String()
  105. }
  106. // GetLocalizer creates a localizer based on the request's Accept-Language header or forcedLanguage option
  107. func GetLocalizer(r *http.Request) *SimpleLocalizer {
  108. var localizer *i18n.Localizer
  109. if anubis.ForcedLanguage == "" {
  110. localizer = NewLocalizationService().GetLocalizerFromRequest(r)
  111. } else {
  112. localizer = NewLocalizationService().GetLocalizer(anubis.ForcedLanguage)
  113. }
  114. return &SimpleLocalizer{Localizer: localizer}
  115. }