mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-20 00:43:03 +00:00
v2.8.0
This commit is contained in:
parent
c8d71ea748
commit
5408a2f82c
5 changed files with 93 additions and 44 deletions
|
@ -1 +1 @@
|
|||
2.7.0
|
||||
2.8.0
|
51
sub/sub.go
51
sub/sub.go
|
@ -3,16 +3,18 @@ package sub
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
webpkg "x-ui/web"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
|
@ -21,6 +23,21 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||
webpkg.EmbeddedHTML(),
|
||||
"html/common/page.html",
|
||||
"html/component/aThemeSwitch.html",
|
||||
"html/subscription.html",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
@ -41,13 +58,10 @@ func NewServer() *Server {
|
|||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
// Always run in release mode for the subscription server
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
|
@ -120,28 +134,35 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
SubTitle = ""
|
||||
}
|
||||
|
||||
// init i18n for sub server using disk FS so templates can use {{ i18n }}
|
||||
// Root FS is project root; translation files are under web/translation
|
||||
if err := locale.InitLocalizerFS(os.DirFS("web"), &s.settingService); err != nil {
|
||||
logger.Warning("sub: i18n init failed:", err)
|
||||
}
|
||||
// set per-request localizer from headers/cookies
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// load HTML templates needed for subscription page (common layout + page + component + subscription)
|
||||
if files, err := s.getHtmlFiles(); err != nil {
|
||||
logger.Warning("sub: getHtmlFiles failed:", err)
|
||||
} else {
|
||||
// register i18n function similar to web server
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
|
||||
|
||||
// Templates: prefer embedded; fallback to disk if necessary
|
||||
if err := setEmbeddedTemplates(engine); err != nil {
|
||||
logger.Warning("sub: failed to parse embedded templates:", err)
|
||||
if files, derr := s.getHtmlFiles(); derr == nil {
|
||||
engine.LoadHTMLFiles(files...)
|
||||
} else {
|
||||
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
|
||||
}
|
||||
}
|
||||
|
||||
// serve assets from web/assets to use shared JS/CSS like other pages
|
||||
// Assets: use disk if present, fallback to embedded
|
||||
if _, err := os.Stat("web/assets"); err == nil {
|
||||
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
||||
} else {
|
||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||
engine.StaticFS("/assets", http.FS(subFS))
|
||||
} else {
|
||||
logger.Error("sub: failed to mount embedded assets:", err)
|
||||
}
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package locale
|
|||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"x-ui/logger"
|
||||
|
@ -48,22 +49,6 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// InitLocalizerFS allows initializing i18n from any fs.FS (e.g., disk), rooted at a directory containing a "translation" folder
|
||||
func InitLocalizerFS(fsys fs.FS, settingService SettingService) error {
|
||||
// set default bundle to english
|
||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
if err := parseTranslationFiles(fsys, i18nBundle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initTGBotLocalizer(settingService); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTemplateData(params []string, seperator ...string) map[string]any {
|
||||
var sep string = "=="
|
||||
if len(seperator) > 0 {
|
||||
|
@ -94,6 +79,11 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
|||
|
||||
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,
|
||||
|
@ -118,6 +108,15 @@ 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("toml", toml.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 {
|
||||
|
@ -134,8 +133,27 @@ func LocalizerMiddleware() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func parseTranslationFiles(fsys fs.FS, i18nBundle *i18n.Bundle) error {
|
||||
err := fs.WalkDir(fsys, "translation",
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -145,7 +163,7 @@ func parseTranslationFiles(fsys fs.FS, i18nBundle *i18n.Bundle) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
data, err := i18nFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -78,6 +78,15 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
|
|||
return startTime
|
||||
}
|
||||
|
||||
// Expose embedded resources for reuse by other servers (e.g., sub server)
|
||||
func EmbeddedHTML() embed.FS {
|
||||
return htmlFS
|
||||
}
|
||||
|
||||
func EmbeddedAssets() embed.FS {
|
||||
return assetsFS
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
|
Loading…
Reference in a new issue