i18n: localize sidebar theme toggle, xray-status badge, and nodes menu

The sidebar theme submenu (Theme / Dark / Ultra dark) and the dashboard's
Xray status badge ("Xray is running" etc.) were hardcoded English strings.
Wire them through vue-i18n: ThemeSwitch.vue uses menu.theme/dark/ultraDark,
and XrayStatusCard.vue derives the badge text from the existing
pages.index.xrayStatus{Running,Stop,Error,Unknown} keys (status.js no
longer carries an English stateMsg field).

The "Nodes" menu item was already keyed as menu.nodes but only en-US and
fa-IR had a translation; add it to the other 11 languages, matching the
wording each file already uses for pages.nodes.title.
#4201
This commit is contained in:
MHSanaei 2026-05-10 11:55:37 +02:00
parent 444b05cac9
commit cf5767acd1
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
15 changed files with 33 additions and 20 deletions

View file

@ -1,8 +1,11 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { BulbFilled, BulbOutlined } from '@ant-design/icons-vue'; import { BulbFilled, BulbOutlined } from '@ant-design/icons-vue';
import { theme, currentTheme, toggleTheme, toggleUltra, pauseAnimationsUntilLeave } from '@/composables/useTheme.js'; import { theme, currentTheme, toggleTheme, toggleUltra, pauseAnimationsUntilLeave } from '@/composables/useTheme.js';
const { t } = useI18n();
const BulbIcon = computed(() => (theme.isDark ? BulbFilled : BulbOutlined)); const BulbIcon = computed(() => (theme.isDark ? BulbFilled : BulbOutlined));
function onDarkChange() { function onDarkChange() {
@ -22,17 +25,17 @@ function onUltraClick() {
<template #title> <template #title>
<span> <span>
<component :is="BulbIcon" /> <component :is="BulbIcon" />
<span class="theme-label">Theme</span> <span class="theme-label">{{ t('menu.theme') }}</span>
</span> </span>
</template> </template>
<a-menu-item id="change-theme" class="ant-menu-theme-switch"> <a-menu-item id="change-theme" class="ant-menu-theme-switch">
<span>Dark</span> <span>{{ t('menu.dark') }}</span>
<a-switch :style="{ marginLeft: '2px' }" size="small" :checked="theme.isDark" @change="onDarkChange" /> <a-switch :style="{ marginLeft: '2px' }" size="small" :checked="theme.isDark" @change="onDarkChange" />
</a-menu-item> </a-menu-item>
<a-menu-item v-if="theme.isDark" id="change-theme-ultra" class="ant-menu-theme-switch"> <a-menu-item v-if="theme.isDark" id="change-theme-ultra" class="ant-menu-theme-switch">
<span>Ultra dark</span> <span>{{ t('menu.ultraDark') }}</span>
<a-checkbox :style="{ marginLeft: '2px' }" :checked="theme.isUltra" @click="onUltraClick" /> <a-checkbox :style="{ marginLeft: '2px' }" :checked="theme.isUltra" @click="onUltraClick" />
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>

View file

@ -27,12 +27,6 @@ const XRAY_STATE_COLORS = {
error: 'red', error: 'red',
}; };
const XRAY_STATE_MESSAGES = {
running: 'Xray is running',
stop: 'Xray is stopped',
error: 'Xray error',
};
export class Status { export class Status {
constructor(data) { constructor(data) {
this.cpu = new CurTotal(0, 0); this.cpu = new CurTotal(0, 0);
@ -51,7 +45,7 @@ export class Status {
this.uptime = 0; this.uptime = 0;
this.appUptime = 0; this.appUptime = 0;
this.appStats = { threads: 0, mem: 0, uptime: 0 }; this.appStats = { threads: 0, mem: 0, uptime: 0 };
this.xray = { state: 'stop', stateMsg: '', errorMsg: '', version: '', color: '' }; this.xray = { state: 'stop', errorMsg: '', version: '', color: '' };
if (data == null) return; if (data == null) return;
@ -73,6 +67,5 @@ export class Status {
this.appStats = data.appStats ?? this.appStats; this.appStats = data.appStats ?? this.appStats;
this.xray = { ...this.xray, ...(data.xray || {}) }; this.xray = { ...this.xray, ...(data.xray || {}) };
this.xray.color = XRAY_STATE_COLORS[this.xray.state] ?? 'gray'; this.xray.color = XRAY_STATE_COLORS[this.xray.state] ?? 'gray';
this.xray.stateMsg = XRAY_STATE_MESSAGES[this.xray.state] ?? 'Unknown';
} }
} }

View file

@ -1,4 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { import {
BarsOutlined, BarsOutlined,
@ -9,7 +10,7 @@ import {
const { t } = useI18n(); const { t } = useI18n();
defineProps({ const props = defineProps({
status: { type: Object, required: true }, status: { type: Object, required: true },
isMobile: { type: Boolean, default: false }, isMobile: { type: Boolean, default: false },
ipLimitEnable: { type: Boolean, default: false }, ipLimitEnable: { type: Boolean, default: false },
@ -17,10 +18,16 @@ defineProps({
defineEmits(['stop-xray', 'restart-xray', 'open-logs', 'open-xray-logs', 'open-version-switch']); defineEmits(['stop-xray', 'restart-xray', 'open-logs', 'open-xray-logs', 'open-version-switch']);
// Map xray.color which animation class to apply on the badge dot. const XRAY_STATE_KEYS = {
// The legacy .xray-*-animation classes only override the badge ring running: 'pages.index.xrayStatusRunning',
// color; the actual pulsing comes from .xray-processing-animation stop: 'pages.index.xrayStatusStop',
// (which animates .ant-badge-status-dot via @keyframes runningAnimation). error: 'pages.index.xrayStatusError',
};
const stateText = computed(() =>
t(XRAY_STATE_KEYS[props.status.xray.state] ?? 'pages.index.xrayStatusUnknown'),
);
function badgeAnimationClass(color) { function badgeAnimationClass(color) {
if (color === 'green') return 'xray-running-animation'; if (color === 'green') return 'xray-running-animation';
if (color === 'orange') return 'xray-stop-animation'; if (color === 'orange') return 'xray-stop-animation';
@ -43,7 +50,7 @@ function badgeAnimationClass(color) {
<template #extra> <template #extra>
<template v-if="status.xray.state !== 'error'"> <template v-if="status.xray.state !== 'error'">
<a-badge status="processing" :class="['xray-processing-animation', badgeAnimationClass(status.xray.color)]" <a-badge status="processing" :class="['xray-processing-animation', badgeAnimationClass(status.xray.color)]"
:text="status.xray.stateMsg" :color="status.xray.color" /> :text="stateText" :color="status.xray.color" />
</template> </template>
<template v-else> <template v-else>
<a-popover> <a-popover>
@ -60,7 +67,7 @@ function badgeAnimationClass(color) {
{{ line }} {{ line }}
</span> </span>
</template> </template>
<a-badge status="processing" :text="status.xray.stateMsg" :color="status.xray.color" <a-badge status="processing" :text="stateText" :color="status.xray.color"
:class="['xray-processing-animation', 'xray-error-animation']" /> :class="['xray-processing-animation', 'xray-error-animation']" />
</a-popover> </a-popover>
</template> </template>

View file

@ -94,6 +94,7 @@
"ultraDark": "داكن جدًا", "ultraDark": "داكن جدًا",
"dashboard": "نظرة عامة", "dashboard": "نظرة عامة",
"inbounds": "الإدخالات", "inbounds": "الإدخالات",
"nodes": "النودز",
"settings": "إعدادات البانل", "settings": "إعدادات البانل",
"xray": "إعدادات Xray", "xray": "إعدادات Xray",
"logout": "تسجيل خروج", "logout": "تسجيل خروج",

View file

@ -94,6 +94,7 @@
"ultraDark": "Ultra Oscuro", "ultraDark": "Ultra Oscuro",
"dashboard": "Estado del Sistema", "dashboard": "Estado del Sistema",
"inbounds": "Entradas", "inbounds": "Entradas",
"nodes": "Nodos",
"settings": "Configuraciones", "settings": "Configuraciones",
"xray": "Ajustes Xray", "xray": "Ajustes Xray",
"logout": "Cerrar Sesión", "logout": "Cerrar Sesión",

View file

@ -94,6 +94,7 @@
"ultraDark": "Sangat Gelap", "ultraDark": "Sangat Gelap",
"dashboard": "Ikhtisar", "dashboard": "Ikhtisar",
"inbounds": "Masuk", "inbounds": "Masuk",
"nodes": "Node",
"settings": "Pengaturan Panel", "settings": "Pengaturan Panel",
"xray": "Konfigurasi Xray", "xray": "Konfigurasi Xray",
"logout": "Keluar", "logout": "Keluar",

View file

@ -94,6 +94,7 @@
"ultraDark": "ウルトラダーク", "ultraDark": "ウルトラダーク",
"dashboard": "ダッシュボード", "dashboard": "ダッシュボード",
"inbounds": "インバウンド一覧", "inbounds": "インバウンド一覧",
"nodes": "ノード",
"settings": "パネル設定", "settings": "パネル設定",
"xray": "Xray設定", "xray": "Xray設定",
"logout": "ログアウト", "logout": "ログアウト",

View file

@ -94,6 +94,7 @@
"ultraDark": "Ultra Escuro", "ultraDark": "Ultra Escuro",
"dashboard": "Visão Geral", "dashboard": "Visão Geral",
"inbounds": "Inbounds", "inbounds": "Inbounds",
"nodes": "Nós",
"settings": "Panel Settings", "settings": "Panel Settings",
"xray": "Xray Configs", "xray": "Xray Configs",
"logout": "Sair", "logout": "Sair",

View file

@ -94,6 +94,7 @@
"ultraDark": "Очень темная", "ultraDark": "Очень темная",
"dashboard": "Дашборд", "dashboard": "Дашборд",
"inbounds": "Подключения", "inbounds": "Подключения",
"nodes": "Узлы",
"settings": "Настройки", "settings": "Настройки",
"xray": "Настройки Xray", "xray": "Настройки Xray",
"logout": "Выход", "logout": "Выход",

View file

@ -94,6 +94,7 @@
"ultraDark": "Ultra Koyu", "ultraDark": "Ultra Koyu",
"dashboard": "Genel Bakış", "dashboard": "Genel Bakış",
"inbounds": "Gelenler", "inbounds": "Gelenler",
"nodes": "Düğümler",
"settings": "Panel Ayarları", "settings": "Panel Ayarları",
"xray": "Xray Yapılandırmaları", "xray": "Xray Yapılandırmaları",
"logout": ıkış Yap", "logout": ıkış Yap",

View file

@ -94,6 +94,7 @@
"ultraDark": "Ультра темна", "ultraDark": "Ультра темна",
"dashboard": "Огляд", "dashboard": "Огляд",
"inbounds": "Вхідні", "inbounds": "Вхідні",
"nodes": "Вузли",
"settings": "Параметри панелі", "settings": "Параметри панелі",
"xray": "Конфігурації Xray", "xray": "Конфігурації Xray",
"logout": "Вийти", "logout": "Вийти",

View file

@ -94,6 +94,7 @@
"ultraDark": "Siêu tối", "ultraDark": "Siêu tối",
"dashboard": "Trạng thái hệ thống", "dashboard": "Trạng thái hệ thống",
"inbounds": "Đầu vào khách hàng", "inbounds": "Đầu vào khách hàng",
"nodes": "Nút",
"settings": "Cài đặt bảng điều khiển", "settings": "Cài đặt bảng điều khiển",
"logout": "Đăng xuất", "logout": "Đăng xuất",
"xray": "Cài đặt Xray", "xray": "Cài đặt Xray",

View file

@ -94,6 +94,7 @@
"ultraDark": "超暗色", "ultraDark": "超暗色",
"dashboard": "系统状态", "dashboard": "系统状态",
"inbounds": "入站列表", "inbounds": "入站列表",
"nodes": "节点",
"settings": "面板设置", "settings": "面板设置",
"xray": "Xray 设置", "xray": "Xray 设置",
"logout": "退出登录", "logout": "退出登录",

View file

@ -94,6 +94,7 @@
"ultraDark": "超深色", "ultraDark": "超深色",
"dashboard": "系統狀態", "dashboard": "系統狀態",
"inbounds": "入站列表", "inbounds": "入站列表",
"nodes": "節點",
"settings": "面板設定", "settings": "面板設定",
"xray": "Xray 設定", "xray": "Xray 設定",
"logout": "退出登入", "logout": "退出登入",

View file

@ -280,8 +280,7 @@ func (s *Server) startTask() {
go func() { go func() {
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
// Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray s.cron.AddJob("@every 5s", job.NewXrayTrafficJob())
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}() }()
// check client ips from log file every 10 sec // check client ips from log file every 10 sec