From 22de983752fb4cc10bd16b756c4ccaab239e2e3b Mon Sep 17 00:00:00 2001 From: pwnnex Date: Tue, 28 Apr 2026 18:49:39 +0300 Subject: [PATCH 1/3] xray-setting: pin api routing rule to index 0 on save (#4124) when the admin adds a custom outbound (eg vless cascade to a second server) and a routing rule sending all inbound traffic to it, that catch-all gets evaluated before the existing api->api rule, so the panel's internal stats inbound's traffic ends up on the cascade outbound. the grpc stats query then can't see anything, GetTraffic returns no inbound/user counters, and every client appears offline with zero traffic even though the actual proxy path works fine. before save, find the api rule and move it to the front of routing.rules. if it's missing entirely, insert a default. other rules keep their relative order. closes #4113. probably also fixes the long-standing #2818 where the documented workaround was "manually move the api rule to the top". --- web/service/xray_setting.go | 120 +++++++++++++++++++++++++++++++ web/service/xray_setting_test.go | 115 +++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/web/service/xray_setting.go b/web/service/xray_setting.go index 4c3892e4..da77404a 100644 --- a/web/service/xray_setting.go +++ b/web/service/xray_setting.go @@ -24,6 +24,9 @@ func (s *XraySettingService) SaveXraySetting(newXraySettings string) error { if err := s.CheckXrayConfig(newXraySettings); err != nil { return err } + if hoisted, err := EnsureStatsRouting(newXraySettings); err == nil { + newXraySettings = hoisted + } return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings) } @@ -83,3 +86,120 @@ func UnwrapXrayTemplateConfig(raw string) string { } return raw } + +// EnsureStatsRouting hoists the `api -> api` routing rule to the front +// of routing.rules so the stats query path is never starved by a +// catch-all rule the admin may have added or reordered above it. +// +// Why this matters (#4113, #2818): an admin who adds a cascade outbound +// (e.g. vless to another server) and a routing rule sending all inbound +// traffic to it ends up sending the internal stats inbound's traffic to +// that cascade too, since rules are evaluated top-to-bottom and the +// catch-all matches first. The panel's gRPC stats query then can't reach +// the running xray instance, GetTraffic returns nothing, and every +// client appears offline with zero traffic even though the actual proxy +// path works fine. +// +// The api inbound is special-cased internal infrastructure for the +// panel, not something the admin should ever route to a real outbound. +// Keeping its rule pinned at index 0 is the only correct configuration. +// +// If the api rule is already at index 0 the input is returned unchanged. +// If it exists somewhere else it is moved. If it is missing entirely a +// default rule (`type=field, inboundTag=[api], outboundTag=api`) is +// inserted at the front. Other routing entries keep their relative order. +func EnsureStatsRouting(raw string) (string, error) { + var cfg map[string]json.RawMessage + if err := json.Unmarshal([]byte(raw), &cfg); err != nil { + return raw, err + } + + var routing map[string]json.RawMessage + if r, ok := cfg["routing"]; ok && len(r) > 0 { + if err := json.Unmarshal(r, &routing); err != nil { + return raw, err + } + } + if routing == nil { + routing = make(map[string]json.RawMessage) + } + + var rules []map[string]any + if r, ok := routing["rules"]; ok && len(r) > 0 { + if err := json.Unmarshal(r, &rules); err != nil { + return raw, err + } + } + + apiIdx := findApiRule(rules) + if apiIdx == 0 { + return raw, nil // already correct, don't churn the JSON + } + + var apiRule map[string]any + if apiIdx > 0 { + apiRule = rules[apiIdx] + rules = append(rules[:apiIdx], rules[apiIdx+1:]...) + } else { + apiRule = map[string]any{ + "type": "field", + "inboundTag": []string{"api"}, + "outboundTag": "api", + } + } + rules = append([]map[string]any{apiRule}, rules...) + + rulesJSON, err := json.Marshal(rules) + if err != nil { + return raw, err + } + routing["rules"] = rulesJSON + + routingJSON, err := json.Marshal(routing) + if err != nil { + return raw, err + } + cfg["routing"] = routingJSON + + out, err := json.Marshal(cfg) + if err != nil { + return raw, err + } + return string(out), nil +} + +// findApiRule returns the index of the routing rule that targets the +// internal api inbound (inboundTag contains "api" and outboundTag is +// "api"), or -1 if no such rule exists. +func findApiRule(rules []map[string]any) int { + for i, rule := range rules { + if outTag, _ := rule["outboundTag"].(string); outTag != "api" { + continue + } + raw, ok := rule["inboundTag"] + if !ok { + continue + } + // inboundTag is usually []string but can come as []any from a + // roundtrip through map[string]any. Accept both shapes. + switch tags := raw.(type) { + case []any: + for _, t := range tags { + if s, ok := t.(string); ok && s == "api" { + return i + } + } + case []string: + for _, s := range tags { + if s == "api" { + return i + } + } + case string: + if tags == "api" { + return i + } + } + } + return -1 +} diff --git a/web/service/xray_setting_test.go b/web/service/xray_setting_test.go index 2c165576..22b00ce3 100644 --- a/web/service/xray_setting_test.go +++ b/web/service/xray_setting_test.go @@ -88,3 +88,118 @@ func equalJSON(t *testing.T, a, b string) bool { jb, _ := json.Marshal(vb) return string(ja) == string(jb) } + +// firstRuleOutbound parses the (post-hoisted) config and returns +// routing.rules[0].outboundTag, or "" if anything is missing. +func firstRuleOutbound(t *testing.T, raw string) string { + t.Helper() + var cfg map[string]any + if err := json.Unmarshal([]byte(raw), &cfg); err != nil { + t.Fatalf("unmarshal cfg: %v", err) + } + routing, _ := cfg["routing"].(map[string]any) + rules, _ := routing["rules"].([]any) + if len(rules) == 0 { + return "" + } + first, _ := rules[0].(map[string]any) + tag, _ := first["outboundTag"].(string) + return tag +} + +func TestEnsureStatsRouting_HoistsApiRuleFromMiddle(t *testing.T) { + // #4113 repro shape: admin added a cascade outbound and put a + // catch-all routing rule above the api rule. stats query path + // gets starved by the catch-all unless we hoist the api rule. + in := `{ + "routing": { + "rules": [ + {"type":"field","inboundTag":["inbound-vless"],"outboundTag":"vless-cascade"}, + {"type":"field","inboundTag":["api"],"outboundTag":"api"}, + {"type":"field","outboundTag":"blocked","ip":["geoip:private"]} + ] + } + }` + out, err := EnsureStatsRouting(in) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if got := firstRuleOutbound(t, out); got != "api" { + t.Fatalf("api rule should be at index 0 after hoist, got first outboundTag = %q\nfull: %s", got, out) + } +} + +func TestEnsureStatsRouting_NoOpWhenAlreadyFirst(t *testing.T) { + // Don't churn the JSON when nothing needs fixing — same string in, + // same string out. Lets the diff in the panel UI stay quiet for + // well-formed configs. + in := `{"routing":{"rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"},{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}}` + out, err := EnsureStatsRouting(in) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if out != in { + t.Fatalf("expected unchanged input, got: %s", out) + } +} + +func TestEnsureStatsRouting_InsertsDefaultWhenMissing(t *testing.T) { + // Some admins delete the api rule by accident. Re-add a default + // at the front so stats keep working after the next save. + in := `{"routing":{"rules":[{"type":"field","outboundTag":"vless-cascade","inboundTag":["inbound-vless"]}]}}` + out, err := EnsureStatsRouting(in) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if got := firstRuleOutbound(t, out); got != "api" { + t.Fatalf("default api rule should be inserted at index 0, got %q\nfull: %s", got, out) + } + // The original rule should still be there, just shifted. + var cfg map[string]any + json.Unmarshal([]byte(out), &cfg) + rules := cfg["routing"].(map[string]any)["rules"].([]any) + if len(rules) != 2 { + t.Fatalf("expected 2 rules after insert, got %d: %v", len(rules), rules) + } +} + +func TestEnsureStatsRouting_NoRoutingBlock(t *testing.T) { + // Pathological but possible: empty config or one without a routing + // section. Don't crash, and create the section with the api rule. + in := `{"log":{}}` + out, err := EnsureStatsRouting(in) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if got := firstRuleOutbound(t, out); got != "api" { + t.Fatalf("api rule should be created when routing was missing, got %q\nfull: %s", got, out) + } +} + +func TestEnsureStatsRouting_InvalidJsonReturnsAsIs(t *testing.T) { + // SaveXraySetting calls CheckXrayConfig before this helper, so + // invalid JSON shouldn't reach us in practice — but be defensive + // about garbage in (return same garbage out plus an error) so the + // caller can choose to skip the hoist instead of corrupting input. + in := "definitely not json" + out, err := EnsureStatsRouting(in) + if err == nil { + t.Fatalf("expected error for invalid json, got none") + } + if out != in { + t.Fatalf("expected raw passthrough on error, got %q", out) + } +} + +func TestEnsureStatsRouting_AcceptsInboundTagAsString(t *testing.T) { + // Some manually-edited configs use a single string instead of an + // array for inboundTag. Make sure we still recognize the api rule. + in := `{"routing":{"rules":[{"type":"field","inboundTag":["other"],"outboundTag":"vless-cascade"},{"type":"field","inboundTag":"api","outboundTag":"api"}]}}` + out, err := EnsureStatsRouting(in) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if got := firstRuleOutbound(t, out); got != "api" { + t.Fatalf("api rule with string-form inboundTag should hoist to front, got %q\nfull: %s", got, out) + } +} From f21ed9229695ed1cbcc86d65b52d98cfc9116f5a Mon Sep 17 00:00:00 2001 From: "Farhad H. P. Shirvan" <9374298+farhadh@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:46:55 +0200 Subject: [PATCH 2/3] feat: add panel update functionality via web GUI (#4117) * feat: add panel update functionality via web GUI * feat: enhance panel update notifications in web GUI * feat: implement panel update modal and enhance translation strings * fix design --- web/controller/server.go | 19 +++ web/html/index.html | 113 +++++++++++++++-- web/service/panel.go | 174 +++++++++++++++++++++++++++ web/service/panel_other.go | 7 ++ web/service/panel_test.go | 41 +++++++ web/service/panel_unix.go | 12 ++ web/translation/translate.en_US.toml | 11 ++ 7 files changed, 367 insertions(+), 10 deletions(-) create mode 100644 web/service/panel_other.go create mode 100644 web/service/panel_test.go create mode 100644 web/service/panel_unix.go diff --git a/web/controller/server.go b/web/controller/server.go index d32209e1..188e987a 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -22,6 +22,7 @@ type ServerController struct { serverService service.ServerService settingService service.SettingService + panelService service.PanelService lastStatus *service.Status @@ -43,6 +44,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.GET("/status", a.status) g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.GET("/getXrayVersion", a.getXrayVersion) + g.GET("/getPanelUpdateInfo", a.getPanelUpdateInfo) g.GET("/getConfigJson", a.getConfigJson) g.GET("/getDb", a.getDb) g.GET("/getNewUUID", a.getNewUUID) @@ -54,6 +56,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.POST("/stopXrayService", a.stopXrayService) g.POST("/restartXrayService", a.restartXrayService) g.POST("/installXray/:version", a.installXray) + g.POST("/updatePanel", a.updatePanel) g.POST("/updateGeofile", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/logs/:count", a.getLogs) @@ -131,6 +134,16 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { jsonObj(c, versions, nil) } +// getPanelUpdateInfo retrieves the current and latest panel version. +func (a *ServerController) getPanelUpdateInfo(c *gin.Context) { + info, err := a.panelService.GetUpdateInfo() + if err != nil { + jsonMsg(c, I18nWeb(c, "pages.index.panelUpdateCheckPopover"), err) + return + } + jsonObj(c, info, nil) +} + // installXray installs or updates Xray to the specified version. func (a *ServerController) installXray(c *gin.Context) { version := c.Param("version") @@ -138,6 +151,12 @@ func (a *ServerController) installXray(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err) } +// updatePanel starts a panel self-update to the latest release. +func (a *ServerController) updatePanel(c *gin.Context) { + err := a.panelService.StartUpdate() + jsonMsg(c, I18nWeb(c, "pages.index.panelUpdateStartedPopover"), err) +} + // updateGeofile updates the specified geo file for Xray. func (a *ServerController) updateGeofile(c *gin.Context) { fileName := c.Param("fileName") diff --git a/web/html/index.html b/web/html/index.html index 0a36b9cb..62e9453b 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -139,7 +139,7 @@ {{ i18n "pages.index.restartXray" }} - + [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n @@ -169,6 +169,14 @@ + v{{ .cur_ver }} @@ -317,9 +325,36 @@ - + + + + {{ i18n "pages.index.currentPanelVersion" }} + v[[ panelUpdateModal.info.currentVersion || '{{ .cur_ver }}' ]] + + + {{ i18n "pages.index.latestPanelVersion" }} + + [[ panelUpdateModal.info.latestVersion || '-' ]] + + + + {{ i18n "pages.index.panelUpToDate" }} + {{ i18n "pages.index.upToDate" }} + + +
+ + {{ i18n "pages.index.updatePanel" }} + +
+
+ - + @@ -799,9 +834,11 @@ const versionModal = { visible: false, + activeKey: '1', versions: [], - show(versions) { + show(versions, activeKey = '1') { this.visible = true; + this.activeKey = activeKey; this.versions = versions; }, hide() { @@ -809,6 +846,24 @@ }, }; + const panelUpdateModal = { + visible: false, + info: { + currentVersion: '{{ .cur_ver }}', + latestVersion: '', + updateAvailable: false, + }, + show(info) { + this.visible = true; + if (info) { + this.info = info; + } + }, + hide() { + this.visible = false; + }, + }; + const logModal = { visible: false, logs: [], @@ -958,11 +1013,12 @@ spinning: false }, status: new Status(), - cpuHistory: [], // small live widget history - cpuHistoryLong: [], // aggregated points from backend - cpuHistoryLabels: [], - cpuHistoryModal: { visible: false, bucket: 2 }, + cpuHistory: [], // small live widget history + cpuHistoryLong: [], // aggregated points from backend + cpuHistoryLabels: [], + cpuHistoryModal: { visible: false, bucket: 2 }, versionModal, + panelUpdateModal, logModal, xraylogModal, backupModal, @@ -1049,16 +1105,25 @@ console.error('Failed to fetch bucketed cpu history', e) } }, - async openSelectV2rayVersion() { + async openSelectV2rayVersion(activeKey = '1') { this.loading(true); const msg = await HttpUtil.get('/panel/api/server/getXrayVersion'); this.loading(false); if (!msg.success) { return; } - versionModal.show(msg.obj); + versionModal.show(msg.obj, activeKey); this.loadCustomGeo(); }, + async openPanelUpdate() { + this.loading(true); + const msg = await HttpUtil.get('/panel/api/server/getPanelUpdateInfo'); + this.loading(false); + if (!msg.success) { + return; + } + panelUpdateModal.show(msg.obj); + }, customGeoFormatTime(ts) { if (!ts) return ''; return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts); @@ -1195,6 +1260,27 @@ }, }); }, + updatePanel() { + this.$confirm({ + title: '{{ i18n "pages.index.panelUpdateDialog" }}', + content: '{{ i18n "pages.index.panelUpdateDialogDesc" }}' + .replace('#version#', panelUpdateModal.info.latestVersion || ''), + okText: '{{ i18n "confirm"}}', + class: themeSwitcher.currentTheme, + cancelText: '{{ i18n "cancel"}}', + onOk: async () => { + panelUpdateModal.hide(); + this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); + const msg = await HttpUtil.post('/panel/api/server/updatePanel'); + if (!msg.success) { + this.loading(false); + return; + } + await PromiseUtil.sleep(15000); + window.location.reload(); + }, + }); + }, updateGeofile(fileName) { const isSingleFile = !!fileName; this.$confirm({ @@ -1346,6 +1432,13 @@ // Initial status fetch await this.getStatus(); + // Silently check for panel updates so the indicator shows on load + HttpUtil.get('/panel/api/server/getPanelUpdateInfo').then(msg => { + if (msg && msg.success && msg.obj) { + panelUpdateModal.info = msg.obj; + } + }); + // Setup WebSocket for real-time updates if (window.wsClient) { window.wsClient.connect(); diff --git a/web/service/panel.go b/web/service/panel.go index e4fb0c68..75f2f155 100644 --- a/web/service/panel.go +++ b/web/service/panel.go @@ -1,10 +1,19 @@ package service import ( + "encoding/json" + "fmt" + "net/http" "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" "syscall" "time" + "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/logger" ) @@ -12,6 +21,13 @@ import ( // It handles panel restart, updates, and system-level panel controls. type PanelService struct{} +// PanelUpdateInfo contains the current and latest available panel versions. +type PanelUpdateInfo struct { + CurrentVersion string `json:"currentVersion"` + LatestVersion string `json:"latestVersion"` + UpdateAvailable bool `json:"updateAvailable"` +} + func (s *PanelService) RestartPanel(delay time.Duration) error { p, err := os.FindProcess(syscall.Getpid()) if err != nil { @@ -26,3 +42,161 @@ func (s *PanelService) RestartPanel(delay time.Duration) error { }() return nil } + +// GetUpdateInfo checks GitHub for the latest 3x-ui release. +func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) { + latest, err := fetchLatestPanelVersion() + if err != nil { + return nil, err + } + current := config.GetVersion() + return &PanelUpdateInfo{ + CurrentVersion: current, + LatestVersion: latest, + UpdateAvailable: isNewerVersion(latest, current), + }, nil +} + +// StartUpdate starts the official updater outside of the current web request. +func (s *PanelService) StartUpdate() error { + if runtime.GOOS != "linux" { + return fmt.Errorf("panel web update is supported only on Linux installations") + } + + bash, err := exec.LookPath("bash") + if err != nil { + return fmt.Errorf("bash is required to run the panel updater: %w", err) + } + curl, err := exec.LookPath("curl") + if err != nil { + return fmt.Errorf("curl is required to download the panel updater: %w", err) + } + + mainFolder, serviceFolder := resolveUpdateFolders() + updateScript := fmt.Sprintf("set -o pipefail; %s -fLs https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh | %s", shellQuote(curl), shellQuote(bash)) + + if systemdRun, err := exec.LookPath("systemd-run"); err == nil { + unitName := fmt.Sprintf("x-ui-web-update-%d", time.Now().Unix()) + cmd := exec.Command(systemdRun, + "--unit", unitName, + "--setenv", "XUI_MAIN_FOLDER="+mainFolder, + "--setenv", "XUI_SERVICE="+serviceFolder, + bash, "-lc", updateScript, + ) + out, err := cmd.CombinedOutput() + if err != nil { + output := strings.TrimSpace(string(out)) + if !strings.Contains(output, "System has not been booted with systemd") && + !strings.Contains(output, "Failed to connect to bus") { + return fmt.Errorf("failed to start panel update job: %w: %s", err, output) + } + logger.Warning("systemd-run is unavailable, falling back to detached update process:", output) + } else { + logger.Infof("started panel update job via systemd-run unit %s", unitName) + return nil + } + } + + cmd := exec.Command(bash, "-lc", updateScript) + cmd.Env = append(os.Environ(), + "XUI_MAIN_FOLDER="+mainFolder, + "XUI_SERVICE="+serviceFolder, + ) + setDetachedProcess(cmd) + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start panel update job: %w", err) + } + if err := cmd.Process.Release(); err != nil { + logger.Warning("failed to release panel update process:", err) + } + logger.Infof("started panel update job with pid %d", cmd.Process.Pid) + return nil +} + +func fetchLatestPanelVersion() (string, error) { + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get("https://api.github.com/repos/MHSanaei/3x-ui/releases/latest") + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status) + } + + var release Release + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return "", err + } + if release.TagName == "" { + return "", fmt.Errorf("latest panel release tag is empty") + } + return release.TagName, nil +} + +func resolveUpdateFolders() (string, string) { + mainFolder := os.Getenv("XUI_MAIN_FOLDER") + if mainFolder == "" { + if exePath, err := os.Executable(); err == nil { + mainFolder = filepath.Dir(exePath) + } + } + if mainFolder == "" { + mainFolder = "/usr/local/x-ui" + } + + serviceFolder := os.Getenv("XUI_SERVICE") + if serviceFolder == "" { + serviceFolder = "/etc/systemd/system" + } + return mainFolder, serviceFolder +} + +func isNewerVersion(latest string, current string) bool { + cmp, ok := compareVersionStrings(latest, current) + if !ok { + return normalizeVersionTag(latest) != normalizeVersionTag(current) + } + return cmp > 0 +} + +func compareVersionStrings(a string, b string) (int, bool) { + aParts, okA := parseVersionParts(a) + bParts, okB := parseVersionParts(b) + if !okA || !okB { + return 0, false + } + for i := 0; i < len(aParts); i++ { + if aParts[i] > bParts[i] { + return 1, true + } + if aParts[i] < bParts[i] { + return -1, true + } + } + return 0, true +} + +func parseVersionParts(version string) ([3]int, bool) { + var result [3]int + parts := strings.Split(normalizeVersionTag(version), ".") + if len(parts) != 3 { + return result, false + } + for i, part := range parts { + n, err := strconv.Atoi(part) + if err != nil { + return result, false + } + result[i] = n + } + return result, true +} + +func normalizeVersionTag(version string) string { + return strings.TrimPrefix(strings.TrimSpace(version), "v") +} + +func shellQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" +} diff --git a/web/service/panel_other.go b/web/service/panel_other.go new file mode 100644 index 00000000..53295c10 --- /dev/null +++ b/web/service/panel_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package service + +import "os/exec" + +func setDetachedProcess(cmd *exec.Cmd) {} diff --git a/web/service/panel_test.go b/web/service/panel_test.go new file mode 100644 index 00000000..44e9ba34 --- /dev/null +++ b/web/service/panel_test.go @@ -0,0 +1,41 @@ +package service + +import "testing" + +func TestIsNewerVersion(t *testing.T) { + cases := []struct { + latest string + current string + want bool + }{ + {"v2.9.4", "2.9.3", true}, + {"v2.10.0", "2.9.9", true}, + {"v2.9.3", "2.9.3", false}, + {"v2.9.2", "2.9.3", false}, + {"v3.0.0", "2.9.3", true}, + } + + for _, tc := range cases { + if got := isNewerVersion(tc.latest, tc.current); got != tc.want { + t.Fatalf("isNewerVersion(%q, %q) = %v, want %v", tc.latest, tc.current, got, tc.want) + } + } +} + +func TestCompareVersionStringsRejectsUnexpectedFormats(t *testing.T) { + if _, ok := compareVersionStrings("latest", "2.9.3"); ok { + t.Fatal("expected non-semver latest tag to be rejected") + } + if _, ok := compareVersionStrings("v2.9", "2.9.3"); ok { + t.Fatal("expected short version to be rejected") + } +} + +func TestShellQuote(t *testing.T) { + if got := shellQuote("/usr/bin/curl"); got != "'/usr/bin/curl'" { + t.Fatalf("unexpected quote result: %s", got) + } + if got := shellQuote("/tmp/a'b"); got != "'/tmp/a'\\''b'" { + t.Fatalf("unexpected quote result with single quote: %s", got) + } +} diff --git a/web/service/panel_unix.go b/web/service/panel_unix.go new file mode 100644 index 00000000..13d2237c --- /dev/null +++ b/web/service/panel_unix.go @@ -0,0 +1,12 @@ +//go:build linux + +package service + +import ( + "os/exec" + "syscall" +) + +func setDetachedProcess(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} +} diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 45186187..49c9f952 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -124,8 +124,15 @@ "stopXray" = "Stop" "restartXray" = "Restart" "xraySwitch" = "Version" +"xrayUpdates" = "Xray Updates" "xraySwitchClick" = "Choose the version you want to switch to." "xraySwitchClickDesk" = "Choose carefully, as older versions may not be compatible with current configurations." +"updatePanel" = "Update Panel" +"panelUpdateDesc" = "This will update 3X-UI itself to the latest release and restart the panel service." +"currentPanelVersion" = "Current panel version" +"latestPanelVersion" = "Latest panel version" +"panelUpToDate" = "Panel is up to date" +"upToDate" = "Up to date" "xrayStatusUnknown" = "Unknown" "xrayStatusRunning" = "Running" "xrayStatusStop" = "Stop" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Do you really want to change the Xray version?" "xraySwitchVersionDialogDesc" = "This will change the Xray version to #version#." "xraySwitchVersionPopover" = "Xray updated successfully" +"panelUpdateDialog" = "Do you really want to update the panel?" +"panelUpdateDialogDesc" = "This will update 3X-UI to #version# and restart the panel service." +"panelUpdateCheckPopover" = "Panel update check failed" +"panelUpdateStartedPopover" = "Panel update started" "geofileUpdateDialog" = "Do you really want to update the geofile?" "geofileUpdateDialogDesc" = "This will update the #filename# file." "geofilesUpdateDialogDesc" = "This will update all geofiles." From 51e2fb6dbfb6f3f21b3f578c15c3dc0d47c4a66e Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 28 Apr 2026 19:17:11 +0200 Subject: [PATCH 3/3] translate update #4117 --- web/translation/translate.ar_EG.toml | 11 +++++++++++ web/translation/translate.es_ES.toml | 11 +++++++++++ web/translation/translate.fa_IR.toml | 11 +++++++++++ web/translation/translate.id_ID.toml | 11 +++++++++++ web/translation/translate.ja_JP.toml | 11 +++++++++++ web/translation/translate.pt_BR.toml | 11 +++++++++++ web/translation/translate.ru_RU.toml | 11 +++++++++++ web/translation/translate.tr_TR.toml | 11 +++++++++++ web/translation/translate.uk_UA.toml | 11 +++++++++++ web/translation/translate.vi_VN.toml | 11 +++++++++++ web/translation/translate.zh_CN.toml | 11 +++++++++++ web/translation/translate.zh_TW.toml | 11 +++++++++++ 12 files changed, 132 insertions(+) diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml index ca7076c8..2e76cf94 100644 --- a/web/translation/translate.ar_EG.toml +++ b/web/translation/translate.ar_EG.toml @@ -126,6 +126,13 @@ "xraySwitch" = "النسخة" "xraySwitchClick" = "اختار النسخة اللي عايز تتحول لها." "xraySwitchClickDesk" = "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية." +"xrayUpdates" = "تحديثات Xray" +"updatePanel" = "تحديث البانل" +"panelUpdateDesc" = "ده هيحدث 3X-UI لآخر إصدار وهيعيد تشغيل خدمة البانل." +"currentPanelVersion" = "إصدار البانل الحالي" +"latestPanelVersion" = "أحدث إصدار للبانل" +"panelUpToDate" = "البانل محدث لآخر إصدار" +"upToDate" = "محدث" "xrayStatusUnknown" = "مش معروف" "xrayStatusRunning" = "شغالة" "xrayStatusStop" = "متوقفة" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "هل تريد حقًا تغيير إصدار Xray؟" "xraySwitchVersionDialogDesc" = "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#." "xraySwitchVersionPopover" = "تم تحديث Xray بنجاح" +"panelUpdateDialog" = "هل فعلاً عايز تحدث البانل؟" +"panelUpdateDialogDesc" = "ده هيحدث 3X-UI للإصدار #version# وهيعيد تشغيل البانل." +"panelUpdateCheckPopover" = "فشل التحقق من تحديث البانل" +"panelUpdateStartedPopover" = "بدأ تحديث البانل" "geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟" "geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#." "geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات." diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index d8f94461..cce4018f 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Versión" "xraySwitchClick" = "Elige la versión a la que deseas cambiar." "xraySwitchClickDesk" = "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales." +"xrayUpdates" = "Actualizaciones de Xray" +"updatePanel" = "Actualizar panel" +"panelUpdateDesc" = "Esto actualizará 3X-UI a la última versión y reiniciará el servicio del panel." +"currentPanelVersion" = "Versión actual del panel" +"latestPanelVersion" = "Última versión del panel" +"panelUpToDate" = "El panel está actualizado" +"upToDate" = "Actualizado" "xrayStatusUnknown" = "Desconocido" "xrayStatusRunning" = "En ejecución" "xrayStatusStop" = "Detenido" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "¿Realmente deseas cambiar la versión de Xray?" "xraySwitchVersionDialogDesc" = "Esto cambiará la versión de Xray a #version#." "xraySwitchVersionPopover" = "Xray se actualizó correctamente" +"panelUpdateDialog" = "¿Deseas actualizar el panel?" +"panelUpdateDialogDesc" = "Esto actualizará 3X-UI a la versión #version# y reiniciará el servicio del panel." +"panelUpdateCheckPopover" = "Fallo al comprobar actualización del panel" +"panelUpdateStartedPopover" = "Actualización del panel iniciada" "geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?" "geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#." "geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos." diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index aaec75f6..e1f49b80 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -126,6 +126,13 @@ "xraySwitch" = "‌نسخه" "xraySwitchClick" = "نسخه مورد نظر را انتخاب کنید" "xraySwitchClickDesk" = "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد" +"xrayUpdates" = "به‌روزرسانی‌های Xray" +"updatePanel" = "به‌روزرسانی پنل" +"panelUpdateDesc" = "این عملیات 3X-UI را به آخرین نسخه به‌روزرسانی می‌کند و سرویس پنل را مجدداً راه‌اندازی می‌کند." +"currentPanelVersion" = "نسخه فعلی پنل" +"latestPanelVersion" = "آخرین نسخه پنل" +"panelUpToDate" = "پنل به‌روز است" +"upToDate" = "به‌روز" "xrayStatusUnknown" = "ناشناخته" "xrayStatusRunning" = "در حال اجرا" "xrayStatusStop" = "متوقف" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "آیا واقعاً می‌خواهید نسخه Xray را تغییر دهید؟" "xraySwitchVersionDialogDesc" = "این کار نسخه Xray را به #version# تغییر می‌دهد." "xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد" +"panelUpdateDialog" = "آیا مطمئن هستید که می‌خواهید پنل را به‌روزرسانی کنید؟" +"panelUpdateDialogDesc" = "این 3X-UI را به نسخه #version# به‌روزرسانی کرده و سرویس پنل را مجدداً راه‌اندازی می‌کند." +"panelUpdateCheckPopover" = "خطا در بررسی به‌روزرسانی پنل" +"panelUpdateStartedPopover" = "به‌روزرسانی پنل آغاز شد" "geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟" "geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند." "geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند." diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml index e115aaa8..f2ac71fc 100644 --- a/web/translation/translate.id_ID.toml +++ b/web/translation/translate.id_ID.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Versi" "xraySwitchClick" = "Pilih versi yang ingin Anda pindah." "xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini." +"xrayUpdates" = "Pembaruan Xray" +"updatePanel" = "Perbarui Panel" +"panelUpdateDesc" = "Ini akan memperbarui 3X-UI ke rilis terbaru dan me-restart layanan panel." +"currentPanelVersion" = "Versi panel saat ini" +"latestPanelVersion" = "Versi panel terbaru" +"panelUpToDate" = "Panel sudah terbaru" +"upToDate" = "Terbaru" "xrayStatusUnknown" = "Tidak diketahui" "xrayStatusRunning" = "Berjalan" "xrayStatusStop" = "Berhenti" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Apakah Anda yakin ingin mengubah versi Xray?" "xraySwitchVersionDialogDesc" = "Ini akan mengubah versi Xray ke #version#." "xraySwitchVersionPopover" = "Xray berhasil diperbarui" +"panelUpdateDialog" = "Apakah Anda benar-benar ingin memperbarui panel?" +"panelUpdateDialogDesc" = "Ini akan memperbarui 3X-UI ke #version# dan me-restart layanan panel." +"panelUpdateCheckPopover" = "Pemeriksaan pembaruan panel gagal" +"panelUpdateStartedPopover" = "Pembaruan panel dimulai" "geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?" "geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#." "geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas." diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml index ffa9168b..da67e758 100644 --- a/web/translation/translate.ja_JP.toml +++ b/web/translation/translate.ja_JP.toml @@ -126,6 +126,13 @@ "xraySwitch" = "バージョン" "xraySwitchClick" = "切り替えるバージョンを選択してください" "xraySwitchClickDesk" = "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。" +"xrayUpdates" = "Xrayの更新" +"updatePanel" = "パネルを更新" +"panelUpdateDesc" = "これにより3X-UIが最新リリースに更新され、パネルサービスが再起動されます。" +"currentPanelVersion" = "現在のパネルバージョン" +"latestPanelVersion" = "最新のパネルバージョン" +"panelUpToDate" = "パネルは最新です" +"upToDate" = "最新" "xrayStatusUnknown" = "不明" "xrayStatusRunning" = "実行中" "xrayStatusStop" = "停止" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Xrayのバージョンを本当に変更しますか?" "xraySwitchVersionDialogDesc" = "Xrayのバージョンが#version#に変更されます。" "xraySwitchVersionPopover" = "Xrayの更新が成功しました" +"panelUpdateDialog" = "本当にパネルを更新しますか?" +"panelUpdateDialogDesc" = "これにより3X-UIが#version#に更新され、パネルサービスが再起動されます。" +"panelUpdateCheckPopover" = "パネルの更新確認に失敗しました" +"panelUpdateStartedPopover" = "パネルの更新を開始しました" "geofileUpdateDialog" = "ジオファイルを本当に更新しますか?" "geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。" "geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。" diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml index 18db4d62..10a2b156 100644 --- a/web/translation/translate.pt_BR.toml +++ b/web/translation/translate.pt_BR.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Versão" "xraySwitchClick" = "Escolha a versão para a qual deseja alternar." "xraySwitchClickDesk" = "Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais." +"xrayUpdates" = "Atualizações do Xray" +"updatePanel" = "Atualizar painel" +"panelUpdateDesc" = "Isso atualizará o 3X-UI para a versão mais recente e reiniciará o serviço do painel." +"currentPanelVersion" = "Versão atual do painel" +"latestPanelVersion" = "Última versão do painel" +"panelUpToDate" = "O painel está atualizado" +"upToDate" = "Atualizado" "xrayStatusUnknown" = "Desconhecido" "xrayStatusRunning" = "Em execução" "xrayStatusStop" = "Parado" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Você realmente deseja alterar a versão do Xray?" "xraySwitchVersionDialogDesc" = "Isso mudará a versão do Xray para #version#." "xraySwitchVersionPopover" = "Xray atualizado com sucesso" +"panelUpdateDialog" = "Deseja realmente atualizar o painel?" +"panelUpdateDialogDesc" = "Isso atualizará o 3X-UI para #version# e reiniciará o serviço do painel." +"panelUpdateCheckPopover" = "Falha na verificação de atualização do painel" +"panelUpdateStartedPopover" = "Atualização do painel iniciada" "geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?" "geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#." "geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos." diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 5bf89dfd..b3ec617d 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Выбор версии" "xraySwitchClick" = "Выберите нужную версию" "xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки" +"xrayUpdates" = "Обновления Xray" +"updatePanel" = "Обновить панель" +"panelUpdateDesc" = "Это обновит 3X-UI до последнего релиза и перезапустит сервис панели." +"currentPanelVersion" = "Текущая версия панели" +"latestPanelVersion" = "Последняя версия панели" +"panelUpToDate" = "Панель обновлена" +"upToDate" = "Обновлено" "xrayStatusUnknown" = "Неизвестно" "xrayStatusRunning" = "Запущен" "xrayStatusStop" = "Остановлен" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Переключить версию Xray" "xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" "xraySwitchVersionPopover" = "Xray успешно обновлён" +"panelUpdateDialog" = "Вы действительно хотите обновить панель?" +"panelUpdateDialogDesc" = "Это обновит 3X-UI до версии #version# и перезапустит сервис панели." +"panelUpdateCheckPopover" = "Проверка обновления панели не удалась" +"panelUpdateStartedPopover" = "Обновление панели началось" "geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?" "geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofilesUpdateDialogDesc" = "Это обновит все геофайлы." diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml index 0393a7dc..5aaa1b03 100644 --- a/web/translation/translate.tr_TR.toml +++ b/web/translation/translate.tr_TR.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Sürüm" "xraySwitchClick" = "Geçiş yapmak istediğiniz sürümü seçin." "xraySwitchClickDesk" = "Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir." +"xrayUpdates" = "Xray Güncellemeleri" +"updatePanel" = "Paneli Güncelle" +"panelUpdateDesc" = "Bu, 3X-UI'yi en son sürüme güncelleyecek ve panel servisini yeniden başlatacaktır." +"currentPanelVersion" = "Mevcut panel sürümü" +"latestPanelVersion" = "Panelin en son sürümü" +"panelUpToDate" = "Panel güncel" +"upToDate" = "Güncel" "xrayStatusUnknown" = "Bilinmiyor" "xrayStatusRunning" = "Çalışıyor" "xrayStatusStop" = "Durduruldu" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Xray sürümünü gerçekten değiştirmek istiyor musunuz?" "xraySwitchVersionDialogDesc" = "Bu işlem Xray sürümünü #version# olarak değiştirecektir." "xraySwitchVersionPopover" = "Xray başarıyla güncellendi" +"panelUpdateDialog" = "Gerçekten paneli güncellemek istiyor musunuz?" +"panelUpdateDialogDesc" = "Bu, 3X-UI'yi #version# sürümüne güncelleyecek ve panel servisini yeniden başlatacaktır." +"panelUpdateCheckPopover" = "Panel güncelleme kontrolü başarısız oldu" +"panelUpdateStartedPopover" = "Panel güncellemesi başlatıldı" "geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?" "geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir." "geofilesUpdateDialogDesc" = "Bu, tüm dosyaları güncelleyecektir." diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml index 40bcaa76..b83122c9 100644 --- a/web/translation/translate.uk_UA.toml +++ b/web/translation/translate.uk_UA.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Версія" "xraySwitchClick" = "Виберіть версію, на яку ви хочете перейти." "xraySwitchClickDesk" = "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями." +"xrayUpdates" = "Оновлення Xray" +"updatePanel" = "Оновити панель" +"panelUpdateDesc" = "Це оновить 3X-UI до останнього релізу та перезапустить сервіс панелі." +"currentPanelVersion" = "Поточна версія панелі" +"latestPanelVersion" = "Остання версія панелі" +"panelUpToDate" = "Панель оновлено" +"upToDate" = "Оновлено" "xrayStatusUnknown" = "Невідомо" "xrayStatusRunning" = "Запущено" "xrayStatusStop" = "Зупинено" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Ви дійсно хочете змінити версію Xray?" "xraySwitchVersionDialogDesc" = "Це змінить версію Xray на #version#." "xraySwitchVersionPopover" = "Xray успішно оновлено" +"panelUpdateDialog" = "Ви дійсно хочете оновити панель?" +"panelUpdateDialogDesc" = "Це оновить 3X-UI до #version# та перезапустить сервіс панелі." +"panelUpdateCheckPopover" = "Перевірка оновлення панелі не вдалася" +"panelUpdateStartedPopover" = "Розпочато оновлення панелі" "geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?" "geofileUpdateDialogDesc" = "Це оновить файл #filename#." "geofilesUpdateDialogDesc" = "Це оновить усі геофайли." diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 43afe89b..3d836b33 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -126,6 +126,13 @@ "xraySwitch" = "Phiên bản" "xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang." "xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại." +"xrayUpdates" = "Cập nhật Xray" +"updatePanel" = "Cập nhật Panel" +"panelUpdateDesc" = "Điều này sẽ cập nhật 3X-UI lên bản phát hành mới nhất và khởi động lại dịch vụ panel." +"currentPanelVersion" = "Phiên bản panel hiện tại" +"latestPanelVersion" = "Phiên bản panel mới nhất" +"panelUpToDate" = "Panel đã được cập nhật" +"upToDate" = "Đã cập nhật" "xrayStatusUnknown" = "Không xác định" "xrayStatusRunning" = "Đang chạy" "xrayStatusStop" = "Dừng" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "Bạn có chắc chắn muốn thay đổi phiên bản Xray không?" "xraySwitchVersionDialogDesc" = "Hành động này sẽ thay đổi phiên bản Xray thành #version#." "xraySwitchVersionPopover" = "Xray đã được cập nhật thành công" +"panelUpdateDialog" = "Bạn có chắc muốn cập nhật panel không?" +"panelUpdateDialogDesc" = "Điều này sẽ cập nhật 3X-UI lên #version# và khởi động lại dịch vụ panel." +"panelUpdateCheckPopover" = "Kiểm tra cập nhật panel thất bại" +"panelUpdateStartedPopover" = "Bắt đầu cập nhật panel" "geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?" "geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#." "geofilesUpdateDialogDesc" = "Thao tác này sẽ cập nhật tất cả các tập tin." diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index cb42afce..57c23eac 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -126,6 +126,13 @@ "xraySwitch" = "版本" "xraySwitchClick" = "选择你要切换到的版本" "xraySwitchClickDesk" = "请谨慎选择,因为较旧版本可能与当前配置不兼容" +"xrayUpdates" = "Xray 更新" +"updatePanel" = "更新面板" +"panelUpdateDesc" = "这将把 3X-UI 更新到最新版本并重启面板服务。" +"currentPanelVersion" = "当前面板版本" +"latestPanelVersion" = "最新面板版本" +"panelUpToDate" = "面板已是最新" +"upToDate" = "已是最新" "xrayStatusUnknown" = "未知" "xrayStatusRunning" = "运行中" "xrayStatusStop" = "停止" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "您确定要更改Xray版本吗?" "xraySwitchVersionDialogDesc" = "这将把Xray版本更改为#version#。" "xraySwitchVersionPopover" = "Xray 更新成功" +"panelUpdateDialog" = "您确定要更新面板吗?" +"panelUpdateDialogDesc" = "这将把 3X-UI 更新到 #version# 并重启面板服务。" +"panelUpdateCheckPopover" = "面板更新检查失败" +"panelUpdateStartedPopover" = "已开始更新面板" "geofileUpdateDialog" = "您确定要更新地理文件吗?" "geofileUpdateDialogDesc" = "这将更新 #filename# 文件。" "geofilesUpdateDialogDesc" = "这将更新所有文件。" diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml index 551ebbd0..69e5164c 100644 --- a/web/translation/translate.zh_TW.toml +++ b/web/translation/translate.zh_TW.toml @@ -126,6 +126,13 @@ "xraySwitch" = "版本" "xraySwitchClick" = "選擇你要切換到的版本" "xraySwitchClickDesk" = "請謹慎選擇,因為較舊版本可能與當前配置不相容" +"xrayUpdates" = "Xray 更新" +"updatePanel" = "更新面板" +"panelUpdateDesc" = "這將把 3X-UI 更新到最新版本並重新啟動面板服務。" +"currentPanelVersion" = "目前面板版本" +"latestPanelVersion" = "最新面板版本" +"panelUpToDate" = "面板已是最新" +"upToDate" = "已是最新" "xrayStatusUnknown" = "未知" "xrayStatusRunning" = "運行中" "xrayStatusStop" = "停止" @@ -147,6 +154,10 @@ "xraySwitchVersionDialog" = "您確定要變更Xray版本嗎?" "xraySwitchVersionDialogDesc" = "這將會把Xray版本變更為#version#。" "xraySwitchVersionPopover" = "Xray 更新成功" +"panelUpdateDialog" = "您確定要更新面板嗎?" +"panelUpdateDialogDesc" = "這將把 3X-UI 更新到 #version# 並重新啟動面板服務。" +"panelUpdateCheckPopover" = "面板更新檢查失敗" +"panelUpdateStartedPopover" = "面板更新已開始" "geofileUpdateDialog" = "您確定要更新地理檔案嗎?" "geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。" "geofilesUpdateDialogDesc" = "這將更新所有文件。"