diff --git a/ci/check-codegen-drift.sh b/ci/check-codegen-drift.sh index 4c686bf..8df6462 100755 --- a/ci/check-codegen-drift.sh +++ b/ci/check-codegen-drift.sh @@ -46,3 +46,15 @@ if ! git diff --quiet -- "$L10N_GEN"/strings_*.dart; then exit 1 fi echo "✅ l10n 无漂移:strings_*.dart 与 strings.json 单源一致。" + +# ── 原型 i18n 单源:strings.json + alias + overlay → 两份 HTML 的 I18N 块 ── +# --check 只比对 marker 块(unrelated HTML 编辑不误报);再跑 key resolution 验收。 +echo "→ 校验原型 I18N 块 (design/codegen/gen_proto_i18n.mjs --check) ..." +if ! node design/codegen/gen_proto_i18n.mjs --check; then + echo "❌ 原型 I18N 漂移:改了 strings.json/alias/overlay 却没重生成原型块。" >&2 + echo " 修复:node design/codegen/gen_proto_i18n.mjs 后提交。" >&2 + exit 1 +fi +echo "→ 校验原型 data-i18n key 全 resolve (check-proto-i18n.mjs) ..." +node design/prototype/tools/check-proto-i18n.mjs +echo "✅ 原型 i18n 无漂移:I18N 块与单源一致、data-i18n 全 resolve。" diff --git a/client/lib/l10n/app_text.dart b/client/lib/l10n/app_text.dart index f171cf6..07c60d3 100644 --- a/client/lib/l10n/app_text.dart +++ b/client/lib/l10n/app_text.dart @@ -628,4 +628,9 @@ abstract class AppText { String get settingsGroupConn; // 连接 / Connection String get settingsGroupAppearance;// 外观 / Appearance String get settingsGroupAbout; // 关于 / About + // ── 分流规则(configurable proxy)── + String get routingRulesTitle; // 分流规则 / Routing rules + String get routingRulesSub; // 自定义直连 / 走隧道 / 拒绝 + String get routingLanDirect; // 局域网 / 私网直连 / LAN & private direct + String get routingLanForced; // 强制,不可关 / Always on } diff --git a/client/lib/l10n/strings_en.dart b/client/lib/l10n/strings_en.dart index 5644b52..0d01a14 100644 --- a/client/lib/l10n/strings_en.dart +++ b/client/lib/l10n/strings_en.dart @@ -503,6 +503,14 @@ class StringsEn extends AppText { @override String get promoOriginalPrice => 'Was'; @override + String get routingRulesTitle => 'Routing rules'; + @override + String get routingRulesSub => 'Custom direct / tunnel / reject'; + @override + String get routingLanDirect => 'LAN & private direct'; + @override + String get routingLanForced => 'Always on'; + @override List get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S']; @override List get featsFree => const ['1 basic node only', '10 min per day', 'Watch an ad to start', '7-day free trial on sign-up']; diff --git a/client/lib/l10n/strings_es.dart b/client/lib/l10n/strings_es.dart index 29c7bf6..37441fc 100644 --- a/client/lib/l10n/strings_es.dart +++ b/client/lib/l10n/strings_es.dart @@ -503,6 +503,14 @@ class StringsEs extends AppText { @override String get promoOriginalPrice => 'Antes'; @override + String get routingRulesTitle => 'Reglas de enrutamiento'; + @override + String get routingRulesSub => 'Personalizar: directo / túnel / rechazar'; + @override + String get routingLanDirect => 'LAN y red privada directas'; + @override + String get routingLanForced => 'Siempre activo'; + @override List get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D']; @override List get featsFree => const ['Solo 1 nodo básico', '10 min al día', 'Mira un anuncio para empezar', 'Prueba gratis de 7 días al registrarte']; diff --git a/client/lib/l10n/strings_ja.dart b/client/lib/l10n/strings_ja.dart index 3d1ff33..c3d0893 100644 --- a/client/lib/l10n/strings_ja.dart +++ b/client/lib/l10n/strings_ja.dart @@ -503,6 +503,14 @@ class StringsJa extends AppText { @override String get promoOriginalPrice => '通常価格'; @override + String get routingRulesTitle => 'ルーティングルール'; + @override + String get routingRulesSub => '直接接続 / トンネル / 拒否をカスタム'; + @override + String get routingLanDirect => 'LAN・プライベート直接接続'; + @override + String get routingLanForced => '常時オン'; + @override List get days7 => const ['月', '火', '水', '木', '金', '土', '日']; @override List get featsFree => const ['基本ノード1つのみ', '1日10分', '広告を見て開始', '登録で7日間無料トライアル']; diff --git a/client/lib/l10n/strings_ko.dart b/client/lib/l10n/strings_ko.dart index 1072f41..378cb02 100644 --- a/client/lib/l10n/strings_ko.dart +++ b/client/lib/l10n/strings_ko.dart @@ -503,6 +503,14 @@ class StringsKo extends AppText { @override String get promoOriginalPrice => '정가'; @override + String get routingRulesTitle => '라우팅 규칙'; + @override + String get routingRulesSub => '직접 연결 / 터널 / 거부 사용자 지정'; + @override + String get routingLanDirect => 'LAN / 사설망 직접 연결'; + @override + String get routingLanForced => '항상 켜짐'; + @override List get days7 => const ['월', '화', '수', '목', '금', '토', '일']; @override List get featsFree => const ['기본 노드 1개', '하루 10분', '광고 시청 후 시작', '가입 시 7일 무료 체험']; diff --git a/client/lib/l10n/strings_ru.dart b/client/lib/l10n/strings_ru.dart index b51c674..494d189 100644 --- a/client/lib/l10n/strings_ru.dart +++ b/client/lib/l10n/strings_ru.dart @@ -503,6 +503,14 @@ class StringsRu extends AppText { @override String get promoOriginalPrice => 'Было'; @override + String get routingRulesTitle => 'Правила маршрутизации'; + @override + String get routingRulesSub => 'Настройка: напрямую / туннель / отклонить'; + @override + String get routingLanDirect => 'Локальная и частная сеть напрямую'; + @override + String get routingLanForced => 'Всегда включено'; + @override List get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; @override List get featsFree => const ['Только 1 базовый узел', '10 минут в день', 'Смотрите рекламу, чтобы начать', '7 дней бесплатно при регистрации']; diff --git a/client/lib/l10n/strings_zh.dart b/client/lib/l10n/strings_zh.dart index 851c752..855db05 100644 --- a/client/lib/l10n/strings_zh.dart +++ b/client/lib/l10n/strings_zh.dart @@ -503,6 +503,14 @@ class StringsZh extends AppText { @override String get promoOriginalPrice => '原价'; @override + String get routingRulesTitle => '分流规则'; + @override + String get routingRulesSub => '自定义直连 / 走隧道 / 拒绝'; + @override + String get routingLanDirect => '局域网 / 私网直连'; + @override + String get routingLanForced => '强制,不可关'; + @override List get days7 => const ['一', '二', '三', '四', '五', '六', '日']; @override List get featsFree => const ['仅 1 个基础节点', '每日 10 分钟时长', '使用前观看广告', '注册享 7 天免费试用']; diff --git a/design/codegen/gen_proto_i18n.mjs b/design/codegen/gen_proto_i18n.mjs new file mode 100644 index 0000000..6b9a352 --- /dev/null +++ b/design/codegen/gen_proto_i18n.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +// gen_proto_i18n.mjs — 原型 i18n 单源 codegen(i18n 单源工程 ③) +// +// 让 design/prototype/screens/{ui-desktop,ui-mobile}.html 里的 `I18N` 数据块 +// 不再手写,而是从 **翻译单源** 生成,消除与 Flutter app 的重复: +// 产品串 → design/i18n/strings.json 的 getters(取 zh/en;单源赢) +// 原型专属 → design/prototype/i18n/overlay.json(开发挡板 + app 没有的原型串,zh/en) +// 映射关系 → design/prototype/i18n/alias.json(protoKey → flutterKey) +// +// 输出各按该文件原生 shape: +// desktop per-key : var I18N = { 'k':{zh:'..',en:'..'}, ... }; +// mobile per-lang: const I18N = { zh:{ k:'..', ... }, en:{ k:'..', ... } }; +// 只重写 `// <<>>` ... `// <<>>` 之间的内容,其余(HTML_KEYS +// 等)保持原样。此后 I18N 块**只由本生成器写,禁手改**。 +// +// 用法: node design/codegen/gen_proto_i18n.mjs 写入 +// node design/codegen/gen_proto_i18n.mjs --check 只校验(block 漂移即非零退出) +// 确定性: 键按字典序排序,zh/en 两语键集与顺序一致 → 重跑零 diff。 +// --check: 只比对 marker 块(before/after 取自当前文件,unrelated HTML 编辑两侧抵消,不误报)。 + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..'); +const STRINGS = path.join(ROOT, 'design/i18n/strings.json'); +const I18N_DIR = path.join(ROOT, 'design/prototype/i18n'); +const SCREENS = path.join(ROOT, 'design/prototype/screens'); + +const START = '// <<>>'; +const END = '// <<>>'; +const LANGS = ['zh', 'en']; + +const strings = JSON.parse(fs.readFileSync(STRINGS, 'utf8')); +const getters = strings.getters; +const alias = JSON.parse(fs.readFileSync(path.join(I18N_DIR, 'alias.json'), 'utf8')); +const overlay = JSON.parse(fs.readFileSync(path.join(I18N_DIR, 'overlay.json'), 'utf8')); + +// 单引号 JS 字面量转义(值可能含
/emoji/{占位}/单引号) +function esc(s) { + return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\r?\n/g, '\\n'); +} +// mobile 裸标识符键无需引号;含 . 等的键(如 rr.forced)加引号 +function keyLit(k) { + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : `'${k}'`; +} + +// 解析某文件的 { protoKey: {zh,en} } 完整字典(alias 从单源取,overlay 从 overlay 取) +function resolve(file) { + const a = alias[file] || {}; + const o = overlay[file] || {}; + const out = {}; + const errs = []; + for (const [pk, fk] of Object.entries(a)) { + if (!getters[fk]) { errs.push(`alias ${file}.${pk} → 未知 flutter key '${fk}'(不在 strings.json.getters)`); continue; } + out[pk] = { zh: getters[fk].zh, en: getters[fk].en }; + } + for (const [pk, v] of Object.entries(o)) { + if (out[pk]) { errs.push(`键冲突 ${file}.${pk} 同时在 alias 与 overlay`); continue; } + out[pk] = { zh: v.zh, en: v.en }; + } + if (errs.length) { console.error('✗ ' + errs.join('\n✗ ')); process.exit(1); } + return out; +} + +function emitDesktop(dict) { + const keys = Object.keys(dict).sort(); + const L = ['var I18N = {']; + keys.forEach((k, i) => { + const comma = i === keys.length - 1 ? '' : ','; + L.push(` '${k}':{zh:'${esc(dict[k].zh)}',en:'${esc(dict[k].en)}'}${comma}`); + }); + L.push('};'); + return L.join('\n'); +} + +function emitMobile(dict) { + const keys = Object.keys(dict).sort(); + const L = ['const I18N = {']; + LANGS.forEach((lang, li) => { + L.push(` ${lang}:{`); + keys.forEach((k, i) => { + const comma = i === keys.length - 1 ? '' : ','; + L.push(` ${keyLit(k)}:'${esc(dict[k][lang])}'${comma}`); + }); + L.push(li === LANGS.length - 1 ? ' }' : ' },'); + }); + L.push('};'); + return L.join('\n'); +} + +const CHECK = process.argv.includes('--check'); + +function rewrite(fileName, emitted) { + const p = path.join(SCREENS, fileName); + const src = fs.readFileSync(p, 'utf8'); + const s = src.indexOf(START); + const e = src.indexOf(END); + if (s < 0 || e < 0 || e < s) { + console.error(`✗ ${fileName}: 缺少 ${START} / ${END} marker`); + process.exit(1); + } + const lineEnd = src.indexOf('\n', s) + 1; // START 标记行之后 + const before = src.slice(0, lineEnd); + const after = src.slice(e); // 从 END 标记开始保留 + const next = before + emitted + '\n' + after; + if (CHECK) return next !== src; // true = block 漂移 + fs.writeFileSync(p, next); + return false; +} + +const desktopDict = resolve('desktop'); +const mobileDict = resolve('mobile'); +const drifted = [ + rewrite('ui-desktop.html', emitDesktop(desktopDict)) && 'ui-desktop.html', + rewrite('ui-mobile.html', emitMobile(mobileDict)) && 'ui-mobile.html', +].filter(Boolean); + +if (CHECK) { + if (drifted.length) { + console.error(`✗ 原型 I18N 块漂移(与 strings.json/alias/overlay 不一致):${drifted.join(', ')}`); + console.error(' 修复:node design/codegen/gen_proto_i18n.mjs 后提交。'); + process.exit(1); + } + console.log('✓ 原型 I18N 块与单源一致(无漂移)'); +} else { + console.log(`✓ desktop I18N: ${Object.keys(desktopDict).length} keys · mobile I18N: ${Object.keys(mobileDict).length} keys`); +} diff --git a/design/i18n/strings.json b/design/i18n/strings.json index 89aa60d..255087f 100644 --- a/design/i18n/strings.json +++ b/design/i18n/strings.json @@ -1968,6 +1968,38 @@ "ko": "정가", "ru": "Было", "es": "Antes" + }, + "routingRulesTitle": { + "zh": "分流规则", + "en": "Routing rules", + "ja": "ルーティングルール", + "ko": "라우팅 규칙", + "ru": "Правила маршрутизации", + "es": "Reglas de enrutamiento" + }, + "routingRulesSub": { + "zh": "自定义直连 / 走隧道 / 拒绝", + "en": "Custom direct / tunnel / reject", + "ja": "直接接続 / トンネル / 拒否をカスタム", + "ko": "직접 연결 / 터널 / 거부 사용자 지정", + "ru": "Настройка: напрямую / туннель / отклонить", + "es": "Personalizar: directo / túnel / rechazar" + }, + "routingLanDirect": { + "zh": "局域网 / 私网直连", + "en": "LAN & private direct", + "ja": "LAN・プライベート直接接続", + "ko": "LAN / 사설망 직접 연결", + "ru": "Локальная и частная сеть напрямую", + "es": "LAN y red privada directas" + }, + "routingLanForced": { + "zh": "强制,不可关", + "en": "Always on", + "ja": "常時オン", + "ko": "항상 켜짐", + "ru": "Всегда включено", + "es": "Siempre activo" } }, "lists": { diff --git a/design/prototype/i18n/alias.json b/design/prototype/i18n/alias.json new file mode 100644 index 0000000..b3abc73 --- /dev/null +++ b/design/prototype/i18n/alias.json @@ -0,0 +1,279 @@ +{ + "desktop": { + "nav.nodes": "tabServers", + "nav.stats": "tabStats", + "nav.settings": "settingsTitle", + "nav.orders": "ordersTitle", + "nav.notifications": "notifTitle", + "nav.invite": "inviteTitle", + "acc.buy": "purchaseTitle", + "acc.orders": "ordersTitle", + "acc.invite": "inviteEntry", + "acc.inviteSub": "inviteEntrySub", + "nt.title": "notifTitle", + "nt.tImportant": "notifTypeImportant", + "nt.tFeature": "notifTypeFeature", + "nt.tNews": "notifTypeNews", + "nt.tVersion": "notifTypeVersion", + "inv.title": "inviteTitle", + "inv.progHead": "inviteProgressTitle", + "inv.invited": "inviteStatInvited", + "inv.converted": "inviteStatConverted", + "inv.earned": "inviteStatDays", + "inv.tasksHead": "inviteTasksTitle", + "inv.tgTitle": "inviteJoinTgTitle", + "inv.tgJoinBtn": "inviteJoinTgBtn", + "inv.tgVerifyBtn": "inviteVerifyBtn", + "inv.tgDoneBtn": "inviteTaskDone", + "set.orders": "ordersTitle", + "ord.kOrderNo": "orderNoLabel", + "ord.kPlan": "orderPlanLabel", + "ord.kAmount": "orderAmountLabel", + "ord.kMethod": "orderMethodLabel", + "ord.kCreated": "orderCreatedLabel", + "ord.actContinue": "orderContinuePay", + "ord.actCancel": "cancelOrder", + "ord.actRefresh": "orderRefresh", + "ord.actBuyAgain": "orderRebuy", + "ord.copied": "copied", + "pur.channel": "payChannel", + "pur.chanUsdt": "payChannelUsdt", + "pur.chanCny": "payChannelCny", + "pur.pop": "mostPopular", + "pur.buy": "buyNow", + "pur.network": "payNetworkLabel", + "pur.address": "payAddressLabel", + "pur.cancel": "cancelOrder", + "pur.done": "payDone", + "pur.failTitle": "payFailed", + "brand.name": "brand", + "q.remain": "quotaToday", + "nodes.smart": "smartSelect", + "nodes.rec": "recommended", + "stats.month": "periodMonth", + "stats.week": "periodWeek", + "stats.today": "periodToday", + "stats.down": "statDown", + "stats.up": "statUp", + "stats.chart": "chartTwoWeeks", + "stats.allDevices": "allDevices", + "acc.info": "accInfoTitle", + "acc.change": "accChange", + "acc.myDevices": "myDevices", + "acc.lang": "language", + "acc.dark": "darkAppearance", + "acc.protocol": "protocol", + "acc.upgrade": "upgradeBtn", + "acc.logout": "signOut", + "set.redeem": "redeemTitle", + "set.redeemSub": "proMember", + "set.autostart": "autostart", + "set.autostartSub": "autostartSub", + "set.autoconnect": "autoConnect", + "set.autoconnectSub": "autoConnectSub", + "set.smart": "smartRoute", + "set.routeModeSmart": "smartRoute", + "set.lang": "language", + "set.dark": "darkAppearance", + "set.protocol": "protocol", + "set.grpAppear": "settingsGroupAppearance", + "set.grpAbout": "settingsGroupAbout", + "set.update": "checkUpdate", + "contact.web": "contactWebsite", + "contact.hoursTitle": "contactHoursTitle", + "contact.hours": "contactHours", + "dl.title": "deviceLimitTitle", + "dl.remove": "deviceLimitRemoveOldest", + "dl.logout": "signOut", + "dev.current": "thisDevice", + "dev.remove": "remove", + "dev.lastOnline": "devLastOnline", + "dev.mRename": "devRename", + "dev.mForceLogout": "devForceLogout", + "dev.mClearLogin": "devClearLogin", + "state.on": "stateOn", + "state.off": "followLight", + "nav.connect": "tabConnect", + "ord.kValid": "payExpiresAt", + "acc.email": "accEmail", + "acc.pwd": "accPassword", + "set.grpConn": "settingsGroupConn", + "contact.email": "contactEmail", + "login.email": "emailLabel", + "login.pwd": "pwLabel", + "login.signin": "doLogin", + "nav.contact": "contactEntry", + "nav.account": "tabMe", + "nav.purchase": "purchaseTitle", + "nav.devices": "myDevices", + "nav.redeem": "redeemCodeTitle", + "acc.redeem": "redeemCodeTitle", + "nt.tReward": "notifTypeReward", + "nt.tPromo": "notifTypePromo", + "inv.rewardsHead": "inviteRewardsTitle", + "rd.title": "redeemCodeTitle", + "rd.ph": "redeemPh", + "rd.btn": "redeemBtn", + "pur.amount": "payAmountLabel", + "pur.switch": "switchPayMethod", + "pur.okTitle": "paySucceeded", + "pur.retry": "payRetry", + "nodes.smartSub": "smartSub", + "stats.dur": "statDuration", + "acc.devices": "myDevices", + "set.smartSub": "smartRouteSub", + "set.kill": "killSwitch", + "set.killSub": "killSwitchSub", + "set.web": "webUserCenter", + "contact.intro": "contactIntro", + "login.signupTitle": "tabRegister", + "nav.routing": "routingRulesTitle", + "set.routing": "routingRulesTitle", + "set.routingSub": "routingRulesSub" + }, + "mobile": { + "brandName": "brand", + "dlTitle": "deviceLimitTitle", + "dlRemoveLru": "deviceLimitRemoveOldest", + "dlRemove": "remove", + "dlOnline": "devOnline", + "navNodes": "tabServers", + "navStats": "tabStats", + "rateDown": "download", + "rateUp": "upload", + "latency": "latency", + "searchPh": "searchPh", + "smartTitle": "smartSelect", + "recommend": "recommended", + "periodMonth": "periodMonth", + "periodWeek": "periodWeek", + "periodToday": "periodToday", + "statDur": "statDuration", + "chartTwoWeeks": "chartTwoWeeks", + "upgrade": "upgradeBtn", + "renew": "upgradeBtn", + "secAccount": "accInfoTitle", + "accChange": "accChange", + "myDevices": "myDevices", + "thisDevice": "thisDevice", + "lastOnline": "devLastOnline", + "devRename": "devRename", + "devForceLogout": "devForceLogout", + "devClearLogin": "devClearLogin", + "prefLang": "language", + "prefTheme": "darkAppearance", + "prefProtocol": "protocol", + "followLight": "followLight", + "stateOn": "stateOn", + "navSettings": "settingsTitle", + "navRedeem": "redeemTitle", + "logout": "signOut", + "navOrders": "ordersTitle", + "ordDetailTitle": "orderDetailTitle", + "kOrderNo": "orderNoLabel", + "kPlan": "orderPlanLabel", + "kAmount": "orderAmountLabel", + "kMethod": "orderMethodLabel", + "kCreated": "orderCreatedLabel", + "actContinue": "orderContinuePay", + "actCancel": "cancelOrder", + "actRefresh": "orderRefresh", + "actBuyAgain": "orderRebuy", + "connOff": "connectNow", + "cbSubOn": "secure", + "stOff": "capOff", + "stDoing": "capConnecting", + "stOn": "capOn", + "pillOn": "secure", + "authRegister": "tabRegister", + "authTagline": "authTagline", + "authForgot": "forgotPw", + "authTos": "tos", + "authSendCode": "sendCode", + "authRegBtn": "tabRegister", + "devAll": "allDevices", + "planPop": "mostPopular", + "buyNow": "buyNow", + "payChannel": "payChannel", + "chanSwUsdt": "payChannelUsdt", + "chanSwCny": "payChannelCny", + "ordPlan": "orderPlanLabel", + "ordAmount": "orderAmountLabel", + "payNetwork": "payNetworkLabel", + "payAddr": "payAddressLabel", + "payCancel": "cancelOrder", + "purDone": "payDone", + "failTitle": "payFailed", + "rowBuyPlan": "purchaseTitle", + "inviteEntry": "inviteEntry", + "setLaunch": "autostart", + "setAutoConn": "autoConnect", + "setSmartRoute": "smartRoute", + "setGrpAppr": "settingsGroupAppearance", + "setGrpAbout": "settingsGroupAbout", + "setCheckUpdate": "checkUpdate", + "contactWeb": "contactWebsite", + "contactHours": "contactHoursTitle", + "navNotifications": "notifTitle", + "nt1Type": "notifTypeImportant", + "nt2Type": "notifTypeFeature", + "nt3Type": "notifTypeNews", + "nt5Type": "notifTypeVersion", + "inviteTitle": "inviteTitle", + "inviteHeadline": "inviteEntry", + "inviteCode": "inviteCodeLabel", + "inviteLink": "inviteLinkLabel", + "inviteRewards": "inviteRewardsTitle", + "inviteR1": "inviteRewardRegister", + "inviteProgress": "inviteProgressTitle", + "inviteStatInvited": "inviteStatInvited", + "inviteStatConverted": "inviteStatConverted", + "inviteStatDays": "inviteStatDays", + "inviteTasksTitle": "inviteTasksTitle", + "inviteJoinTgTitle": "inviteJoinTgTitle", + "inviteJoinTgBtn": "inviteJoinTgBtn", + "inviteVerifyBtn": "inviteVerifyBtn", + "inviteTaskDone": "inviteTaskDone", + "navConnect": "tabConnect", + "planFree": "planFreeTag", + "accEmail": "accEmail", + "accPassword": "accPassword", + "navContact": "contactEntry", + "kValid": "payExpiresAt", + "pillFree": "planFreeTag", + "authLogin": "tabLogin", + "authEmail": "emailLabel", + "authPassword": "pwLabel", + "authLoginBtn": "doLogin", + "setGrpConn": "settingsGroupConn", + "contactEmail": "emailLabel", + "navAccount": "tabMe", + "smartSub": "smartSub", + "devMgmt": "myDevices", + "navDevices": "myDevices", + "navWebUC": "webUserCenter", + "authCode": "codeLabel", + "purTitle": "purchaseTitle", + "purCheckout": "paymentTitle", + "perMo": "perMonthSuffix", + "ordUntil": "payExpiresAt", + "payAmount": "payAmountLabel", + "payOpenAlipay": "openAlipay", + "paySwitch": "switchPayMethod", + "succTitle": "paySucceeded", + "purRetry": "payRetry", + "setKill": "killSwitch", + "contactHoursVal": "contactHours", + "nt4Type": "notifTypeReward", + "nt6Type": "notifTypePromo", + "inviteEntrySub": "inviteEntrySub", + "inviteR2": "inviteRewardFirstBuy", + "inviteR3": "inviteRewardJoinTg", + "navRedeemCode": "redeemCodeTitle", + "redeemPh": "redeemPh", + "redeemBtn": "redeemBtn", + "navRouting": "routingRulesTitle", + "rr.lan": "routingLanDirect", + "rr.lanSub": "routingLanForced" + } +} diff --git a/design/prototype/i18n/new-strings.json b/design/prototype/i18n/new-strings.json new file mode 100644 index 0000000..50eb1c3 --- /dev/null +++ b/design/prototype/i18n/new-strings.json @@ -0,0 +1,34 @@ +{ + "routingRulesTitle": { + "zh": "分流规则", + "en": "Routing rules", + "ja": "ルーティングルール", + "ko": "라우팅 규칙", + "ru": "Правила маршрутизации", + "es": "Reglas de enrutamiento" + }, + "routingRulesSub": { + "zh": "自定义直连 / 走隧道 / 拒绝", + "en": "Custom direct / tunnel / reject", + "ja": "直接接続 / トンネル / 拒否をカスタム", + "ko": "직접 연결 / 터널 / 거부 사용자 지정", + "ru": "Настройка: напрямую / туннель / отклонить", + "es": "Personalizar: directo / túnel / rechazar" + }, + "routingLanDirect": { + "zh": "局域网 / 私网直连", + "en": "LAN & private direct", + "ja": "LAN・プライベート直接接続", + "ko": "LAN / 사설망 직접 연결", + "ru": "Локальная и частная сеть напрямую", + "es": "LAN y red privada directas" + }, + "routingLanForced": { + "zh": "强制,不可关", + "en": "Always on", + "ja": "常時オン", + "ko": "항상 켜짐", + "ru": "Всегда включено", + "es": "Siempre activo" + } +} diff --git a/design/prototype/i18n/overlay.json b/design/prototype/i18n/overlay.json new file mode 100644 index 0000000..3f603ab --- /dev/null +++ b/design/prototype/i18n/overlay.json @@ -0,0 +1,814 @@ +{ + "desktop": { + "acc.billing": { + "zh": "购买 & 订单 & 兑换", + "en": "Buy · Orders · Redeem" + }, + "acc.buySub": { + "zh": "升级 PRO 会员", + "en": "Upgrade to PRO" + }, + "acc.ordersSub": { + "zh": "查看订单记录", + "en": "View order history" + }, + "acc.redeemSub": { + "zh": "输入激活码开通", + "en": "Activate with a code" + }, + "nt.back": { + "zh": "返回", + "en": "Back" + }, + "nt.n1t": { + "zh": "服务条款与隐私政策更新", + "en": "Terms & privacy policy updated" + }, + "nt.n1s": { + "zh": "我们更新了服务条款,请查阅最新内容。", + "en": "We have updated our terms — please review the latest version." + }, + "nt.n2t": { + "zh": "新增日本 · 大阪加速线路", + "en": "New Japan · Osaka lines added" + }, + "nt.n2s": { + "zh": "三条低延迟线路已上线,欢迎体验。", + "en": "Three low-latency lines are now live." + }, + "nt.n3t": { + "zh": "邀请好友活动进行中", + "en": "Invite-a-friend event is live" + }, + "nt.n3s": { + "zh": "邀请好友注册,双方都可获得会员天数。", + "en": "Invite friends to sign up — both sides earn membership days." + }, + "nt.n1full": { + "zh": "本次更新新增数据留存与第三方共享条款说明,不影响现有账户与订阅权益,建议登录网页用户中心查看完整版本。", + "en": "This update adds details on data retention and third-party sharing. It does not affect your existing account or subscription — please review the full version in the web user center." + }, + "nt.n4t": { + "zh": "邀请奖励 +3 天已到账", + "en": "Referral reward: +3 days credited" + }, + "nt.n4s": { + "zh": "好友已完成注册,会员天数已计入你的账户。", + "en": "Your friend has completed sign-up — the days have been added to your account." + }, + "nt.n5t": { + "zh": "v1.0.73 已发布", + "en": "v1.0.73 is out" + }, + "nt.n5s": { + "zh": "修复已知问题,建议尽快更新。", + "en": "Bug fixes included — update is recommended." + }, + "nt.updateBtn": { + "zh": "去更新", + "en": "Update now" + }, + "nt.n6t": { + "zh": "¥6 月付优惠上线", + "en": "¥6/mo promo is live" + }, + "nt.n6s": { + "zh": "限时活动,PRO 月付低至 ¥6,先到先得。", + "en": "Limited-time offer — PRO monthly as low as ¥6, while it lasts." + }, + "inv.back": { + "zh": "返回", + "en": "Back" + }, + "inv.lead": { + "zh": "分享你的邀请码或链接,好友注册后双方都得会员天数。", + "en": "Share your code or link — when a friend signs up, both of you earn membership days." + }, + "inv.r1": { + "zh": "好友注册", + "en": "Friend signs up" + }, + "inv.r1s": { + "zh": "双方各得 3 天", + "en": "Both earn 3 days each" + }, + "inv.r2": { + "zh": "好友首次购买", + "en": "Friend first purchase" + }, + "inv.r2s": { + "zh": "双方各得 7 天", + "en": "Both earn 7 days each" + }, + "inv.r3": { + "zh": "加入 TG 频道", + "en": "Join the TG channel" + }, + "inv.r3s": { + "zh": "获得 3 天", + "en": "Earn 3 days" + }, + "inv.tgSub": { + "zh": "+3 天", + "en": "+3 days" + }, + "inv.tgDemoPending": { + "zh": "演示态 · 待验证", + "en": "Demo · pending" + }, + "inv.tgDemoDone": { + "zh": "演示态 · 已领取", + "en": "Demo · claimed" + }, + "rd.back": { + "zh": "返回", + "en": "Back" + }, + "rd.label": { + "zh": "兑换码", + "en": "Redemption code" + }, + "rd.hint": { + "zh": "输入卡密即可激活对应时长,激活后立即生效。", + "en": "Enter a code to instantly activate plan time." + }, + "set.ordersSub": { + "zh": "查看订单记录", + "en": "View order history" + }, + "ord.back": { + "zh": "返回", + "en": "Back" + }, + "ord.details": { + "zh": "明细", + "en": "Details" + }, + "ord.kStatus": { + "zh": "状态", + "en": "Status" + }, + "ord.kDeadline": { + "zh": "支付截止", + "en": "Pay before" + }, + "ord.kTrade": { + "zh": "交易流水号", + "en": "Trade No." + }, + "ord.actMembership": { + "zh": "查看会员", + "en": "View membership" + }, + "pur.aliScan": { + "zh": "用支付宝扫码支付", + "en": "Scan with Alipay to pay" + }, + "pur.back": { + "zh": "返回", + "en": "Back" + }, + "pur.proIncludes": { + "zh": "PRO 全部特性", + "en": "Everything in PRO" + }, + "pur.f1": { + "zh": "不限时长 · 不限流量", + "en": "Unlimited time & data" + }, + "pur.f2": { + "zh": "80+ 全球加速线路", + "en": "80+ global lines" + }, + "pur.f3": { + "zh": "5 台设备同时在线", + "en": "5 devices at once" + }, + "pur.f4": { + "zh": "优先客服支持", + "en": "Priority support" + }, + "pur.month": { + "zh": "按月", + "en": "Monthly" + }, + "pur.quarter": { + "zh": "按季", + "en": "Quarterly" + }, + "pur.year": { + "zh": "按年", + "en": "Yearly" + }, + "pur.usdtHint": { + "zh": "请按精确金额(含小数末位)转账,金额用于自动对账识别,多退少不到账。", + "en": "Send the exact amount (incl. trailing decimals) — it is used to auto-match your order." + }, + "pur.alipayHint": { + "zh": "付款后自动开通,无需手动确认。", + "en": "Activated automatically after payment — no manual confirmation needed." + }, + "pur.waitCrypto": { + "zh": "等待到账确认…", + "en": "Waiting for confirmation…" + }, + "pur.waitAlipay": { + "zh": "等待付款确认…", + "en": "Waiting for payment…" + }, + "pur.demo": { + "zh": "评审演示", + "en": "Review demo" + }, + "pur.simPaid": { + "zh": "▸ 模拟到账", + "en": "▸ Simulate paid" + }, + "pur.simFail": { + "zh": "▸ 模拟失败", + "en": "▸ Simulate failure" + }, + "pur.failSub": { + "zh": "订单已超时,请重试", + "en": "Order timed out, please retry" + }, + "plan.free": { + "zh": "免费版", + "en": "Free" + }, + "q.zero": { + "zh": "0 分钟", + "en": "0 min" + }, + "nodes.search": { + "zh": "搜索加速线路…", + "en": "Search lines…" + }, + "nodes.stream": { + "zh": "流媒体", + "en": "Streaming" + }, + "nodes.hk": { + "zh": "香港 · 优选", + "en": "Hong Kong · Premium" + }, + "nodes.jp": { + "zh": "日本 · 东京", + "en": "Japan · Tokyo" + }, + "nodes.sg": { + "zh": "新加坡", + "en": "Singapore" + }, + "nodes.us": { + "zh": "美国 · 洛杉矶", + "en": "US · Los Angeles" + }, + "nodes.de": { + "zh": "德国 · 法兰克福", + "en": "Germany · Frankfurt" + }, + "nodes.unavail": { + "zh": "暂不可用", + "en": "Unavailable" + }, + "acc.pref": { + "zh": "偏好", + "en": "Preferences" + }, + "acc.myDevicesSub": { + "zh": "重命名 · 强制下线 · 移除", + "en": "Rename · force offline · remove" + }, + "ub.title": { + "zh": "发现新版本 v2.5.0,建议更新", + "en": "New version v2.5.0 available" + }, + "ub.cta": { + "zh": "立即更新", + "en": "Update" + }, + "dl.desc": { + "zh": "你的套餐最多 5 台设备,请移除一台以继续。", + "en": "Your plan allows up to 5 devices. Remove one to continue." + }, + "dev.back": { + "zh": "返回", + "en": "Back" + }, + "dev.now": { + "zh": "刚刚", + "en": "just now" + }, + "dev.h2": { + "zh": "2 小时前", + "en": "2h ago" + }, + "dev.yesterday": { + "zh": "昨天", + "en": "yesterday" + }, + "dev.tRenamed": { + "zh": "已重命名(演示)", + "en": "Renamed (demo)" + }, + "dev.tLoggedOut": { + "zh": "已强制退出(演示)", + "en": "Force logged out (demo)" + }, + "dev.tCleared": { + "zh": "已清除登录信息(演示)", + "en": "Login info cleared (demo)" + }, + "demo.theme": { + "zh": "主题", + "en": "Theme" + }, + "demo.light": { + "zh": "明", + "en": "Light" + }, + "demo.dark": { + "zh": "暗", + "en": "Dark" + }, + "demo.lang": { + "zh": "语言", + "en": "Language" + }, + "demo.persp": { + "zh": "视角", + "en": "Perspective" + }, + "demo.upd": { + "zh": "更新条", + "en": "Banner" + }, + "demo.show": { + "zh": "显", + "en": "On" + }, + "demo.hide": { + "zh": "隐", + "en": "Off" + }, + "demo.gate": { + "zh": "挡板", + "en": "Gate" + }, + "demo.deviceLimit": { + "zh": "设备超限", + "en": "Device limit" + }, + "login.frame": { + "zh": "分屏卡 · 品牌左 / 表单右", + "en": "Split card · brand left / form right" + }, + "login.note": { + "zh": "(独立静态屏,不并入交互窗口)", + "en": "(standalone static screen, outside the interactive window)" + }, + "login.slogan": { + "zh": "极速畅连 · 稳定加速线路
隐私保护 · 全程无日志", + "en": "Fast & stable lines
Privacy protected · No logs" + }, + "login.title": { + "zh": "登录账户", + "en": "Sign in" + }, + "login.signup": { + "zh": "没有账户?免费注册", + "en": "No account? Sign up free" + }, + "login.pwd2": { + "zh": "确认密码", + "en": "Confirm password" + }, + "login.signupBtn": { + "zh": "免费注册", + "en": "Sign up free" + }, + "login.haveAccount": { + "zh": "已有账户?登录", + "en": "Have an account? Sign in" + } + }, + "mobile": { + "brandLatin": { + "zh": "Pangolin", + "en": "" + }, + "demoTheme": { + "zh": "主题", + "en": "Theme" + }, + "demoLang": { + "zh": "语言", + "en": "Language" + }, + "demoView": { + "zh": "视角", + "en": "View" + }, + "demoUpdate": { + "zh": "更新", + "en": "Update" + }, + "demoGate": { + "zh": "挡板", + "en": "Gate" + }, + "demoDeviceLimit": { + "zh": "设备超限", + "en": "Device limit" + }, + "dlDesc": { + "zh": "你的账户最多可同时登录 5 台设备,请移除一台后继续。", + "en": "Your account allows up to 5 devices at once. Remove one to continue." + }, + "segLight": { + "zh": "明", + "en": "Light" + }, + "segDark": { + "zh": "暗", + "en": "Dark" + }, + "segFree": { + "zh": "免费版", + "en": "Free" + }, + "segPro": { + "zh": "PRO", + "en": "PRO" + }, + "segShow": { + "zh": "显示", + "en": "Show" + }, + "segHide": { + "zh": "隐藏", + "en": "Hide" + }, + "disconnect": { + "zh": "断开连接", + "en": "Disconnect" + }, + "nHKt": { + "zh": "香港 01", + "en": "Hong Kong 01" + }, + "nJPt": { + "zh": "日本 · 东京 02", + "en": "Japan · Tokyo 02" + }, + "nSGt": { + "zh": "新加坡 01", + "en": "Singapore 01" + }, + "nUSt": { + "zh": "美国 · 洛杉矶 03", + "en": "USA · Los Angeles 03" + }, + "nTWt": { + "zh": "台湾 01", + "en": "Taiwan 01" + }, + "tagStream": { + "zh": "流媒体", + "en": "Streaming" + }, + "tagLowLat": { + "zh": "低延迟", + "en": "Low latency" + }, + "tagStable": { + "zh": "高速稳定", + "en": "Fast & stable" + }, + "tagDistant": { + "zh": "远距离", + "en": "Distant" + }, + "maintenance": { + "zh": "维护中", + "en": "Maintenance" + }, + "unavail": { + "zh": "不可用", + "en": "Unavailable" + }, + "statDownArr": { + "zh": "↓ 下行", + "en": "↓ Down" + }, + "statUpArr": { + "zh": "↑ 上行", + "en": "↑ Up" + }, + "planProMeta": { + "zh": "PRO 会员 · 2027-07-11", + "en": "PRO member · 2027-07-11" + }, + "devFreeLbl": { + "zh": "我的设备 · 1 / 1 台", + "en": "My devices · 1 / 1" + }, + "devProLbl": { + "zh": "我的设备 · 3 / 5 台", + "en": "My devices · 3 / 5" + }, + "secDevices": { + "zh": "设备", + "en": "Devices" + }, + "devOnlineNow": { + "zh": "当前在线", + "en": "Online now" + }, + "rel2h": { + "zh": "2 小时前", + "en": "2h ago" + }, + "relYday": { + "zh": "昨天", + "en": "yesterday" + }, + "devRenamedToast": { + "zh": "已重命名(演示)", + "en": "Renamed (demo)" + }, + "devLoggedOutToast": { + "zh": "已强制退出(演示)", + "en": "Force logged out (demo)" + }, + "devClearedToast": { + "zh": "已清除登录信息(演示)", + "en": "Login info cleared (demo)" + }, + "secPref": { + "zh": "偏好", + "en": "Preferences" + }, + "secMore": { + "zh": "更多", + "en": "More" + }, + "rr.forced": { + "zh": "🔒", + "en": "🔒" + }, + "ordSecDetail": { + "zh": "明细", + "en": "Details" + }, + "kStatus": { + "zh": "状态", + "en": "Status" + }, + "kDeadline": { + "zh": "支付截止", + "en": "Pay before" + }, + "kTrade": { + "zh": "交易流水号", + "en": "Trade No." + }, + "actMembership": { + "zh": "查看会员", + "en": "View membership" + }, + "updateTitle": { + "zh": "发现新版本 v1.1.0", + "en": "New version v1.1.0" + }, + "updateCta": { + "zh": "更新", + "en": "Update" + }, + "connDoing": { + "zh": "连接中", + "en": "Connecting" + }, + "pillDoing": { + "zh": "连接中", + "en": "Connecting" + }, + "pillOffPro": { + "zh": "未连接", + "en": "Not connected" + }, + "toastUpdate": { + "zh": "演示:前往应用商店更新", + "en": "Demo: go to store to update" + }, + "authEmailPh": { + "zh": "you@example.com", + "en": "you@example.com" + }, + "authCodePh": { + "zh": "6 位验证码", + "en": "6-digit code" + }, + "authSetPw": { + "zh": "设置密码", + "en": "Set password" + }, + "authSetPwPh": { + "zh": "至少 6 位", + "en": "At least 6 characters" + }, + "statsFilterLbl": { + "zh": "设备", + "en": "Device" + }, + "proIncludes": { + "zh": "所有套餐均含", + "en": "All plans include" + }, + "featSupport": { + "zh": "优先客服", + "en": "Priority support" + }, + "planMonth": { + "zh": "按月", + "en": "Monthly" + }, + "planQuarter": { + "zh": "按季", + "en": "Quarterly" + }, + "planYear": { + "zh": "按年", + "en": "Yearly" + }, + "featUnlimited": { + "zh": "不限时长 · 不限流量", + "en": "Unlimited time & data" + }, + "feat80": { + "zh": "80+ 全球加速线路", + "en": "80+ global nodes" + }, + "feat5": { + "zh": "5 台设备同时在线", + "en": "5 devices at once" + }, + "chanUsdt": { + "zh": "加密货币 · USDT", + "en": "Crypto · USDT" + }, + "chanCny": { + "zh": "支付宝 · 人民币", + "en": "Alipay · CNY" + }, + "payHintCryptoTpl": { + "zh": "请转账 {amt} USDT(含小数末位),金额用于自动对账识别,多退少不到账。", + "en": "Send exactly {amt} USDT (including the last decimals) — the amount is used for automatic reconciliation." + }, + "payHintAlipay": { + "zh": "将跳转至支付宝完成付款,付款后自动返回本页并开通。", + "en": "You will be redirected to Alipay; after payment you return here and get activated." + }, + "payWaitCrypto": { + "zh": "等待到账确认…", + "en": "Waiting for confirmation…" + }, + "payWaitAlipay": { + "zh": "等待付款确认…", + "en": "Waiting for payment…" + }, + "demoReview": { + "zh": "评审演示", + "en": "Demo" + }, + "demoPaid": { + "zh": "▸ 模拟到账", + "en": "▸ Simulate paid" + }, + "demoFailed": { + "zh": "▸ 模拟失败", + "en": "▸ Simulate failure" + }, + "succSubTpl": { + "zh": "PRO({name})有效期至 {date}", + "en": "PRO ({name}) · valid until {date}" + }, + "failSub": { + "zh": "订单已超时,请重试", + "en": "Order timed out, please retry" + }, + "toastCopied": { + "zh": "已复制:{v}", + "en": "Copied: {v}" + }, + "toastAlipay": { + "zh": "已拉起支付宝(演示)", + "en": "Opened Alipay (demo)" + }, + "secBilling": { + "zh": "购买 · 订单 · 兑换", + "en": "Buy · Orders · Redeem" + }, + "secInvite": { + "zh": "推广", + "en": "Referral" + }, + "secGeneral": { + "zh": "通用", + "en": "General" + }, + "notiFoot": { + "zh": "仅显示最近通知(演示占位)", + "en": "Recent notifications only (demo)" + }, + "nt1Title": { + "zh": "服务条款与隐私政策更新", + "en": "Terms & privacy policy updated" + }, + "nt1Sub": { + "zh": "我们更新了服务条款,请查阅最新内容。", + "en": "We have updated our terms — please review the latest version." + }, + "nt1Full": { + "zh": "本次更新新增数据留存与第三方共享条款说明,不影响现有账户与订阅权益,建议登录网页用户中心查看完整版本。", + "en": "This update adds details on data retention and third-party sharing. It does not affect your existing account or subscription — please review the full version in the web user center." + }, + "nt2Title": { + "zh": "新增日本 · 大阪加速线路", + "en": "New Japan · Osaka lines added" + }, + "nt2Sub": { + "zh": "三条低延迟线路已上线,欢迎体验。", + "en": "Three low-latency lines are now live." + }, + "nt3Title": { + "zh": "邀请好友活动进行中", + "en": "Invite-a-friend event is live" + }, + "nt3Sub": { + "zh": "邀请好友注册,双方都可获得会员天数。", + "en": "Invite friends to sign up — both sides earn membership days." + }, + "nt4Title": { + "zh": "邀请奖励 +3 天已到账", + "en": "Referral reward: +3 days credited" + }, + "nt4Sub": { + "zh": "好友已完成注册,会员天数已计入你的账户。", + "en": "Your friend has completed sign-up — the days have been added to your account." + }, + "nt5Title": { + "zh": "v1.0.73 已发布", + "en": "v1.0.73 is out" + }, + "nt5Sub": { + "zh": "修复已知问题,建议尽快更新。", + "en": "Bug fixes included — update is recommended." + }, + "nt5Btn": { + "zh": "去更新", + "en": "Update now" + }, + "nt6Title": { + "zh": "¥6 月付优惠上线", + "en": "¥6/mo promo is live" + }, + "nt6Sub": { + "zh": "限时活动,PRO 月付低至 ¥6,先到先得。", + "en": "Limited-time offer — PRO monthly as low as ¥6, while it lasts." + }, + "inviteLede": { + "zh": "好友注册、首购、加入社区,双方都各得订阅天数。", + "en": "When friends sign up, buy, or join the community, both of you earn plan days." + }, + "inviteProgressSub": { + "zh": "已邀请 2 位好友,累计获得 6 天会员(演示占位)", + "en": "Invited 2 friends · earned 6 days (demo)" + }, + "inviteShare": { + "zh": "分享邀请链接", + "en": "Share invite link" + }, + "inviteTgSub": { + "zh": "+3 天", + "en": "+3 days" + }, + "inviteTgDemoPending": { + "zh": "演示态 · 待验证", + "en": "Demo · pending" + }, + "inviteTgDemoDone": { + "zh": "演示态 · 已领取", + "en": "Demo · claimed" + }, + "redeemLabel": { + "zh": "兑换码", + "en": "Redemption code" + }, + "redeemHint": { + "zh": "输入卡密即可激活对应时长,激活后立即生效。", + "en": "Enter a code to instantly activate plan time." + } + } +} diff --git a/design/prototype/screens/ui-desktop.html b/design/prototype/screens/ui-desktop.html index cbe720d..0eb63aa 100644 --- a/design/prototype/screens/ui-desktop.html +++ b/design/prototype/screens/ui-desktop.html @@ -750,7 +750,6 @@
国内直连
GeoIP / GeoSite CN
局域网 / 私网直连
强制,不可关
🔒 强制
-
私有服务走隧道
服务端下发
🔒 服务端
我的规则 · 有序,首命中生效
@@ -1174,127 +1173,238 @@