dsh-pocket 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -1290,9 +1290,115 @@ function mobileApply(ctx) {
1290
1290
  }, MobileDrawerFooter));
1291
1291
  }
1292
1292
 
1293
+ // client/pocket-locales.js
1294
+ var NS2 = "pocket";
1295
+ var zh2 = {
1296
+ "section": "\u624B\u673A\u8BBF\u95EE",
1297
+ "title": "\u{1F4F1} \u624B\u673A\u8BBF\u95EE",
1298
+ "subtitle": "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65",
1299
+ "developer": "\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668",
1300
+ "starAsk": "\u2B50 \u987A\u624B\u7559\u9897 Star\uFF0C\u4F5C\u8005\u80FD\u9AD8\u5174\u4E00\u6574\u5929",
1301
+ "starCta": "\u884C\uFF0C\u7ED9\u4F60\u4E00\u9897 Star",
1302
+ "restarted": "\u{1F504} \u5DF2\u91CD\u542F",
1303
+ "ok": "\u77E5\u9053\u4E86",
1304
+ "bgHint": "\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\uFF1A{cmd}",
1305
+ "updatedRestart": "\u2705 \u5DF2\u66F4\u65B0 v{ver}\uFF0C\u91CD\u542F\u751F\u6548",
1306
+ "updateAutoRestarting": "\u2705 \u5DF2\u66F4\u65B0 v{ver}\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u2026",
1307
+ "updatedOk": "\u2705 \u5DF2\u66F4\u65B0 v{ver}",
1308
+ "updateAvailable": "\u{1F4E6} \u65B0\u7248\u672C v{ver}",
1309
+ "updating": "\u66F4\u65B0\u4E2D\u2026",
1310
+ "updateTo": "\u66F4\u65B0\u5230 v{ver}",
1311
+ "restartingNow": "\u6B63\u5728\u91CD\u542F\u751F\u6548\u2026",
1312
+ "restarting": "\u91CD\u542F\u4E2D\u2026",
1313
+ "restartNow": "\u{1F504} \u91CD\u542F dsh web \u751F\u6548",
1314
+ "updatingDetail": "\u23F3 \u66F4\u65B0\u4E2D\uFF08\u901A\u5E38 1-2 \u5206\u949F\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
1315
+ "restartingDetail": "\u23F3 \u6B63\u5728\u91CD\u542F\u751F\u6548\uFF08\u901A\u5E38 10-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
1316
+ "updatedAutoDetail": "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u751F\u6548\uFF0C\u8BF7\u7A0D\u5019\u5237\u65B0",
1317
+ "updatedRestartDetail": "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548",
1318
+ "updateFailed": "\u274C \u5931\u8D25\uFF1A{err}\uFF08\u624B\u52A8\u66F4\u65B0\uFF1Adsh plugin --profile web update dsh-pocket --latest -w\uFF09",
1319
+ "versionRange": "\u5F53\u524D v{cur} \u2192 \u6700\u65B0 v{latest}",
1320
+ "lanTitle": "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09",
1321
+ "lanHint": "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00",
1322
+ "lanPin": "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5BC6\u7801",
1323
+ "on": "\u5F00",
1324
+ "off": "\u5173",
1325
+ "lanPinValue": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
1326
+ "refresh": "\u5237\u65B0",
1327
+ "lanPinOff": "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09",
1328
+ "lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026",
1329
+ "wanTitle": "\u{1F310} \u516C\u7F51\uFF08\u4EBA\u5728\u5916\u9762\uFF09",
1330
+ "wanHint": "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u81EA\u52A8\u6362\u65B0\uFF09",
1331
+ "wanPin": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u6BCF\u6B21\u5F00\u542F\u516C\u7F51\u53D8\u65B0\uFF1B\u624B\u673A\u6253\u5F00\u94FE\u63A5\u9700\u8F93\u5165\u6B64\u5BC6\u7801\uFF09",
1332
+ "stopTunnel": "\u5173\u95ED\u516C\u7F51",
1333
+ "enable": "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE",
1334
+ "opening": "\u5F00\u542F\u4E2D\u2026",
1335
+ "downloading": "\u23F3 \u4E0B\u8F7D cloudflared\uFF08\u9996\u6B21\u7EA6 20-50MB\uFF0C\u901A\u5E38 1-2 \u5206\u949F\uFF1B\u4E4B\u540E\u79D2\u5F00\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
1336
+ "connecting": "\u23F3 \u8FDE\u63A5 Cloudflare \u8FB9\u7F18\uFF08\u901A\u5E38 5-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2{suffix}",
1337
+ "slowHint": " \u2014 \u6709\u70B9\u4E45\uFF1F\u68C0\u67E5\u662F\u5426\u5F00\u7740\u4EE3\u7406/VPN\uFF08Clash TUN \u7B49\uFF09",
1338
+ "error": "\u274C \u5F00\u542F\u5931\u8D25\uFF1A{detail}\uFF08\u53EF\u91CD\u8BD5\uFF1B\u82E5\u662F\u4EE3\u7406/VPN \u95EE\u9898\u89C1 README \u6392\u969C\uFF09",
1339
+ "unknownError": "\u672A\u77E5\u9519\u8BEF",
1340
+ "feedback": "\u6709\u95EE\u9898\uFF1F\u6B22\u8FCE\u5230 GitHub Issues \u53CD\u9988 \u{1F64F}"
1341
+ };
1342
+ var en2 = {
1343
+ "section": "Phone access",
1344
+ "title": "\u{1F4F1} Phone access",
1345
+ "subtitle": "The phone shows this exact screen, live",
1346
+ "developer": "Developer: \u5C11\u5317\u6668 (shaobeichen)",
1347
+ "starAsk": "\u2B50 Drop a Star if it helped \u2014 it makes the author\u2019s day",
1348
+ "starCta": "\u2605 Give a Star",
1349
+ "restarted": "\u{1F504} Restarted",
1350
+ "ok": "Got it",
1351
+ "bgHint": "Running in the background (not attached to a terminal). To stop: {cmd}",
1352
+ "updatedRestart": "\u2705 Updated to v{ver} \u2014 restart to apply",
1353
+ "updateAutoRestarting": "\u2705 Updated to v{ver} \u2014 auto-restarting\u2026",
1354
+ "updatedOk": "\u2705 Updated to v{ver}",
1355
+ "updateAvailable": "\u{1F4E6} Update available: v{ver}",
1356
+ "updating": "Updating\u2026",
1357
+ "updateTo": "Update to v{ver}",
1358
+ "restartingNow": "Restarting to apply\u2026",
1359
+ "restarting": "Restarting\u2026",
1360
+ "restartNow": "\u{1F504} Restart dsh web now",
1361
+ "updatingDetail": "\u23F3 Updating (usually 1-2 min) \xB7 {s}s elapsed",
1362
+ "restartingDetail": "\u23F3 Restarting to apply (usually 10-30s) \xB7 {s}s elapsed",
1363
+ "updatedAutoDetail": "\u2705 Updated \u2014 auto-restarting in progress, refresh shortly",
1364
+ "updatedRestartDetail": "\u2705 Updated \u2014 restart dsh web to apply",
1365
+ "updateFailed": "\u274C Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket --latest -w)",
1366
+ "versionRange": "Current v{cur} \u2192 latest v{latest}",
1367
+ "lanTitle": "\u{1F4F6} LAN (same Wi-Fi)",
1368
+ "lanHint": "Scan to open once your phone is on the same Wi-Fi",
1369
+ "lanPin": "LAN access PIN",
1370
+ "on": "On",
1371
+ "off": "Off",
1372
+ "lanPinValue": "\u{1F510} PIN: {pin} (required on the phone; separate from the public PIN)",
1373
+ "refresh": "Refresh",
1374
+ "lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)",
1375
+ "lanStarting": "Proxy starting\u2026",
1376
+ "wanTitle": "\u{1F310} Anywhere (public)",
1377
+ "wanHint": "Scan from any network (the URL changes on every restart)",
1378
+ "wanPin": "\u{1F510} PIN: {pin} (changes each time the tunnel is enabled; required on the phone)",
1379
+ "stopTunnel": "Stop",
1380
+ "enable": "Enable anywhere",
1381
+ "opening": "Enabling\u2026",
1382
+ "downloading": "\u23F3 Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) \xB7 {s}s elapsed",
1383
+ "connecting": "\u23F3 Connecting to Cloudflare edge (usually 5-30s) \xB7 {s}s elapsed{suffix}",
1384
+ "slowHint": " \u2014 taking long? Check for a proxy/VPN (e.g., Clash TUN)",
1385
+ "error": "\u274C Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)",
1386
+ "unknownError": "unknown error",
1387
+ "feedback": "\u{1F64F} Questions? Open an issue on GitHub"
1388
+ };
1389
+
1293
1390
  // client/index.jsx
1294
1391
  var name = "dsh-pocket";
1295
1392
  var inject = ["slots", "connection", "layout", "locale", "sessionLogDownload"];
1393
+ function fmt(t, key, vars) {
1394
+ let s = t(key);
1395
+ if (vars) {
1396
+ for (const [k, v] of Object.entries(vars)) {
1397
+ s = String(s).split(`{${k}}`).join(String(v));
1398
+ }
1399
+ }
1400
+ return s;
1401
+ }
1296
1402
  var styles = {
1297
1403
  card: { background: "var(--dsw-alias-bg-layer-1,#fff)", border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", borderRadius: 12, padding: "16px 20px", maxWidth: 480 },
1298
1404
  block: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", marginTop: 16, paddingTop: 16 },
@@ -1305,7 +1411,7 @@ var styles = {
1305
1411
  qr: { width: 220, height: 220, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", margin: "8px 0" },
1306
1412
  warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12, lineHeight: 1.5 }
1307
1413
  };
1308
- function PocketSettingsTab({ rpcCall }) {
1414
+ function PocketSettingsTab({ rpcCall, t }) {
1309
1415
  const [status, setStatus] = (0, import_react2.useState)(null);
1310
1416
  const [busy, setBusy] = (0, import_react2.useState)(false);
1311
1417
  const [error, setError] = (0, import_react2.useState)(null);
@@ -1315,8 +1421,8 @@ function PocketSettingsTab({ rpcCall }) {
1315
1421
  const [isDesktop, setIsDesktop] = (0, import_react2.useState)(false);
1316
1422
  const [now, setNow] = (0, import_react2.useState)(Date.now());
1317
1423
  (0, import_react2.useEffect)(() => {
1318
- const t = setInterval(() => setNow(Date.now()), 1e3);
1319
- return () => clearInterval(t);
1424
+ const t2 = setInterval(() => setNow(Date.now()), 1e3);
1425
+ return () => clearInterval(t2);
1320
1426
  }, []);
1321
1427
  const elapsed = (startedAt) => startedAt ? Math.max(0, Math.floor((Date.now() - startedAt) / 1e3)) : 0;
1322
1428
  const call = async (endpoint, payload) => {
@@ -1348,8 +1454,8 @@ function PocketSettingsTab({ rpcCall }) {
1348
1454
  };
1349
1455
  (0, import_react2.useEffect)(() => {
1350
1456
  load();
1351
- const t = setInterval(load, 3e3);
1352
- return () => clearInterval(t);
1457
+ const t2 = setInterval(load, 3e3);
1458
+ return () => clearInterval(t2);
1353
1459
  }, []);
1354
1460
  (0, import_react2.useEffect)(() => {
1355
1461
  try {
@@ -1375,10 +1481,10 @@ function PocketSettingsTab({ rpcCall }) {
1375
1481
  }
1376
1482
  };
1377
1483
  check();
1378
- const t = setInterval(check, 5 * 60 * 1e3);
1484
+ const t2 = setInterval(check, 5 * 60 * 1e3);
1379
1485
  return () => {
1380
1486
  alive = false;
1381
- clearInterval(t);
1487
+ clearInterval(t2);
1382
1488
  };
1383
1489
  }, [isDesktop]);
1384
1490
  const restartPocket = async () => {
@@ -1460,18 +1566,18 @@ function PocketSettingsTab({ rpcCall }) {
1460
1566
  (0, import_react2.createElement)(
1461
1567
  "div",
1462
1568
  null,
1463
- (0, import_react2.createElement)("strong", null, "\u{1F4F1} \u624B\u673A\u8BBF\u95EE | Phone access"),
1464
- (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65 | the phone shows this exact screen, live")
1569
+ (0, import_react2.createElement)("strong", null, t("title")),
1570
+ (0, import_react2.createElement)("div", { style: styles.muted }, t("subtitle"))
1465
1571
  ),
1466
1572
  (0, import_react2.createElement)(
1467
1573
  "div",
1468
1574
  { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", textAlign: "right" } },
1469
- (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, "\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668"),
1470
- (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, "\u2B50 \u987A\u624B\u7559\u9897 Star\uFF0C\u4F5C\u8005\u80FD\u9AD8\u5174\u4E00\u6574\u5929"),
1575
+ (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
1576
+ (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
1471
1577
  (0, import_react2.createElement)(
1472
1578
  "a",
1473
1579
  { href: "https://github.com/shaobeichen/dsh-pocket", target: "_blank", rel: "noreferrer", style: { color: "var(--dsw-alias-brand-primary,#4f6ef7)", fontSize: 12, lineHeight: 1.6, textDecoration: "underline" } },
1474
- "\u884C\uFF0C\u7ED9\u4F60\u4E00\u9897 Star"
1580
+ t("starCta")
1475
1581
  )
1476
1582
  )
1477
1583
  ),
@@ -1483,10 +1589,10 @@ function PocketSettingsTab({ rpcCall }) {
1483
1589
  (0, import_react2.createElement)(
1484
1590
  "div",
1485
1591
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
1486
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F504} \u5DF2\u91CD\u542F | Restarted"),
1487
- (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "\u77E5\u9053\u4E86 | OK")
1592
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
1593
+ (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
1488
1594
  ),
1489
- (0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, `\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\uFF1A${status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`}`)
1595
+ (0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, fmt(t, "bgHint", { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` }))
1490
1596
  ) : null,
1491
1597
  // 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
1492
1598
  // 桌面端不渲染(更新由 DSH Desktop 管理)
@@ -1499,84 +1605,82 @@ function PocketSettingsTab({ rpcCall }) {
1499
1605
  (0, import_react2.createElement)(
1500
1606
  "div",
1501
1607
  { style: { fontWeight: 600, fontSize: 13 } },
1502
- updateInfo.updated ? `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.current}\uFF0C\u91CD\u542F\u751F\u6548 | Updated \u2014 restart to apply` : updateInfo.result === "ok" ? updateInfo.autoRestart ? `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.latest}\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u2026 | updated \u2014 restarting\u2026` : `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.latest} | Updated` : `\u{1F4E6} \u65B0\u7248\u672C v${updateInfo.latest} | Update available`
1608
+ updateInfo.updated ? fmt(t, "updatedRestart", { ver: updateInfo.current }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? fmt(t, "updateAutoRestarting", { ver: updateInfo.latest }) : fmt(t, "updatedOk", { ver: updateInfo.latest }) : fmt(t, "updateAvailable", { ver: updateInfo.latest })
1503
1609
  ),
1504
- updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "\u66F4\u65B0\u4E2D\u2026" : `\u66F4\u65B0\u5230 v${updateInfo.latest} | Update`) : updateInfo.autoRestart ? (0, import_react2.createElement)("button", { style: styles.btn, disabled: true }, "\u6B63\u5728\u91CD\u542F\u751F\u6548\u2026 | restarting\u2026") : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? "\u91CD\u542F\u4E2D\u2026" : "\u{1F504} \u91CD\u542F dsh web \u751F\u6548 | Restart now")
1610
+ updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t("updating") : fmt(t, "updateTo", { ver: updateInfo.latest })) : updateInfo.autoRestart ? (0, import_react2.createElement)("button", { style: styles.btn, disabled: true }, t("restartingNow")) : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t("restarting") : t("restartNow"))
1505
1611
  ),
1506
1612
  (0, import_react2.createElement)(
1507
1613
  "div",
1508
1614
  { style: styles.muted, marginTop: 4 },
1509
- updateInfo.updating ? `\u23F3 \u66F4\u65B0\u4E2D\uFF08\u901A\u5E38 1-2 \u5206\u949F\uFF09\xB7 \u5DF2\u7B49\u5F85 ${elapsed(updateInfo.startedAt)} \u79D2 | updating (usually 1-2 min) \xB7 ${elapsed(updateInfo.startedAt)}s` : updateInfo.restarting ? `\u23F3 \u6B63\u5728\u91CD\u542F\u751F\u6548\uFF08\u901A\u5E38 10-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 ${elapsed(updateInfo.startedAt)} \u79D2 | restarting (usually 10-30s) \xB7 ${elapsed(updateInfo.startedAt)}s` : updateInfo.result === "ok" ? updateInfo.autoRestart ? "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u751F\u6548\uFF0C\u8BF7\u7A0D\u5019\u5237\u65B0 | updated \u2014 restarting automatically, refresh shortly" : "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548 | updated \u2014 restart dsh web" : updateInfo.result === "fail" ? `\u274C \u5931\u8D25\uFF1A${updateInfo.output || "\u672A\u77E5"}\uFF08\u624B\u52A8\u66F4\u65B0\uFF1Adsh plugin --profile web update dsh-pocket --latest -w\uFF09` : `\u5F53\u524D v${updateInfo.current} \u2192 \u6700\u65B0 v${updateInfo.latest}`
1615
+ updateInfo.updating ? fmt(t, "updatingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.restarting ? fmt(t, "restartingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? t("updatedAutoDetail") : t("updatedRestartDetail") : updateInfo.result === "fail" ? fmt(t, "updateFailed", { err: updateInfo.output || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
1510
1616
  )
1511
1617
  ) : null,
1512
1618
  // 局域网
1513
1619
  (0, import_react2.createElement)(
1514
1620
  "div",
1515
1621
  { style: styles.block },
1516
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09| LAN"),
1622
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("lanTitle")),
1517
1623
  lanUrl ? (0, import_react2.createElement)(
1518
1624
  "div",
1519
1625
  null,
1520
1626
  (0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
1521
1627
  (0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
1522
- (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00"),
1628
+ (0, import_react2.createElement)("div", { style: styles.muted }, t("lanHint")),
1523
1629
  // 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
1524
1630
  (0, import_react2.createElement)(
1525
1631
  "div",
1526
1632
  { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
1527
- (0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5BC6\u7801 | LAN access PIN"),
1633
+ (0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, t("lanPin")),
1528
1634
  (0, import_react2.createElement)("button", {
1529
1635
  style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanAuthEnabled !== false ? 600 : 400, background: status?.lanAuthEnabled !== false ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanAuthEnabled !== false ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)" },
1530
1636
  onClick: () => setLanAuth(true)
1531
- }, "\u5F00 | On"),
1637
+ }, t("on")),
1532
1638
  (0, import_react2.createElement)("button", {
1533
1639
  style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanAuthEnabled === false ? 600 : 400, background: status?.lanAuthEnabled === false ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanAuthEnabled === false ? "#fff" : "var(--dsw-alias-label-primary,inherit)" },
1534
1640
  onClick: () => setLanAuth(false)
1535
- }, "\u5173 | Off")
1641
+ }, t("off"))
1536
1642
  ),
1537
1643
  status?.lanAuthEnabled !== false ? (0, import_react2.createElement)(
1538
1644
  "div",
1539
1645
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1540
- "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A",
1541
- status.lanToken,
1542
- "\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
1543
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, "\u5237\u65B0 | Refresh")
1646
+ fmt(t, "lanPinValue", { pin: status.lanToken }),
1647
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, t("refresh"))
1544
1648
  ) : (0, import_react2.createElement)(
1545
1649
  "div",
1546
1650
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } },
1547
- "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09| PIN off \u2014 scan & go (LAN only; public still requires PIN)"
1651
+ t("lanPinOff")
1548
1652
  )
1549
- ) : (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
1653
+ ) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
1550
1654
  ),
1551
1655
  // 公网
1552
1656
  (0, import_react2.createElement)(
1553
1657
  "div",
1554
1658
  { style: styles.block },
1555
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F310} \u516C\u7F51\uFF08\u4EBA\u5728\u5916\u9762\uFF09| Anywhere"),
1659
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("wanTitle")),
1556
1660
  tunnelUrl ? (0, import_react2.createElement)(
1557
1661
  "div",
1558
1662
  null,
1559
1663
  (0, import_react2.createElement)("img", { src: status.tunnelQr, alt: "Tunnel QR", style: styles.qr }),
1560
1664
  (0, import_react2.createElement)("div", { style: styles.code }, tunnelUrl),
1561
- (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u81EA\u52A8\u6362\u65B0\uFF09"),
1665
+ (0, import_react2.createElement)("div", { style: styles.muted }, t("wanHint")),
1562
1666
  status.accessToken ? (0, import_react2.createElement)(
1563
1667
  "div",
1564
1668
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1565
- `\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A${status.accessToken}\uFF08\u6BCF\u6B21\u5F00\u542F\u516C\u7F51\u53D8\u65B0\uFF1B\u624B\u673A\u6253\u5F00\u94FE\u63A5\u9700\u8F93\u5165\u6B64\u5BC6\u7801\uFF09| PIN: ${status.accessToken} \u2014 required on the phone`
1669
+ fmt(t, "wanPin", { pin: status.accessToken })
1566
1670
  ) : null,
1567
- (0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, "\u5173\u95ED\u516C\u7F51 | Stop")
1671
+ (0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, t("stopTunnel"))
1568
1672
  ) : (0, import_react2.createElement)(
1569
1673
  "div",
1570
1674
  null,
1571
- (0, import_react2.createElement)("button", { style: { ...styles.primary, margin: "8px 0" }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? "\u5F00\u542F\u4E2D\u2026" : "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE | Enable anywhere"),
1675
+ (0, import_react2.createElement)("button", { style: { ...styles.primary, margin: "8px 0" }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? t("opening") : t("enable")),
1572
1676
  tunnelStarting ? (0, import_react2.createElement)(
1573
1677
  "div",
1574
1678
  { style: { marginTop: 4, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
1575
- tunnelPhase === "downloading" ? `\u23F3 \u4E0B\u8F7D cloudflared\uFF08\u9996\u6B21\u7EA6 20-50MB\uFF0C\u901A\u5E38 1-2 \u5206\u949F\uFF1B\u4E4B\u540E\u79D2\u5F00\uFF09\xB7 \u5DF2\u7B49\u5F85 ${elapsed(tunnelStateStarted)} \u79D2` : `\u23F3 \u8FDE\u63A5 Cloudflare \u8FB9\u7F18\uFF08\u901A\u5E38 5-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 ${elapsed(tunnelStateStarted)} \u79D2${elapsed(tunnelStateStarted) > 30 ? " \u2014 \u6709\u70B9\u4E45\uFF1F\u68C0\u67E5\u662F\u5426\u5F00\u7740\u4EE3\u7406/VPN\uFF08Clash TUN \u7B49\uFF09" : ""}`
1679
+ tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
1576
1680
  ) : tunnelPhase === "error" ? (0, import_react2.createElement)(
1577
1681
  "div",
1578
1682
  { style: { marginTop: 4, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
1579
- `\u274C \u5F00\u542F\u5931\u8D25\uFF1A${tunnelStateDetail || "\u672A\u77E5\u9519\u8BEF | failed"}\uFF08\u53EF\u91CD\u8BD5\uFF1B\u82E5\u662F\u4EE3\u7406/VPN \u95EE\u9898\u89C1 README \u6392\u969C\uFF09`
1683
+ fmt(t, "error", { detail: tunnelStateDetail || t("unknownError") })
1580
1684
  ) : null
1581
1685
  )
1582
1686
  ),
@@ -1588,7 +1692,7 @@ function PocketSettingsTab({ rpcCall }) {
1588
1692
  (0, import_react2.createElement)(
1589
1693
  "a",
1590
1694
  { href: "https://github.com/shaobeichen/dsh-pocket/issues", target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", textDecoration: "none" } },
1591
- "\u6709\u95EE\u9898\uFF1F\u6B22\u8FCE\u5230 GitHub Issues \u53CD\u9988 \u{1F64F} | Questions? Open an issue on GitHub"
1695
+ t("feedback")
1592
1696
  )
1593
1697
  )
1594
1698
  );
@@ -1596,6 +1700,8 @@ function PocketSettingsTab({ rpcCall }) {
1596
1700
  function apply(ctx) {
1597
1701
  mobileApply(ctx);
1598
1702
  const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(POCKET_RPC_CHANNEL, endpoint, payload, signal);
1703
+ const translate = ctx.locale.bind(NS2);
1704
+ ctx.effect(() => ctx.locale.register(NS2, { zh: zh2, en: en2 }), "dsh-pocket: pocket locale dictionaries");
1599
1705
  ctx.slots.inject(
1600
1706
  "settings.section",
1601
1707
  () => ctx.slots.register(
@@ -1603,8 +1709,8 @@ function apply(ctx) {
1603
1709
  name: "settings.section",
1604
1710
  id: "pocket",
1605
1711
  order: 1,
1606
- label: () => "\u624B\u673A\u8BBF\u95EE",
1607
- inject: () => ({ rpcCall })
1712
+ label: () => translate("section"),
1713
+ inject: () => ({ rpcCall, t: translate })
1608
1714
  },
1609
1715
  PocketSettingsTab
1610
1716
  )
package/client/index.jsx CHANGED
@@ -11,10 +11,23 @@ import { createElement as h, useEffect, useState } from 'react';
11
11
 
12
12
  import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS, redactStatus, compareVersions } from './api.js';
13
13
  import { mobileApply } from './mobile/mobile-apply.tsx';
14
+ import { NS as POCKET_NS, zh as POCKET_ZH, en as POCKET_EN } from './pocket-locales.js';
14
15
 
15
16
  const name = 'dsh-pocket';
16
17
  const inject = ['slots', 'connection', 'layout', 'locale', 'sessionLogDownload'];
17
18
 
19
+ // 词典在 pocket-locales.js;这里只做「取 key → 替换 {占位符} → 字符串」。
20
+ // 不依赖 DSH t() 的插值能力,避免行为不一致。
21
+ function fmt(t, key, vars) {
22
+ let s = t(key);
23
+ if (vars) {
24
+ for (const [k, v] of Object.entries(vars)) {
25
+ s = String(s).split(`{${k}}`).join(String(v));
26
+ }
27
+ }
28
+ return s;
29
+ }
30
+
18
31
  // 官方 DeepSeek Harness 设计系统(dsh-client-ui-theme design-platform.css):
19
32
  // 按钮 md=36px 胶囊形 / sm=28px;品牌色 --dsw-alias-brand-primary;
20
33
  // hover 走 --dsw-alias-button-*-hover;间距 4px 栅格;正文 13px。
@@ -31,7 +44,7 @@ const styles = {
31
44
  warn: { color: 'var(--dsw-alias-state-warn-primary,#b45309)', fontSize: 12, lineHeight: 1.5 },
32
45
  };
33
46
 
34
- function PocketSettingsTab({ rpcCall }) {
47
+ function PocketSettingsTab({ rpcCall, t }) {
35
48
  const [status, setStatus] = useState(null);
36
49
  const [busy, setBusy] = useState(false);
37
50
  const [error, setError] = useState(null);
@@ -193,14 +206,14 @@ function PocketSettingsTab({ rpcCall }) {
193
206
  return h('div', { style: styles.card },
194
207
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
195
208
  h('div', null,
196
- h('strong', null, '📱 手机访问 | Phone access'),
197
- h('div', { style: styles.muted }, '手机扫码打开的就是电脑上的这个界面,实时同步 | the phone shows this exact screen, live'),
209
+ h('strong', null, t('title')),
210
+ h('div', { style: styles.muted }, t('subtitle')),
198
211
  ),
199
212
  h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary,#8b93a1)', textAlign: 'right' } },
200
- h('div', { style: { whiteSpace: 'nowrap' } }, '开发者:程序员少北晨'),
201
- h('div', { style: { whiteSpace: 'nowrap' } }, '⭐ 顺手留颗 Star,作者能高兴一整天'),
213
+ h('div', { style: { whiteSpace: 'nowrap' } }, t('developer')),
214
+ h('div', { style: { whiteSpace: 'nowrap' } }, t('starAsk')),
202
215
  h('a', { href: 'https://github.com/shaobeichen/dsh-pocket', target: '_blank', rel: 'noreferrer', style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)', fontSize: 12, lineHeight: 1.6, textDecoration: 'underline' } },
203
- '行,给你一颗 Star'),
216
+ t('starCta')),
204
217
  ),
205
218
  ),
206
219
 
@@ -209,10 +222,10 @@ function PocketSettingsTab({ rpcCall }) {
209
222
  // 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
210
223
  !isDesktop && restartNotice ? h('div', { style: { ...styles.block, borderLeft: '4px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
211
224
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
212
- h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
213
- h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
225
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('restarted')),
226
+ h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, t('ok')),
214
227
  ),
215
- h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, `进程在后台运行(不挂终端)。如需停止:${status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`}`),
228
+ h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, fmt(t, 'bgHint', { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` })),
216
229
  ) : null,
217
230
 
218
231
  // 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
@@ -221,85 +234,83 @@ function PocketSettingsTab({ rpcCall }) {
221
234
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
222
235
  h('div', { style: { fontWeight: 600, fontSize: 13 } },
223
236
  updateInfo.updated
224
- ? `✅ 已更新 v${updateInfo.current},重启生效 | Updated — restart to apply`
237
+ ? fmt(t, 'updatedRestart', { ver: updateInfo.current })
225
238
  : updateInfo.result === 'ok'
226
- ? (updateInfo.autoRestart ? `✅ 已更新 v${updateInfo.latest},正在自动重启… | updated restarting…` : `✅ 已更新 v${updateInfo.latest} | Updated`)
227
- : `📦 新版本 v${updateInfo.latest} | Update available`),
239
+ ? (updateInfo.autoRestart ? fmt(t, 'updateAutoRestarting', { ver: updateInfo.latest }) : fmt(t, 'updatedOk', { ver: updateInfo.latest }))
240
+ : fmt(t, 'updateAvailable', { ver: updateInfo.latest })),
228
241
  updateInfo.result !== 'ok'
229
- ? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? '更新中…' : `更新到 v${updateInfo.latest} | Update`)
242
+ ? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t('updating') : fmt(t, 'updateTo', { ver: updateInfo.latest }))
230
243
  : updateInfo.autoRestart
231
- ? h('button', { style: styles.btn, disabled: true }, '正在重启生效… | restarting…')
232
- : h('button', { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? '重启中…' : '🔄 重启 dsh web 生效 | Restart now'),
244
+ ? h('button', { style: styles.btn, disabled: true }, t('restartingNow'))
245
+ : h('button', { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t('restarting') : t('restartNow')),
233
246
  ),
234
247
  h('div', { style: styles.muted, marginTop: 4 },
235
248
  updateInfo.updating
236
- ? `⏳ 更新中(通常 1-2 分钟)· 已等待 ${elapsed(updateInfo.startedAt)} 秒 | updating (usually 1-2 min) · ${elapsed(updateInfo.startedAt)}s`
249
+ ? fmt(t, 'updatingDetail', { s: elapsed(updateInfo.startedAt) })
237
250
  : updateInfo.restarting
238
- ? `⏳ 正在重启生效(通常 10-30 秒)· 已等待 ${elapsed(updateInfo.startedAt)} 秒 | restarting (usually 10-30s) · ${elapsed(updateInfo.startedAt)}s`
251
+ ? fmt(t, 'restartingDetail', { s: elapsed(updateInfo.startedAt) })
239
252
  : updateInfo.result === 'ok'
240
- ? (updateInfo.autoRestart ? '✅ 已更新,正在自动重启生效,请稍候刷新 | updated — restarting automatically, refresh shortly'
241
- : '✅ 已更新,重启 dsh web 生效 | updated — restart dsh web')
242
- : updateInfo.result === 'fail' ? `❌ 失败:${updateInfo.output || '未知'}(手动更新:dsh plugin --profile web update dsh-pocket --latest -w)`
243
- : `当前 v${updateInfo.current} 最新 v${updateInfo.latest}`),
253
+ ? (updateInfo.autoRestart ? t('updatedAutoDetail')
254
+ : t('updatedRestartDetail'))
255
+ : updateInfo.result === 'fail' ? fmt(t, 'updateFailed', { err: updateInfo.output || t('unknownError') })
256
+ : fmt(t, 'versionRange', { cur: updateInfo.current, latest: updateInfo.latest })),
244
257
  ) : null,
245
258
 
246
259
  // 局域网
247
260
  h('div', { style: styles.block },
248
- h('div', { style: { fontWeight: 600, fontSize: 13 } }, '📶 局域网(同一 WiFi)| LAN'),
261
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('lanTitle')),
249
262
  lanUrl
250
263
  ? h('div', null,
251
264
  h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
252
265
  h('div', { style: styles.code }, lanUrl),
253
- h('div', { style: styles.muted }, '手机连接同一 WiFi 后扫码即可打开'),
266
+ h('div', { style: styles.muted }, t('lanHint')),
254
267
  // 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
255
268
  h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
256
- h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, '局域网访问密码 | LAN access PIN'),
269
+ h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, t('lanPin')),
257
270
  h('button', {
258
271
  style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanAuthEnabled !== false ? 600 : 400, background: status?.lanAuthEnabled !== false ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanAuthEnabled !== false ? 'var(--dsw-alias-label-primary-foreground, #fff)' : 'var(--dsw-alias-label-primary,inherit)' },
259
272
  onClick: () => setLanAuth(true),
260
- }, '开 | On'),
273
+ }, t('on')),
261
274
  h('button', {
262
275
  style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanAuthEnabled === false ? 600 : 400, background: status?.lanAuthEnabled === false ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanAuthEnabled === false ? '#fff' : 'var(--dsw-alias-label-primary,inherit)' },
263
276
  onClick: () => setLanAuth(false),
264
- }, '关 | Off'),
277
+ }, t('off')),
265
278
  ),
266
279
  status?.lanAuthEnabled !== false
267
280
  ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
268
- '🔐 访问密码:',
269
- status.lanToken,
270
- '(手机打开需输入;与公网密码分开)',
271
- h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, '刷新 | Refresh'),
281
+ fmt(t, 'lanPinValue', { pin: status.lanToken }),
282
+ h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, t('refresh')),
272
283
  )
273
284
  : h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } },
274
- '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)| PIN off — scan & go (LAN only; public still requires PIN)'),
285
+ t('lanPinOff')),
275
286
  )
276
- : h('div', { style: styles.muted }, '代理未就绪… | proxy starting…'),
287
+ : h('div', { style: styles.muted }, t('lanStarting')),
277
288
  ),
278
289
 
279
290
  // 公网
280
291
  h('div', { style: styles.block },
281
- h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🌐 公网(人在外面)| Anywhere'),
292
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('wanTitle')),
282
293
  tunnelUrl
283
294
  ? h('div', null,
284
295
  h('img', { src: status.tunnelQr, alt: 'Tunnel QR', style: styles.qr }),
285
296
  h('div', { style: styles.code }, tunnelUrl),
286
- h('div', { style: styles.muted }, '任何网络扫码即用(URL 每次重启自动换新)'),
297
+ h('div', { style: styles.muted }, t('wanHint')),
287
298
  status.accessToken
288
299
  ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
289
- `🔐 访问密码:${status.accessToken}(每次开启公网变新;手机打开链接需输入此密码)| PIN: ${status.accessToken} — required on the phone`)
300
+ fmt(t, 'wanPin', { pin: status.accessToken }))
290
301
  : null,
291
- h('button', { style: styles.btn, onClick: stopTunnel }, '关闭公网 | Stop'),
302
+ h('button', { style: styles.btn, onClick: stopTunnel }, t('stopTunnel')),
292
303
  )
293
304
  : h('div', null,
294
- h('button', { style: { ...styles.primary, margin: '8px 0' }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? '开启中…' : '开启公网访问 | Enable anywhere'),
305
+ h('button', { style: { ...styles.primary, margin: '8px 0' }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? t('opening') : t('enable')),
295
306
  tunnelStarting
296
307
  ? h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
297
308
  tunnelPhase === 'downloading'
298
- ? `⏳ 下载 cloudflared(首次约 20-50MB,通常 1-2 分钟;之后秒开)· 已等待 ${elapsed(tunnelStateStarted)} 秒`
299
- : `⏳ 连接 Cloudflare 边缘(通常 5-30 秒)· 已等待 ${elapsed(tunnelStateStarted)} 秒${elapsed(tunnelStateStarted) > 30 ? ' — 有点久?检查是否开着代理/VPN(Clash TUN 等)' : ''}`)
309
+ ? fmt(t, 'downloading', { s: elapsed(tunnelStateStarted) })
310
+ : fmt(t, 'connecting', { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t('slowHint') : '' }))
300
311
  : tunnelPhase === 'error'
301
312
  ? h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } },
302
- `❌ 开启失败:${tunnelStateDetail || '未知错误 | failed'}(可重试;若是代理/VPN 问题见 README 排障)`)
313
+ fmt(t, 'error', { detail: tunnelStateDetail || t('unknownError') }))
303
314
  : null,
304
315
  ),
305
316
  ),
@@ -309,7 +320,7 @@ function PocketSettingsTab({ rpcCall }) {
309
320
  // 页面最底部:反馈入口
310
321
  h('div', { style: { ...styles.block, textAlign: 'center' } },
311
322
  h('a', { href: 'https://github.com/shaobeichen/dsh-pocket/issues', target: '_blank', rel: 'noreferrer', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', textDecoration: 'none' } },
312
- '有问题?欢迎到 GitHub Issues 反馈 🙏 | Questions? Open an issue on GitHub'),
323
+ t('feedback')),
313
324
  ),
314
325
  );
315
326
  }
@@ -321,6 +332,10 @@ export function apply(ctx) {
321
332
  const rpcCall = (endpoint, payload, signal) =>
322
333
  ctx.connection.rpc.call(POCKET_RPC_CHANNEL, endpoint, payload, signal);
323
334
 
335
+ // 设置页签接入 DSH 本地化:注册 pocket 词典(zh/en),并绑定一个随当前 locale 切换的 t()。
336
+ const translate = ctx.locale.bind(POCKET_NS);
337
+ ctx.effect(() => ctx.locale.register(POCKET_NS, { zh: POCKET_ZH, en: POCKET_EN }), 'dsh-pocket: pocket locale dictionaries');
338
+
324
339
  // 设置一级入口(与 通用设置/模型/插件 同级,order 1 = 通用之后、最外层)
325
340
  ctx.slots.inject('settings.section', () =>
326
341
  ctx.slots.register(
@@ -328,8 +343,8 @@ export function apply(ctx) {
328
343
  name: 'settings.section',
329
344
  id: 'pocket',
330
345
  order: 1,
331
- label: () => '手机访问',
332
- inject: () => ({ rpcCall }),
346
+ label: () => translate('section'),
347
+ inject: () => ({ rpcCall, t: translate }),
333
348
  },
334
349
  PocketSettingsTab,
335
350
  ),
@@ -0,0 +1,101 @@
1
+ // dsh-pocket 设置页签 i18n 词典(与 client/mobile/locales.ts 同款:zh 为源真,en 为译文)。
2
+ // 需要新增字符串时:先加到 zh(key-set 唯一来源),再补 en(key 必须与 zh 完全一致)。
3
+ export const NS = 'pocket'
4
+
5
+ /** Simplified Chinese dictionary (the key-set source of truth). */
6
+ export const zh = {
7
+ 'section': '手机访问',
8
+ 'title': '📱 手机访问',
9
+ 'subtitle': '手机扫码打开的就是电脑上的这个界面,实时同步',
10
+ 'developer': '开发者:程序员少北晨',
11
+ 'starAsk': '⭐ 顺手留颗 Star,作者能高兴一整天',
12
+ 'starCta': '行,给你一颗 Star',
13
+ 'restarted': '🔄 已重启',
14
+ 'ok': '知道了',
15
+ 'bgHint': '进程在后台运行(不挂终端)。如需停止:{cmd}',
16
+ 'updatedRestart': '✅ 已更新 v{ver},重启生效',
17
+ 'updateAutoRestarting': '✅ 已更新 v{ver},正在自动重启…',
18
+ 'updatedOk': '✅ 已更新 v{ver}',
19
+ 'updateAvailable': '📦 新版本 v{ver}',
20
+ 'updating': '更新中…',
21
+ 'updateTo': '更新到 v{ver}',
22
+ 'restartingNow': '正在重启生效…',
23
+ 'restarting': '重启中…',
24
+ 'restartNow': '🔄 重启 dsh web 生效',
25
+ 'updatingDetail': '⏳ 更新中(通常 1-2 分钟)· 已等待 {s} 秒',
26
+ 'restartingDetail': '⏳ 正在重启生效(通常 10-30 秒)· 已等待 {s} 秒',
27
+ 'updatedAutoDetail': '✅ 已更新,正在自动重启生效,请稍候刷新',
28
+ 'updatedRestartDetail': '✅ 已更新,重启 dsh web 生效',
29
+ 'updateFailed': '❌ 失败:{err}(手动更新:dsh plugin --profile web update dsh-pocket --latest -w)',
30
+ 'versionRange': '当前 v{cur} → 最新 v{latest}',
31
+ 'lanTitle': '📶 局域网(同一 WiFi)',
32
+ 'lanHint': '手机连接同一 WiFi 后扫码即可打开',
33
+ 'lanPin': '局域网访问密码',
34
+ 'on': '开',
35
+ 'off': '关',
36
+ 'lanPinValue': '🔐 访问密码:{pin}(手机打开需输入;与公网密码分开)',
37
+ 'refresh': '刷新',
38
+ 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)',
39
+ 'lanStarting': '代理未就绪…',
40
+ 'wanTitle': '🌐 公网(人在外面)',
41
+ 'wanHint': '任何网络扫码即用(URL 每次重启自动换新)',
42
+ 'wanPin': '🔐 访问密码:{pin}(每次开启公网变新;手机打开链接需输入此密码)',
43
+ 'stopTunnel': '关闭公网',
44
+ 'enable': '开启公网访问',
45
+ 'opening': '开启中…',
46
+ 'downloading': '⏳ 下载 cloudflared(首次约 20-50MB,通常 1-2 分钟;之后秒开)· 已等待 {s} 秒',
47
+ 'connecting': '⏳ 连接 Cloudflare 边缘(通常 5-30 秒)· 已等待 {s} 秒{suffix}',
48
+ 'slowHint': ' — 有点久?检查是否开着代理/VPN(Clash TUN 等)',
49
+ 'error': '❌ 开启失败:{detail}(可重试;若是代理/VPN 问题见 README 排障)',
50
+ 'unknownError': '未知错误',
51
+ 'feedback': '有问题?欢迎到 GitHub Issues 反馈 🙏',
52
+ }
53
+
54
+ /** English dictionary, key-identical to the Chinese source of truth. */
55
+ export const en = {
56
+ 'section': 'Phone access',
57
+ 'title': '📱 Phone access',
58
+ 'subtitle': 'The phone shows this exact screen, live',
59
+ 'developer': 'Developer: 少北晨 (shaobeichen)',
60
+ 'starAsk': '⭐ Drop a Star if it helped — it makes the author’s day',
61
+ 'starCta': '★ Give a Star',
62
+ 'restarted': '🔄 Restarted',
63
+ 'ok': 'Got it',
64
+ 'bgHint': 'Running in the background (not attached to a terminal). To stop: {cmd}',
65
+ 'updatedRestart': '✅ Updated to v{ver} — restart to apply',
66
+ 'updateAutoRestarting': '✅ Updated to v{ver} — auto-restarting…',
67
+ 'updatedOk': '✅ Updated to v{ver}',
68
+ 'updateAvailable': '📦 Update available: v{ver}',
69
+ 'updating': 'Updating…',
70
+ 'updateTo': 'Update to v{ver}',
71
+ 'restartingNow': 'Restarting to apply…',
72
+ 'restarting': 'Restarting…',
73
+ 'restartNow': '🔄 Restart dsh web now',
74
+ 'updatingDetail': '⏳ Updating (usually 1-2 min) · {s}s elapsed',
75
+ 'restartingDetail': '⏳ Restarting to apply (usually 10-30s) · {s}s elapsed',
76
+ 'updatedAutoDetail': '✅ Updated — auto-restarting in progress, refresh shortly',
77
+ 'updatedRestartDetail': '✅ Updated — restart dsh web to apply',
78
+ 'updateFailed': '❌ Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket --latest -w)',
79
+ 'versionRange': 'Current v{cur} → latest v{latest}',
80
+ 'lanTitle': '📶 LAN (same Wi-Fi)',
81
+ 'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
82
+ 'lanPin': 'LAN access PIN',
83
+ 'on': 'On',
84
+ 'off': 'Off',
85
+ 'lanPinValue': '🔐 PIN: {pin} (required on the phone; separate from the public PIN)',
86
+ 'refresh': 'Refresh',
87
+ 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)',
88
+ 'lanStarting': 'Proxy starting…',
89
+ 'wanTitle': '🌐 Anywhere (public)',
90
+ 'wanHint': 'Scan from any network (the URL changes on every restart)',
91
+ 'wanPin': '🔐 PIN: {pin} (changes each time the tunnel is enabled; required on the phone)',
92
+ 'stopTunnel': 'Stop',
93
+ 'enable': 'Enable anywhere',
94
+ 'opening': 'Enabling…',
95
+ 'downloading': '⏳ Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) · {s}s elapsed',
96
+ 'connecting': '⏳ Connecting to Cloudflare edge (usually 5-30s) · {s}s elapsed{suffix}',
97
+ 'slowHint': ' — taking long? Check for a proxy/VPN (e.g., Clash TUN)',
98
+ 'error': '❌ Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)',
99
+ 'unknownError': 'unknown error',
100
+ 'feedback': '🙏 Questions? Open an issue on GitHub',
101
+ }
package/package.json CHANGED
@@ -76,5 +76,5 @@
76
76
  "access": "public",
77
77
  "registry": "https://registry.npmjs.org/"
78
78
  },
79
- "version": "1.9.2"
79
+ "version": "1.10.0"
80
80
  }