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:
MHSanaei
2026-10-31 09:11:54 +00:00
parent e3736564a3
commit f1e1b0fb0c
7 changed files with 89 additions and 32 deletions
+2 -3
View File
@@ -636,9 +636,8 @@ func localizeWithRealBundle(t *testing.T) {
if _, err := bundle.ParseMessageFileBytes(data, path); err != nil {
t.Fatalf("parse %s: %v", path, err)
}
orig := locale.LocalizerBot
locale.LocalizerBot = i18n.NewLocalizer(bundle, "en-US")
t.Cleanup(func() { locale.LocalizerBot = orig })
orig := locale.SwapBotLocalizer(i18n.NewLocalizer(bundle, "en-US"))
t.Cleanup(func() { locale.SwapBotLocalizer(orig) })
}
// seeding a client bound to a Telegram user, which the client screens key on
@@ -121,9 +121,8 @@ func broadcastLocalizer(t *testing.T) {
&i18n.Message{ID: "tgbot.messages.broadcastCanceled", Other: "canceled"},
&i18n.Message{ID: "tgbot.messages.broadcastUnreachable", Other: "ℹ️ {{ .Count }} recipients cannot be messaged — ask them to press Start."},
)
orig := locale.LocalizerBot
t.Cleanup(func() { locale.LocalizerBot = orig })
locale.LocalizerBot = i18n.NewLocalizer(bundle, "en-US")
orig := locale.SwapBotLocalizer(i18n.NewLocalizer(bundle, "en-US"))
t.Cleanup(func() { locale.SwapBotLocalizer(orig) })
}
func createBroadcastInbound(t *testing.T, tag, settings string) {
@@ -27,9 +27,8 @@ func clientInfoLocalizer(t *testing.T) {
&i18n.Message{ID: "tgbot.messages.expire", Other: "Expire Date: {{ .Time }}\r\n"},
&i18n.Message{ID: "tgbot.wentWrong", Other: "went wrong"},
)
orig := locale.LocalizerBot
t.Cleanup(func() { locale.LocalizerBot = orig })
locale.LocalizerBot = i18n.NewLocalizer(bundle, "en-US")
orig := locale.SwapBotLocalizer(i18n.NewLocalizer(bundle, "en-US"))
t.Cleanup(func() { locale.SwapBotLocalizer(orig) })
}
// Regression test: a start-after-first-use client is stored as a negative duration,
@@ -52,9 +52,8 @@ func draftLocalizer(t *testing.T, msgs ...*i18n.Message) {
bundle := i18n.NewBundle(language.MustParse("en-US"))
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
_ = bundle.AddMessages(language.MustParse("en-US"), msgs...)
orig := locale.LocalizerBot
t.Cleanup(func() { locale.LocalizerBot = orig })
locale.LocalizerBot = i18n.NewLocalizer(bundle, "en-US")
orig := locale.SwapBotLocalizer(i18n.NewLocalizer(bundle, "en-US"))
t.Cleanup(func() { locale.SwapBotLocalizer(orig) })
}
// Regression test: the wizard's own prompts are HTML-parsed as well, so the
@@ -291,9 +291,8 @@ func TestNotifyExhaustedLooksUpEachIDOnce(t *testing.T) {
func TestServerUsageInfoShowsUnavailableUptime(t *testing.T) {
bundle := i18n.NewBundle(language.English)
_ = bundle.AddMessages(language.English, &i18n.Message{ID: "tgbot.messages.serverUpTime", Other: "Uptime: {{.UpTime}} {{.Unit}}\n"}, &i18n.Message{ID: "tgbot.days", Other: "days"})
oldLocale := locale.LocalizerBot
locale.LocalizerBot = i18n.NewLocalizer(bundle, "en")
t.Cleanup(func() { locale.LocalizerBot = oldLocale })
oldLocale := locale.SwapBotLocalizer(i18n.NewLocalizer(bundle, "en"))
t.Cleanup(func() { locale.SwapBotLocalizer(oldLocale) })
statusCache.mutex.Lock()
oldStatus, oldTime := statusCache.data, statusCache.timestamp
statusCache.mutex.Unlock()