mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-10 07:05:02 +03:00
fix(i18n): publish the translation bundle only once it is complete
InitLocalizer filled the shared bundle in place, and the Telegram bot runs it again while the panel is serving, so a request could read the bundle's maps while they were being written and the process died on a concurrent map access. Build the bundle privately and publish it, and the bot and web localizers, through atomic pointers; the lazy load in the middleware publishes only if nothing else did first.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
|
||||
@@ -15,10 +16,11 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Each is published whole: InitLocalizer runs again (Telegram bot start) while requests read them.
|
||||
var (
|
||||
i18nBundle *i18n.Bundle
|
||||
LocalizerWeb *i18n.Localizer
|
||||
LocalizerBot *i18n.Localizer
|
||||
i18nBundle atomic.Pointer[i18n.Bundle]
|
||||
localizerWeb atomic.Pointer[i18n.Localizer]
|
||||
localizerBot atomic.Pointer[i18n.Localizer]
|
||||
)
|
||||
|
||||
// I18nType represents the type of interface for internationalization.
|
||||
@@ -37,13 +39,14 @@ type SettingService interface {
|
||||
// InitLocalizer initializes the internationalization system with embedded translation files.
|
||||
func InitLocalizer(i18nFS fs.FS, settingService SettingService) error {
|
||||
// set default bundle to English
|
||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
i18nBundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
bundle := i18n.NewBundle(language.MustParse("en-US"))
|
||||
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
|
||||
// parse files
|
||||
if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
|
||||
if err := parseTranslationFiles(i18nFS, bundle); err != nil {
|
||||
return err
|
||||
}
|
||||
i18nBundle.Store(bundle)
|
||||
|
||||
// setup bot locale
|
||||
if err := initTGBotLocalizer(settingService); err != nil {
|
||||
@@ -77,9 +80,9 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
||||
|
||||
switch i18nType {
|
||||
case "bot":
|
||||
localizer = LocalizerBot
|
||||
localizer = localizerBot.Load()
|
||||
case "web":
|
||||
localizer = LocalizerWeb
|
||||
localizer = localizerWeb.Load()
|
||||
default:
|
||||
logger.Errorf("Invalid type for I18n: %s", i18nType)
|
||||
return ""
|
||||
@@ -106,10 +109,11 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
||||
|
||||
// LocalizerFor returns a new Localizer for the given language tag using the global bundle.
|
||||
func LocalizerFor(lang string) *i18n.Localizer {
|
||||
if i18nBundle == nil {
|
||||
bundle := i18nBundle.Load()
|
||||
if bundle == nil {
|
||||
return nil
|
||||
}
|
||||
return i18n.NewLocalizer(i18nBundle, lang)
|
||||
return i18n.NewLocalizer(bundle, lang)
|
||||
}
|
||||
|
||||
// I18nForLang retrieves a localized message for a specific language tag with optional params.
|
||||
@@ -136,7 +140,7 @@ func initTGBotLocalizer(settingService SettingService) error {
|
||||
return err
|
||||
}
|
||||
|
||||
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
|
||||
localizerBot.Store(i18n.NewLocalizer(i18nBundle.Load(), botLang))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -147,13 +151,17 @@ func initTGBotLocalizer(settingService SettingService) error {
|
||||
func LocalizerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Ensure bundle is initialized so creating a Localizer won't panic
|
||||
if i18nBundle == nil {
|
||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
i18nBundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
bundle := i18nBundle.Load()
|
||||
if bundle == nil {
|
||||
bundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||
// Try lazy-load from disk when running sub server without InitLocalizer
|
||||
if err := loadTranslationsFromDisk(i18nBundle); err != nil {
|
||||
if err := loadTranslationsFromDisk(bundle); err != nil {
|
||||
logger.Warning("i18n lazy load failed:", err)
|
||||
}
|
||||
if !i18nBundle.CompareAndSwap(nil, bundle) {
|
||||
bundle = i18nBundle.Load()
|
||||
}
|
||||
}
|
||||
var lang string
|
||||
|
||||
@@ -163,9 +171,10 @@ func LocalizerMiddleware() gin.HandlerFunc {
|
||||
lang = c.GetHeader("Accept-Language")
|
||||
}
|
||||
|
||||
LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
|
||||
localizer := i18n.NewLocalizer(bundle, lang)
|
||||
localizerWeb.Store(localizer)
|
||||
|
||||
c.Set("localizer", LocalizerWeb)
|
||||
c.Set("localizer", localizer)
|
||||
c.Set("I18n", I18n)
|
||||
c.Next()
|
||||
}
|
||||
@@ -216,3 +225,8 @@ func parseTranslationFiles(i18nFS fs.FS, i18nBundle *i18n.Bundle) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SwapBotLocalizer installs l as the bot localizer and returns the previous one.
|
||||
func SwapBotLocalizer(l *i18n.Localizer) *i18n.Localizer {
|
||||
return localizerBot.Swap(l)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user