1
0
mhsanaei 20 цаг өмнө
parent
commit
5408a2f82c

+ 1 - 1
config/version

@@ -1 +1 @@
-2.7.0
+2.8.0

+ 45 - 24
sub/sub.go

@@ -3,16 +3,18 @@ package sub
 import (
 import (
 	"context"
 	"context"
 	"crypto/tls"
 	"crypto/tls"
+	"html/template"
 	"io"
 	"io"
+	"io/fs"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"strconv"
 	"strconv"
 
 
-	"x-ui/config"
 	"x-ui/logger"
 	"x-ui/logger"
 	"x-ui/util/common"
 	"x-ui/util/common"
+	webpkg "x-ui/web"
 	"x-ui/web/locale"
 	"x-ui/web/locale"
 	"x-ui/web/middleware"
 	"x-ui/web/middleware"
 	"x-ui/web/network"
 	"x-ui/web/network"
@@ -21,6 +23,21 @@ import (
 	"github.com/gin-gonic/gin"
 	"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 {
 type Server struct {
 	httpServer *http.Server
 	httpServer *http.Server
 	listener   net.Listener
 	listener   net.Listener
@@ -41,13 +58,10 @@ func NewServer() *Server {
 }
 }
 
 
 func (s *Server) initRouter() (*gin.Engine, error) {
 func (s *Server) initRouter() (*gin.Engine, error) {
-	if config.IsDebug() {
-		gin.SetMode(gin.DebugMode)
-	} else {
-		gin.DefaultWriter = io.Discard
-		gin.DefaultErrorWriter = io.Discard
-		gin.SetMode(gin.ReleaseMode)
-	}
+	// Always run in release mode for the subscription server
+	gin.DefaultWriter = io.Discard
+	gin.DefaultErrorWriter = io.Discard
+	gin.SetMode(gin.ReleaseMode)
 
 
 	engine := gin.Default()
 	engine := gin.Default()
 
 
@@ -120,28 +134,35 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 		SubTitle = ""
 		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
 	// set per-request localizer from headers/cookies
 	engine.Use(locale.LocalizerMiddleware())
 	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...)
+	// 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)
 		}
 		}
-		engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
-		engine.LoadHTMLFiles(files...)
 	}
 	}
 
 
-	// serve assets from web/assets to use shared JS/CSS like other pages
-	engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
+	// 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("/")
 	g := engine.Group("/")
 
 

+ 1 - 0
web/html/subscription.html

@@ -4,6 +4,7 @@
 <script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
 <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/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/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>
 <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
 {{ template "page/head_end" .}}
 {{ template "page/head_end" .}}
 
 

+ 37 - 19
web/locale/locale.go

@@ -3,6 +3,7 @@ package locale
 import (
 import (
 	"embed"
 	"embed"
 	"io/fs"
 	"io/fs"
+	"os"
 	"strings"
 	"strings"
 
 
 	"x-ui/logger"
 	"x-ui/logger"
@@ -48,22 +49,6 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
 	return nil
 	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 {
 func createTemplateData(params []string, seperator ...string) map[string]any {
 	var sep string = "=="
 	var sep string = "=="
 	if len(seperator) > 0 {
 	if len(seperator) > 0 {
@@ -94,6 +79,11 @@ func I18n(i18nType I18nType, key string, params ...string) string {
 
 
 	templateData := createTemplateData(params)
 	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{
 	msg, err := localizer.Localize(&i18n.LocalizeConfig{
 		MessageID:    key,
 		MessageID:    key,
 		TemplateData: templateData,
 		TemplateData: templateData,
@@ -118,6 +108,15 @@ func initTGBotLocalizer(settingService SettingService) error {
 
 
 func LocalizerMiddleware() gin.HandlerFunc {
 func LocalizerMiddleware() gin.HandlerFunc {
 	return func(c *gin.Context) {
 	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
 		var lang string
 
 
 		if cookie, err := c.Request.Cookie("lang"); err == nil {
 		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 {
 		func(path string, d fs.DirEntry, err error) error {
 			if err != nil {
 			if err != nil {
 				return err
 				return err
@@ -145,7 +163,7 @@ func parseTranslationFiles(fsys fs.FS, i18nBundle *i18n.Bundle) error {
 				return nil
 				return nil
 			}
 			}
 
 
-			data, err := fs.ReadFile(fsys, path)
+			data, err := i18nFS.ReadFile(path)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}

+ 9 - 0
web/web.go

@@ -78,6 +78,15 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
 	return startTime
 	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 {
 type Server struct {
 	httpServer *http.Server
 	httpServer *http.Server
 	listener   net.Listener
 	listener   net.Listener