- {/* ── 显示 ── */}
-
-
- goSelect("屏幕亮度", SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
- goSelect("自动锁屏", SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />
-
-
- {/* ── 灯光 ── */}
-
-
- goSelect("旋钮亮度", KNOB_LIGHT_OPTIONS, knobLightIdx, "knob_breathlight")} />
- 0}
- onToggle={(v) => updateSetting({ knob_breathlight: v ? 2 : 0 })} />
- updateSetting({ buttonLight: v ? 0 : 1 })} />
-
-
- {/* ── 电源管理 ── */}
-
-
- goSelect("休眠时间", SLEEP_OPTIONS, sleepIdx, "sleep")} />
-
-
- {/* ── 灯光预览 ── */}
-
-
-
- {/* Knob preview */}
-
-
-
0
- ? `0 0 ${knobLightIdx * 10}px rgba(0,255,246,${knobLightIdx * 0.2})`
- : "none",
- }} />
-
0 ? `inset 0 0 8px rgba(0,255,246,0.15)` : "none",
- }} />
-
-
旋钮
-
- {/* Button preview */}
-
-
- {[1, 2, 3].map(i => (
-
- ))}
-
-
按键
-
-
-
-
-
-
-
- );
-}
diff --git a/client/src/pages/DspPage.tsx b/client/src/pages/DspPage.tsx
deleted file mode 100644
index 133eb31..0000000
--- a/client/src/pages/DspPage.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-/* ============================================================
- DSP & DAC PAGE
- Design: iOS Settings style
- Sections: DSP引擎 / DAC配置 / 模拟级 / 音频格式
- All selectable options → navigate to /select page
- ============================================================ */
-import { useDevice } from "@/contexts/DeviceContext";
-import { ChevronLeft, ChevronRight } from "lucide-react";
-import { useLocation } from "wouter";
-import BottomNav from "@/components/BottomNav";
-import { navigateToSelect } from "./SelectPage";
-
-function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
- return (
-
- );
-}
-
-function SectionHeader({ title }: { title: string }) {
- return (
-
- {title}
-
- );
-}
-
-function ListRow({ label, description, value, onClick, toggle, checked, onToggle }: {
- label: string; description?: string; value?: string;
- onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void;
-}) {
- if (toggle) {
- return (
-
-
-
{label}
- {description &&
{description}
}
-
-
{})} />
-
- );
- }
- return (
-
- );
-}
-
-const DAC_ARC_OPTIONS = ["关闭", "模式1", "模式2", "模式3"];
-const DAC_GAIN_OPTIONS = ["0dB", "+3dB", "+6dB", "+9dB"];
-const DAC_IMP_OPTIONS = ["32Ω", "150Ω", "300Ω"];
-const ANALOG_GAIN_OPTIONS = ["低", "中低", "中高", "高"];
-const XLR_OPTIONS = ["正常", "反转"];
-
-export default function DspPage() {
- const [, setLocation] = useLocation();
- const { deviceState, updateSetting } = useDevice();
- const ds = deviceState;
-
- const dspOn = (ds?.dsp_enable ?? 0) === 0;
- const peqOn = (ds?.peqEnable ?? 0) === 1;
- const dacVolDirect = (ds?.dacVolumeDirect ?? 0) === 1;
- const dacArcIdx = ds?.dacArc ?? 0;
- const dacGainIdx = ds?.dacGain ?? 0;
- const dacImpIdx = ds?.dacImpedance ?? 0;
- const analogGainIdx = ds?.analogGain ?? 0;
- const xlrIdx = ds?.xlr ?? 0;
-
- const goSelect = (title: string, options: string[], selected: number, key: string) => {
- navigateToSelect(title, options, selected, "/dsp", key);
- setLocation("/select");
- };
-
- return (
-
-
-
-
DSP & DAC
-
-
-
-
- {/* ── DSP 引擎 ── */}
-
-
- updateSetting({ dsp_enable: v ? 0 : 1 })} />
- updateSetting({ peqEnable: v ? 1 : 0 })} />
-
-
- {/* ── DAC 配置 ── */}
-
-
- goSelect("ARC 模式", DAC_ARC_OPTIONS, dacArcIdx, "dacArc")} />
- goSelect("DAC 增益", DAC_GAIN_OPTIONS, dacGainIdx, "dacGain")} />
- goSelect("DAC 阻抗", DAC_IMP_OPTIONS, dacImpIdx, "dacImpedance")} />
- updateSetting({ dacVolumeDirect: v ? 1 : 0 })} />
-
-
- {/* ── 模拟级 ── */}
-
-
- goSelect("模拟增益", ANALOG_GAIN_OPTIONS, analogGainIdx, "analogGain")} />
- goSelect("XLR 极性", XLR_OPTIONS, xlrIdx, "xlr")} />
-
-
- {/* ── 音频格式 ── */}
-
-
-
-
-
- {ds?.audioFormat ?? "PCM 44.1kHz"}
-
-
-
-
-
-
-
- );
-}
diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx
index 552aa22..1924d3d 100644
--- a/client/src/pages/EQPage.tsx
+++ b/client/src/pages/EQPage.tsx
@@ -37,8 +37,9 @@ import {
} from "@/lib/luxsinApi";
import { getFilterType, getFilterShortName } from "@/lib/peqAudio";
import localeZh from "@/locales/data-zh.json";
-import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json";
+import { resolveLocalePack } from "@/locales/resolveLocale";
+import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
import {
FILTER_TYPES,
BAND_FREQ_MIN,
@@ -106,18 +107,18 @@ export default function EQPage() {
const eqOn = peqOn && !bypassOn;
const eqUi = useMemo((): PeqEqUi => {
- const lang = deviceState?.language;
- const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
+ const pack = resolveLocalePack(deviceState?.language);
const peq = pack.peq as typeof localeZh.peq;
- return (peq.eqUi ?? (localeZh.peq as typeof localeZh.peq).eqUi) as PeqEqUi;
+ return (peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi) as PeqEqUi;
}, [deviceState?.language]);
const peqCardLabels = useMemo(() => {
- const lang = deviceState?.language;
- const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
- return pack.peq ?? localeZh.peq;
+ const pack = resolveLocalePack(deviceState?.language);
+ return pack.peq ?? localeEn.peq;
}, [deviceState?.language]);
+ const eqPageTitle = usePageTitleWithConnection("HP-EQ");
+
const [bandsByMode, setBandsByMode] = useState<
Record<"A" | "B", typeof DEFAULT_BANDS>
>({
@@ -1481,7 +1482,7 @@ export default function EQPage() {
- HP-EQ
+ {eqPageTitle}
;
@@ -225,11 +226,12 @@ export default function EffectsPage() {
const { deviceState, updateSetting } = useDevice();
const effectText = useMemo((): EffectLocale => {
- const lang = deviceState?.language;
- const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
- return (pack.effect ?? localeZh.effect) as EffectLocale;
+ const pack = resolveLocalePack(deviceState?.language);
+ return (pack.effect ?? localeEn.effect) as EffectLocale;
}, [deviceState?.language]);
+ const pageTitle = usePageTitleWithConnection(effectText.pageTitle ?? "Effects");
+
const sceneLabels = useMemo(() => {
const opts = effectText.style?.options ?? [];
return [...opts].sort((a, b) => a.index - b.index).map((o) => o.label);
@@ -346,7 +348,7 @@ export default function EffectsPage() {
const [delayInputTarget, setDelayInputTarget] = useState(null);
const [delayInputValue, setDelayInputValue] = useState("");
- const delayInputEnglish = deviceState?.language === 0;
+ const delayInputEnglish = resolveDeviceLanguage(deviceState?.language) === 0;
const maxDelayMs = SUBWOOFER_DELAY_MAX / SUBWOOFER_DELAY_MS_DIVISOR;
const openDelayInput = (target: DelayInputTarget) => {
@@ -423,7 +425,7 @@ export default function EffectsPage() {
- {effectText.pageTitle}
+ {pageTitle}
{
diff --git a/client/src/pages/IOPage.tsx b/client/src/pages/IOPage.tsx
index c26cc71..9d1e8aa 100644
--- a/client/src/pages/IOPage.tsx
+++ b/client/src/pages/IOPage.tsx
@@ -12,8 +12,9 @@ import { ChevronLeft } from "lucide-react";
import { useLocation } from "wouter";
import { useMemo } from "react";
import localeZh from "@/locales/data-zh.json";
-import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json";
+import { resolveLocalePack } from "@/locales/resolveLocale";
+import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
type SourceLocale = NonNullable<(typeof localeZh)["source"]>;
@@ -212,8 +213,7 @@ export default function IOPage() {
const { deviceState, setInput, setOutput } = useDevice();
const sourceText = useMemo((): SourceLocale => {
- const lang = deviceState?.language;
- const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
+ const pack = resolveLocalePack(deviceState?.language);
return (pack.source ?? {}) as SourceLocale;
}, [deviceState?.language]);
@@ -227,7 +227,9 @@ export default function IOPage() {
[sourceText.outputOptions],
);
- const ioPageTitle = sourceText.pageTitle ?? sourceText.label ?? "输入输出";
+ const ioPageTitle = usePageTitleWithConnection(
+ sourceText.pageTitle ?? sourceText.label ?? "I/O",
+ );
const inputSectionTitle = sourceText.inputSection ?? "输入";
const outputSectionTitle = sourceText.outputSection ?? "输出";
diff --git a/client/src/pages/SelectPage.tsx b/client/src/pages/SelectPage.tsx
index c3e2356..3f3203e 100644
--- a/client/src/pages/SelectPage.tsx
+++ b/client/src/pages/SelectPage.tsx
@@ -53,10 +53,9 @@ import {
} from "@/components/ui/alert-dialog";
import localeZh from "@/locales/data-zh.json";
-
-import localeZhHK from "@/locales/data-zh-HK.json";
-
import localeEn from "@/locales/data-en.json";
+import { resolveLocalePack } from "@/locales/resolveLocale";
+import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
@@ -92,18 +91,6 @@ type LocaleRoot = {
-const LOCALE_BY_LANG: Record = {
-
- 0: localeEn as LocaleRoot,
-
- 1: localeZhHK as LocaleRoot,
-
- 2: localeZh as LocaleRoot,
-
-};
-
-
-
/* ── Key → API field mapping ── */
const KEY_TO_SETTING: Record = {
@@ -232,11 +219,9 @@ export default function SelectPage() {
const [passthroughConfirmIdx, setPassthroughConfirmIdx] = useState(null);
+ const pageTitle = usePageTitleWithConnection(state?.title ?? "");
-
- const langIdx = deviceState?.language ?? 2;
-
- const dacVolumeDirectText = (LOCALE_BY_LANG[langIdx] ?? localeZh as LocaleRoot).dac?.dacVolumeDirect;
+ const dacVolumeDirectText = resolveLocalePack(deviceState?.language).dac?.dacVolumeDirect;
@@ -390,7 +375,7 @@ export default function SelectPage() {
- {title}
+ {pageTitle}
diff --git a/client/src/pages/SystemPage.tsx b/client/src/pages/SystemPage.tsx
index 8787390..a84dbd7 100644
--- a/client/src/pages/SystemPage.tsx
+++ b/client/src/pages/SystemPage.tsx
@@ -15,9 +15,9 @@ import { useEffect } from "react";
import BottomNav from "@/components/BottomNav";
import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
import { navigateToSelect } from "./SelectPage";
-import localeZh from "@/locales/data-zh.json";
-import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json";
+import { DEFAULT_DEVICE_LANGUAGE, resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
+import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
return (
@@ -96,12 +96,6 @@ type LocaleSettings = {
};
};
-const LOCALE_BY_LANG: Record = {
- 0: localeEn as LocaleSettings,
- 1: localeZhHK as LocaleSettings,
- 2: localeZh as LocaleSettings,
-};
-
function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) {
if (!items || items.length === 0) return fallback;
const sorted = [...items].sort((a, b) => a.index - b.index);
@@ -137,10 +131,10 @@ export default function SystemPage() {
const screenOffIdx = ds?.screenOff ?? 2;
const sleepIdx = ds?.sleep ?? 0;
const autoHomeIdx = ds?.autoHome ?? 1;
- const rawLangIdx = ds?.language ?? 2;
- const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : 2;
+ const rawLangIdx = resolveDeviceLanguage(ds?.language);
+ const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : DEFAULT_DEVICE_LANGUAGE;
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
- const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh;
+ const localeData = resolveLocalePack(ds?.language) as LocaleSettings;
const settingsText = localeData.settings;
const ui = settingsText.systemPage;
const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]);
@@ -160,6 +154,8 @@ export default function SystemPage() {
(localeEn as LocaleSettings).settings.sleep?.label ??
"Sleep";
+ const pageTitle = usePageTitleWithConnection(settingsText.vu);
+
const goSelect = (title: string, options: string[], selected: number, key: string) => {
navigateToSelect(title, options, selected, "/system", key);
setLocation("/select");
@@ -171,7 +167,7 @@ export default function SystemPage() {
- {settingsText.vu}
+ {pageTitle}
diff --git a/client/src/pages/VUPage.tsx b/client/src/pages/VUPage.tsx
index 772ca74..bbc7933 100644
--- a/client/src/pages/VUPage.tsx
+++ b/client/src/pages/VUPage.tsx
@@ -12,8 +12,9 @@ import { Check, ChevronLeft } from "lucide-react";
import { useLocation } from "wouter";
import { useMemo } from "react";
import localeZh from "@/locales/data-zh.json";
-import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json";
+import { resolveLocalePack } from "@/locales/resolveLocale";
+import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
type VUPageLocale = NonNullable<(typeof localeZh)["vuPage"]>;
@@ -35,12 +36,12 @@ export default function VUPage() {
const currentVU = deviceState?.vu ?? 0;
const vuPageText = useMemo((): VUPageLocale => {
- const lang = deviceState?.language;
- const loc =
- lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
- return (loc.vuPage ?? {}) as VUPageLocale;
+ const loc = resolveLocalePack(deviceState?.language);
+ return (loc.vuPage ?? localeEn.vuPage ?? {}) as VUPageLocale;
}, [deviceState?.language]);
+ const pageTitle = usePageTitleWithConnection(vuPageText.title ?? "VU");
+
return (
@@ -48,7 +49,7 @@ export default function VUPage() {
- {vuPageText.title ?? "VU 表库"}
+ {pageTitle}
diff --git a/package.json b/package.json
index 1ffd44a..3e6f9b1 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"add": "^2.0.6",
"autoprefixer": "^10.4.20",
"esbuild": "^0.25.0",
+ "jiti": "^2.7.0",
"pnpm": "^10.15.1",
"postcss": "^8.4.47",
"prettier": "^3.6.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7276826..154c87a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -167,13 +167,13 @@ importers:
devDependencies:
'@builder.io/vite-plugin-jsx-loc':
specifier: ^0.1.1
- version: 0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
+ version: 0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
'@tailwindcss/typography':
specifier: ^0.5.15
version: 0.5.20(tailwindcss@4.3.0)
'@tailwindcss/vite':
specifier: ^4.1.3
- version: 4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
+ version: 4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
'@types/google.maps':
specifier: ^3.58.1
version: 3.65.1
@@ -188,7 +188,7 @@ importers:
version: 19.2.1(@types/react@19.2.17)
'@vitejs/plugin-react':
specifier: ^5.0.4
- version: 5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
+ version: 5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
add:
specifier: ^2.0.6
version: 2.0.6
@@ -198,6 +198,9 @@ importers:
esbuild:
specifier: ^0.25.0
version: 0.25.10
+ jiti:
+ specifier: ^2.7.0
+ version: 2.7.0
pnpm:
specifier: ^10.15.1
version: 10.34.1
@@ -224,7 +227,7 @@ importers:
version: 5.6.3
vite:
specifier: ^7.1.7
- version: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
+ version: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
vite-plugin-manus-runtime:
specifier: ^0.0.57
version: 0.0.57
@@ -4164,10 +4167,10 @@ snapshots:
estree-walker: 2.0.2
magic-string: 0.30.19
- '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
+ '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
dependencies:
'@builder.io/jsx-loc-internals': 0.0.1
- vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
+ vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
'@chevrotain/cst-dts-gen@11.0.3':
dependencies:
@@ -5427,12 +5430,12 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.3.0
- '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
+ '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
- vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
+ vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
'@types/babel__core@7.20.5':
dependencies:
@@ -5623,7 +5626,7 @@ snapshots:
'@ungap/structured-clone@1.3.1': {}
- '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
+ '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
dependencies:
'@babel/core': 7.29.7
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7)
@@ -5631,7 +5634,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-rc.3
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
- vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
+ vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
transitivePeerDependencies:
- supports-color
@@ -7903,7 +7906,7 @@ snapshots:
lightningcss: 1.32.0
terser: 5.46.1
- vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4):
+ vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4):
dependencies:
esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.3)
@@ -7914,7 +7917,7 @@ snapshots:
optionalDependencies:
'@types/node': 24.13.1
fsevents: 2.3.3
- jiti: 1.21.7
+ jiti: 2.7.0
lightningcss: 1.32.0
terser: 5.46.1
tsx: 4.22.4
diff --git a/vite.config.ts b/vite.config.ts
index 8ea127e..3b3f293 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -64,6 +64,7 @@ export default defineConfig({
"@": path.resolve(import.meta.dirname, "client", "src"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
},
+ dedupe: ["react", "react-dom"],
},
envDir: path.resolve(import.meta.dirname),
root: path.resolve(import.meta.dirname, "client"),