mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
- Update go.mod module path from mhsanaei/3x-ui/v3 to saeederamy/3x-ui/v3 - Update all 73 Go files' import paths accordingly - Fix README.fa_IR.md install command to point to fork's main branch The fork was referencing the original repo's module path in go.mod and all Go source imports, making it dependent on MHSanaei's namespace at build time. https://claude.ai/code/session_01M6d5atbWjuLTj6UwRHoK5m
194 lines
5 KiB
Go
194 lines
5 KiB
Go
// Package locale provides internationalization (i18n) support for the 3x-ui web panel,
|
|
// including translation loading, localization, and middleware for web and bot interfaces.
|
|
package locale
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/saeederamy/3x-ui/v3/logger"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
var (
|
|
i18nBundle *i18n.Bundle
|
|
LocalizerWeb *i18n.Localizer
|
|
LocalizerBot *i18n.Localizer
|
|
)
|
|
|
|
// I18nType represents the type of interface for internationalization.
|
|
type I18nType string
|
|
|
|
const (
|
|
Bot I18nType = "bot" // Bot interface type
|
|
Web I18nType = "web" // Web interface type
|
|
)
|
|
|
|
// SettingService interface defines methods for accessing locale settings.
|
|
type SettingService interface {
|
|
GetTgLang() (string, error)
|
|
}
|
|
|
|
// InitLocalizer initializes the internationalization system with embedded translation files.
|
|
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
|
// set default bundle to English
|
|
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
|
i18nBundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
|
|
|
// parse files
|
|
if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
|
|
return err
|
|
}
|
|
|
|
// setup bot locale
|
|
if err := initTGBotLocalizer(settingService); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createTemplateData creates a template data map from parameters with optional separator.
|
|
func createTemplateData(params []string, separator ...string) map[string]any {
|
|
var sep string = "=="
|
|
if len(separator) > 0 {
|
|
sep = separator[0]
|
|
}
|
|
|
|
templateData := make(map[string]any)
|
|
for _, param := range params {
|
|
parts := strings.SplitN(param, sep, 2)
|
|
templateData[parts[0]] = parts[1]
|
|
}
|
|
|
|
return templateData
|
|
}
|
|
|
|
// I18n retrieves a localized message for the given key and type.
|
|
// It supports both bot and web contexts, with optional template parameters.
|
|
// Returns the localized message or an empty string if localization fails.
|
|
func I18n(i18nType I18nType, key string, params ...string) string {
|
|
var localizer *i18n.Localizer
|
|
|
|
switch i18nType {
|
|
case "bot":
|
|
localizer = LocalizerBot
|
|
case "web":
|
|
localizer = LocalizerWeb
|
|
default:
|
|
logger.Errorf("Invalid type for I18n: %s", i18nType)
|
|
return ""
|
|
}
|
|
|
|
templateData := createTemplateData(params)
|
|
|
|
if localizer == nil {
|
|
// Fallback to key if localizer not ready; prevents nil panic on pages like sub
|
|
return key
|
|
}
|
|
|
|
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
|
MessageID: key,
|
|
TemplateData: templateData,
|
|
})
|
|
if err != nil {
|
|
logger.Errorf("Failed to localize message: %v", err)
|
|
return ""
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
// initTGBotLocalizer initializes the bot localizer with the configured language.
|
|
func initTGBotLocalizer(settingService SettingService) error {
|
|
botLang, err := settingService.GetTgLang()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
|
|
return nil
|
|
}
|
|
|
|
// LocalizerMiddleware returns a Gin middleware that sets up localization for web requests.
|
|
// It determines the user's language from cookies or Accept-Language header,
|
|
// creates a localizer instance, and stores it in the Gin context for use in handlers.
|
|
// Also provides the I18n function in the context for template rendering.
|
|
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)
|
|
// Try lazy-load from disk when running sub server without InitLocalizer
|
|
if err := loadTranslationsFromDisk(i18nBundle); err != nil {
|
|
logger.Warning("i18n lazy load failed:", err)
|
|
}
|
|
}
|
|
var lang string
|
|
|
|
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
|
lang = cookie.Value
|
|
} else {
|
|
lang = c.GetHeader("Accept-Language")
|
|
}
|
|
|
|
LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
|
|
|
|
c.Set("localizer", LocalizerWeb)
|
|
c.Set("I18n", I18n)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// loadTranslationsFromDisk attempts to load translation files from "web/translation" using the local filesystem.
|
|
func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
|
|
root := os.DirFS("web")
|
|
return fs.WalkDir(root, "translation", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
data, err := fs.ReadFile(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = bundle.ParseMessageFileBytes(data, path)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// parseTranslationFiles parses embedded translation files and adds them to the i18n bundle.
|
|
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
|
err := fs.WalkDir(i18nFS, "translation",
|
|
func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
data, err := i18nFS.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = i18nBundle.ParseMessageFileBytes(data, path)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|