From 56a6791c530d3aa4caae83f31455853c8eff37a2 Mon Sep 17 00:00:00 2001 From: RikaCelery Date: Sun, 14 Jun 2026 13:57:49 +0800 Subject: [PATCH 1/3] feat(v3): add i18n support for WebUI (en / zh-CN) Lightweight self-built i18n with no external dependencies. Locale persists to localStorage, auto-detected from browser language on first visit. Translations object maps English keys to zh-CN values. $t() method falls back to English keys when no translation found. A globe button in the toolbar toggles between English and Simplified Chinese. --- src/main/resources/vue.html | 155 +++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 55 deletions(-) diff --git a/src/main/resources/vue.html b/src/main/resources/vue.html index 132d9ac..a46ab32 100644 --- a/src/main/resources/vue.html +++ b/src/main/resources/vue.html @@ -210,7 +210,7 @@

XhRec Manager

- @@ -218,12 +218,12 @@

XhRec Manager

-
-
@@ -234,19 +234,19 @@

XhRec Manager

- - - -
+
@@ -256,12 +256,15 @@

XhRec Manager

:class="maskEnabled ? 'bg-amber-100 text-amber-700 hover:bg-amber-600 hover:text-white' : 'bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600'"> - + - @@ -277,54 +280,54 @@

XhRec Manager

- Room - Status - Preview + {{ $t('Preview') }} - ID - Quality - Limit - State - Auto Pay - Segments + {{ $t('Segments') }} - Size - Actions + {{ $t('Actions') }} -

No rooms found. Add a room to start recording.

+

{{ $t('No rooms found. Add a room to start recording.') }}

XhRec Manager
- N/A
+ {{ $t('N/A') }}
{{room.id || '-'}} @@ -375,7 +378,7 @@

XhRec Manager

+ :title="$t('Actual recording quality')"> ⏺{{room.sessionQuality}}
@@ -386,8 +389,8 @@

XhRec Manager

{{room.timeLimit > 0 ? (room.timeLimit / 1000) : '∞'}}s - @@ -396,8 +399,8 @@

XhRec Manager

{{room.sizeLimitBytes > 0 ? formatBytes(room.sizeLimitBytes) : '∞'}} - @@ -407,19 +410,19 @@

XhRec Manager

- REC + {{ $t('REC') }} - FETCH + {{ $t('FETCH') }} - ARM + {{ $t('ARM') }} - OFF + {{ $t('OFF') }} @@ -437,19 +440,19 @@

XhRec Manager

- + {{room.running || 0}} - + {{room.success || 0}} - + {{room.failed || 0}} - + {{room.segMissing || 0}} @@ -462,23 +465,23 @@

XhRec Manager

- - - - - @@ -493,10 +496,10 @@

XhRec Manager

- NETWORK LOGS + {{ $t('NETWORK LOGS') }}
-
Waiting for connection...
+
{{ $t('Waiting for connection...') }}
XhRec Manager previewHeight: 40, darkMode: false, maskEnabled: true, + locale: 'en', + translations: { + en: {}, + 'zh-CN': { + 'Room': '直播间', 'Status': '状态', 'Preview': '预览', 'ID': 'ID', 'Quality': '画质', + 'Limit': '限时', 'State': '状态', 'Auto Pay': '自动付费', 'Segments': '分片', 'Size': '大小', + 'Actions': '操作', 'Room URL or Slug': '直播间 URL 或名称', + 'LimitPlaceholder': '限时', 'SizePlaceholder': '大小', + 'Add & Activate': '添加并激活', 'Add Only': '仅添加', 'Toggle Dark Mode': '切换深色模式', + 'Preview size': '预览大小', 'Power Off Server': '关闭服务器', + 'Power Off Server (Gracefully)': '优雅关闭\n(等待所有录制停止)', + 'Switch Language / 切换语言': 'Switch Language / 切换语言', + 'REC': '录制中', 'FETCH': '获取中', 'ARM': '监听中', 'OFF': '离线', + 'Recording': '录制中', 'Fetching': '获取中', 'Listening': '监听中', 'Offline': '离线', + 'Mask ON': '脱敏 开', 'Mask OFF': '脱敏 关', 'Network Error': '网络错误', + 'Invalid limit value': '无效的限时值', 'Invalid size value': '无效的大小值', + 'Please enter a room name or URL': '请输入直播间名称或 URL', + 'Room added successfully': '添加成功', + 'No rooms found. Add a room to start recording.': '未找到直播间,请添加直播间开始录制。', + 'NETWORK LOGS': '网络日志', 'Waiting for connection...': '等待连接...', + 'Downloading': '下载中', 'Complete': '已完成', 'Failed': '失败', 'Missing': '缺失', + 'N/A': '无', 'sec': '秒', 'e.g. 1G': '例: 1G', + 'Set Time': '设置限时', 'Set Size': '设置大小', + 'Activate': '激活', 'Deactivate': '停用', 'Restart': '重启', 'Break': '中断', 'Remove': '移除', + 'XhRec Dashboard': 'XhRec 控制台', 'XhRec Manager': 'XhRec 管理器', + 'Actual recording quality': '实际录制画质', + 'Log Masking — hides sensitive data (model names, cookies, tokens) in logs. CRC32 hash stays stable per session, changes on restart.': '日志脱敏 — 在日志中隐藏敏感信息(主播名、Cookie、Token)。CRC32 哈希在同一次运行中保持一致,重启后变化。', + } + }, } }, computed: { @@ -592,6 +624,14 @@

XhRec Manager

} }, methods: { + $t(key) { + return this.translations[this.locale]?.[key] || this.translations.en?.[key] || key; + }, + toggleLocale() { + this.locale = this.locale === 'en' ? 'zh-CN' : 'en'; + localStorage.setItem('xhrec-locale', this.locale); + document.documentElement.lang = this.locale; + }, toggleDark() { this.darkMode = !this.darkMode; document.documentElement.classList.toggle('dark', this.darkMode); @@ -602,8 +642,8 @@

XhRec Manager

const r = await fetch(`${HOST}/mask/toggle`); const text = await r.text(); this.maskEnabled = text === 'true'; - this.showToast(this.maskEnabled ? 'Mask ON' : 'Mask OFF'); - } catch (e) { this.showToast("Network Error", "error"); } + this.showToast(this.maskEnabled ? this.$t('Mask ON') : this.$t('Mask OFF')); + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, showToast(message, type = "success") { const id = this.toastId++; @@ -632,48 +672,48 @@

XhRec Manager

try { const r1 = await fetch(`${HOST}/quality?id=${roomId}&q=${quality}`); this.showToast(await r1.text()); - } catch (e) { this.showToast("Network Error", "error"); } + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, async setAutoPay(value, roomId) { try { const r1 = await fetch(`${HOST}/autopay?id=${roomId}&v=${value}`); this.showToast(await r1.text()); - } catch (e) { this.showToast("Network Error", "error"); } + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, async setLimit(roomId) { const newLimit = this.editLimits[roomId]; if (newLimit === undefined || newLimit === null || newLimit < 0) { - this.showToast(`Invalid limit value`, "error"); + this.showToast(this.$t('Invalid limit value'), "error"); return; } try { const r1 = await fetch(`${HOST}/limit?id=${roomId}&v=${newLimit}`); this.showToast(await r1.text()); this.editLimits[roomId] = null; - } catch (e) { this.showToast("Network Error", "error"); } + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, async setSizeLimit(roomId) { const newLimit = this.editSizeLimits[roomId]; if (!newLimit) { - this.showToast(`Invalid limit value`, "error"); + this.showToast(this.$t('Invalid size value'), "error"); return; } try { const r1 = await fetch(`${HOST}/sizelimit?id=${roomId}&v=${newLimit}`); this.showToast(await r1.text()); this.editSizeLimits[roomId] = null; - } catch (e) { this.showToast("Network Error", "error"); } + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, async action(actionType, roomId) { try { const r1 = await fetch(`${HOST}/${actionType}?id=${roomId}`); this.showToast(await r1.text()); - } catch (e) { this.showToast("Network Error", "error"); } + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, async addRoom(active) { const name = this.addInput.split("/").slice(-1)[0].split("?")[0]; if (!name) { - this.showToast("Please enter a room name or URL", "error"); + this.showToast(this.$t("Please enter a room name or URL"), "error"); return; } const limit = this.timeLimit || 0; @@ -683,14 +723,14 @@

XhRec Manager

if (size) params.append('size', size); const r1 = await fetch(`${HOST}/add?${params}`); if (r1.ok) { - this.showToast("Room added successfully"); + this.showToast(this.$t("Room added successfully")); this.addInput = ""; this.timeLimit = null; this.sizeLimit = null; } else { this.showToast(await r1.text(), "error"); } - } catch (e) { this.showToast("Network Error", "error"); } + } catch (e) { this.showToast(this.$t('Network Error'), "error"); } }, async fetchdata() { try { @@ -807,6 +847,11 @@

XhRec Manager

this.darkMode = localStorage.getItem('xhrec-dark') === '1'; document.documentElement.classList.toggle('dark', this.darkMode); + // Locale init + const savedLocale = localStorage.getItem('xhrec-locale'); + this.locale = savedLocale || (navigator.language?.startsWith('zh') ? 'zh-CN' : 'en'); + document.documentElement.lang = this.locale; + // Mask status try { const r = await fetch(`${HOST}/mask/status`); this.maskEnabled = (await r.text()) === 'true'; } catch (_) {} From 5b8e540671aaa74855c36f2a994ba34773a69243 Mon Sep 17 00:00:00 2001 From: RikaCelery Date: Sun, 14 Jun 2026 13:59:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(i18n):=20change=20Break=20translation?= =?UTF-8?q?=20from=20=E4=B8=AD=E6=96=AD=20to=20=E5=88=87=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/vue.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/vue.html b/src/main/resources/vue.html index a46ab32..8f34279 100644 --- a/src/main/resources/vue.html +++ b/src/main/resources/vue.html @@ -565,7 +565,7 @@

XhRec Manager

'Downloading': '下载中', 'Complete': '已完成', 'Failed': '失败', 'Missing': '缺失', 'N/A': '无', 'sec': '秒', 'e.g. 1G': '例: 1G', 'Set Time': '设置限时', 'Set Size': '设置大小', - 'Activate': '激活', 'Deactivate': '停用', 'Restart': '重启', 'Break': '中断', 'Remove': '移除', + 'Activate': '激活', 'Deactivate': '停用', 'Restart': '重启', 'Break': '切分', 'Remove': '移除', 'XhRec Dashboard': 'XhRec 控制台', 'XhRec Manager': 'XhRec 管理器', 'Actual recording quality': '实际录制画质', 'Log Masking — hides sensitive data (model names, cookies, tokens) in logs. CRC32 hash stays stable per session, changes on restart.': '日志脱敏 — 在日志中隐藏敏感信息(主播名、Cookie、Token)。CRC32 哈希在同一次运行中保持一致,重启后变化。', From 8d8ca4064637c4790aef969d492de180894f4cb8 Mon Sep 17 00:00:00 2001 From: RikaCelery Date: Sun, 14 Jun 2026 13:59:56 +0800 Subject: [PATCH 3/3] fix(i18n): use $t() for log mask tooltip --- src/main/resources/vue.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/vue.html b/src/main/resources/vue.html index 8f34279..067e4c3 100644 --- a/src/main/resources/vue.html +++ b/src/main/resources/vue.html @@ -251,7 +251,7 @@

XhRec Manager

-