web/src/i18n.ts1286 lines · 61588 bytes
| 1 | // Languages: zh-CN (default), zh-TW (Traditional, Taiwan usage), en. |
| 2 | // Pick: ?lang=zh-CN|zh-TW|en first, then localStorage, else zh-CN; L or the language switch cycles through them. |
| 3 | // Switching only redraws (no reconnect, no state reset). Number grouping and the venue clock (Asia/Shanghai) do not |
| 4 | // change with the language. Event-stream lines are stored as {k, p} and rendered in the current language. |
| 5 | // zh-CN is the source table; tsc requires zh-TW and en to have every key. |
| 6 | import type { LabId } from './lab' |
| 7 | import type { CardId } from './types' |
| 8 | |
| 9 | export type Lang = 'zh-CN' | 'zh-TW' | 'en' |
| 10 | export const LANGS: readonly Lang[] = ['zh-CN', 'zh-TW', 'en'] |
| 11 | export const LANG_LABEL: Record<Lang, string> = { 'zh-CN': '简', 'zh-TW': '繁', en: 'EN' } |
| 12 | const LS_KEY = 'airlock.lang' |
| 13 | |
| 14 | /** Template parameter: strings and numbers go in as they are; a Msg is translated in the same language (nesting) */ |
| 15 | export type Param = string | number | Msg |
| 16 | export interface Msg { |
| 17 | k: string |
| 18 | p?: Record<string, Param> |
| 19 | } |
| 20 | export const msg = (k: Key, p?: Record<string, Param>): Msg => (p ? { k, p } : { k }) |
| 21 | |
| 22 | // ---------- fixed strings ---------- |
| 23 | const ZH_CN = { |
| 24 | 'doc.title': 'Upgrade Airlock 升级气闸 · 战情室', |
| 25 | 'brand.sub': '升级气闸 · 每次代理升级都要过 Chainlink CRE', |
| 26 | // top bar |
| 27 | 'st.live': '实时 · Ethereum Sepolia', |
| 28 | 'st.fork': '本机 Sepolia 分叉(彩排)', |
| 29 | 'st.offline': '回放 · 录制自 Ethereum Sepolia', |
| 30 | 'st.offlineFork': '回放 · 录制自 Sepolia 分叉', |
| 31 | 'st.offlineEmpty': '回放 · 还没有录制资料', |
| 32 | 'st.connecting': '正在连接 Sepolia…', |
| 33 | 'st.down': '重新连接中…', |
| 34 | 'st.block': '区块', |
| 35 | 'cre.by': '判定方', |
| 36 | 'ctl.details': '详情', |
| 37 | 'ctl.help': '快捷键', |
| 38 | 'ctl.langTitle': '切换语言(L)', |
| 39 | 'ctl.detailsTitle': '开发者详情(D)', |
| 40 | 'ctl.helpTitle': '讲者快捷键(?)', |
| 41 | // phase chips |
| 42 | 'ph.idle': '空', |
| 43 | 'ph.chamber': '冷静中', |
| 44 | 'ph.cleared': '已放行', |
| 45 | 'ph.waiting': '待放行', |
| 46 | 'ph.ready': '可执行', |
| 47 | 'ph.executed': '已执行', |
| 48 | 'ph.ejected': '已否决', |
| 49 | 'who.cre': 'Chainlink CRE', |
| 50 | 'who.human': '人工紧急否决', |
| 51 | 'who.approver': 'approver(人工)', |
| 52 | 'who.someone': '未知地址', |
| 53 | 'who.unknown': '未知地址 {a}', |
| 54 | // hero: which proposal |
| 55 | 'h.eyebrow': '提案 #{id} · {kind}', |
| 56 | 'h.replay': '回放 · 冷静期快进', |
| 57 | 'h.replayPaused': '回放已暂停', |
| 58 | 'h.dropped': 'log 因链重组消失', |
| 59 | 'h.newImpl': '新实现 {a}', |
| 60 | 'h.guardOff': '移除 {who} guardian', |
| 61 | 'h.guardOn': '新增 guardian {who}', |
| 62 | 'h.delay': '把冷静期改成 {n} 块', |
| 63 | 'h.paused': '新的暂停实现 {a}', |
| 64 | 'h.nameRemoveCre': '移除 CRE guardian', |
| 65 | 'h.nameGuard': 'guardian 变更', |
| 66 | 'h.nameDelay': '冷静期变更', |
| 67 | 'h.namePaused': '暂停实现变更', |
| 68 | 'h.descRemoveCre': '想把 Chainlink CRE 从 guardian 名单里拿掉', |
| 69 | 'h.descGuard': '改的是气闸本身:由链上读取判定', |
| 70 | 'h.descUnknown': '本页不认得这个实现', |
| 71 | 'h.empty': '气闸空着', |
| 72 | 'h.emptyDesc': '提案先在气闸舱等 {n} 块(≈ {t});内门只认 Chainlink CRE 的放行', |
| 73 | 'h.loading': '正在读取 Ethereum Sepolia 上的提案…', |
| 74 | 'h.noSuch': '没有提案 #{id}', |
| 75 | 'h.noSuchDesc': 'Ethereum Sepolia 上目前最新的是提案 #{n};#{id} 一发起,这页就会显示它', |
| 76 | 'h.pending': 'Sepolia 合约尚未部署', |
| 77 | 'h.noRecording': '还没有录制资料', |
| 78 | // hero: the airlock picture |
| 79 | 'g.owner': 'Owner 私钥', |
| 80 | 'g.ownerSub': '只能提案', |
| 81 | 'g.proxy': 'DemoBTC 代理', |
| 82 | 'g.proxyIdle': '跑现有代码', |
| 83 | 'g.proxySafe': '毫发无伤', |
| 84 | 'g.proxyLive': '已换上 {name}', |
| 85 | 'c.left': '{n|块|块}', |
| 86 | 'c.leftSub': '冷静期剩余', |
| 87 | 'c.eta': '≈ {t}', |
| 88 | 'c.empty': '等待提案', |
| 89 | 'c.over': '冷静期已满', |
| 90 | 'c.noClear': '链上还没有放行', |
| 91 | 'c.ready': '可以执行', |
| 92 | 'c.readySub': '已放行 · 冷静期已满', |
| 93 | 'c.executed': '升级已生效', |
| 94 | 'c.ejected': '已弹出', |
| 95 | 'c.clearStamp': '已放行', |
| 96 | 'c.vetoAt': '提案后 {n} 块就被否决', |
| 97 | 'c.awaiting': '等待 Chainlink CRE 判定', |
| 98 | 'c.cleared': 'Chainlink CRE 已放行', |
| 99 | 'c.clearedBy': '{who} 已放行', |
| 100 | // hero: the line under the picture |
| 101 | 'b.idle': '等待升级提案', |
| 102 | 'b.chamber': '已进气闸 · 等待 Chainlink CRE 判定', |
| 103 | 'b.cleared': '{who} 已放行 · 冷静期满内门就开', |
| 104 | 'b.ready': '{who} 已放行 · 可以执行', |
| 105 | 'b.waiting': '冷静期已满 · 没有放行,内门继续锁住', |
| 106 | 'b.executed': '已执行 · {name} 上线', |
| 107 | 'b.ejected': '已弹出 · {who} 否决', |
| 108 | 'b.ejectedFar': '已弹出 · 链上已否决', |
| 109 | 'd.outer': '外门', |
| 110 | 'd.chamber': '气闸舱', |
| 111 | 'd.inner': '内门', |
| 112 | 'd.outerIdle': '接受提案', |
| 113 | 'd.outerOn': '提案于区块 #{b}', |
| 114 | 'd.chamberSub': '冷静期 {n} 块 ≈ {t}', |
| 115 | 'd.innerLocked': '锁住 · 等 Chainlink CRE 放行', |
| 116 | 'd.innerUnlocked': '已解锁 · 区块 #{b} 起可开', |
| 117 | 'd.innerReady': '已解锁 · 冷静期已满', |
| 118 | 'd.innerOpen': '区块 #{b} 开启', |
| 119 | 'd.innerSealed': '永久封死 · 执行必定回滚', |
| 120 | 'd.innerWaiting': '锁住 · 没有放行', |
| 121 | // verdict card |
| 122 | 'v.title': 'Chainlink CRE 判定', |
| 123 | 'v.veto': '否决', |
| 124 | 'v.clear': '放行', |
| 125 | 'v.awaiting': '判定中', |
| 126 | 'v.none': '—', |
| 127 | 'v.noneText': '提案进入气闸后,判定会以签名报告的形式上链。', |
| 128 | 'v.clearText': '撑过攻击模拟:每条规则都守住了。', |
| 129 | 'v.clearTextCfg': '配置检查通过:approver 已预先核准,或这个变更没有削弱气闸。', |
| 130 | 'v.awaitText': '放行或否决会以签名报告上链。', |
| 131 | 'v.after': '提案后 <b>{n}</b> 块 · <b>{t}</b>', |
| 132 | 'v.afterEst': '提案后 <b>{n}</b> 块 · ≈ <b>{t}</b>', |
| 133 | 'v.human': '由人工紧急否决(值班人员),不是 Chainlink CRE。', |
| 134 | 'v.approver': 'approver 人工放行;Chainlink CRE 仍会补判,否决优先。', |
| 135 | 'v.skipped': '执行前没有 Chainlink CRE 判定(approver 人工放行)。', |
| 136 | 'v.loadingChip': '读取中', |
| 137 | 'v.loading': '正在读取链上判决…', |
| 138 | 'v.far': '链上已否决(否决 log 不在读取范围内)。', |
| 139 | 'v.clearFar': '链上已放行(放行 log 不在读取范围内)。', |
| 140 | 'p.sim': '攻击模拟', |
| 141 | 'p.simS': '4 家 RPC 上跑 eth_simulateV1', |
| 142 | 'p.agree': '答案全部一致', |
| 143 | 'p.agreeS': '4 家里至少 2 家回答,答案全部相同', |
| 144 | 'p.cfg': '配置检查', |
| 145 | 'p.cfgS': '在提案区块读链上状态:approver 是否预先核准', |
| 146 | 'p.static': '核准检查', |
| 147 | 'p.staticS': '在提案区块读链上状态,不需要模拟', |
| 148 | 'p.simFail': '攻击模拟无法判定', |
| 149 | 'p.simFailS': '没有明确且一致的答案:fail-closed 否决', |
| 150 | 'p.don': '共识聚合', |
| 151 | 'p.donS': '判定与规则须完全一致才聚合', |
| 152 | 'p.sign': '签名报告', |
| 153 | 'p.signS': 'EIP-712,绑定这一个提案', |
| 154 | 'p.chain': '上链', |
| 155 | 'p.chainS': 'forwarder → AirlockReceiver → AirlockAdmin', |
| 156 | 'p.landVeto': '否决上链', |
| 157 | 'p.landClear': '放行上链', |
| 158 | 'l.proposal': '提案交易', |
| 159 | 'l.report': '报告交易', |
| 160 | 'l.execute': '执行交易', |
| 161 | 'l.evidence': '证据哈希', |
| 162 | // strip |
| 163 | 's.title': 'Ethereum Sepolia 上的提案', |
| 164 | 's.titleOffline': '录制的提案', |
| 165 | 's.titleFork': '本机分叉上的提案', |
| 166 | 's.follow': '自动跟随', |
| 167 | 's.loop': '回放循环', |
| 168 | 's.on': '开', |
| 169 | 's.off': '关', |
| 170 | 's.hint': '← → 切换 · N 最新 · 空格 重播 · ? 快捷键', |
| 171 | 's.loading': '正在载入提案…', |
| 172 | 's.none': '还没有提案', |
| 173 | 's.veto': '否决', |
| 174 | 's.clear': '放行', |
| 175 | 's.after': '+{n} {n|块|块}', |
| 176 | 's.left': '剩 {n} 块', |
| 177 | 's.noClear': '还没有放行', |
| 178 | 's.executed': '已执行', |
| 179 | 's.ready': '可以执行', |
| 180 | 's.allHeld': '每条规则都守住', |
| 181 | 's.cfgOk': '配置检查通过', |
| 182 | 's.awaiting': '等待 CRE 判定', |
| 183 | 's.replaying': '回放中', |
| 184 | // details drawer |
| 185 | 'dr.title': '详情', |
| 186 | 'dr.close': '关闭(D/Esc)', |
| 187 | 'dr.proposal': '这个提案', |
| 188 | 'dr.contracts': '合约', |
| 189 | 'dr.conn': '连线', |
| 190 | 'dr.stream': '事件串流', |
| 191 | 'dr.about': '证据哈希怎么来', |
| 192 | 'dr.kind': '类型', |
| 193 | 'dr.target': '目标', |
| 194 | 'dr.codehash': 'codehash', |
| 195 | 'dr.proposer': '提案者', |
| 196 | 'dr.proposed': '提案', |
| 197 | 'dr.cooldown': '冷静期', |
| 198 | 'dr.cooldownV': '#{a} → #{b}({n} 块)', |
| 199 | 'dr.verdictTx': '判定交易', |
| 200 | 'dr.cost': '判定交易成本', |
| 201 | 'dr.costV': '{c}(gasUsed {g} × {p})', |
| 202 | 'dr.evidence': '证据哈希', |
| 203 | 'dr.workflow': 'workflow id', |
| 204 | 'dr.executed': '执行', |
| 205 | 'dr.atBlock': '区块 #{b}', |
| 206 | 'dr.reading': '读取中…', |
| 207 | 'dr.verified': '✓ Sourcify 已验证', |
| 208 | 'dr.heads': '区块来源', |
| 209 | 'dr.logs': 'log 来源', |
| 210 | 'dr.bt': '出块时间', |
| 211 | 'dr.endpoint': '节点', |
| 212 | 'dr.recording': '录制', |
| 213 | 'dr.recordingV': '{src} · 区块 #{a} – #{b} · 录制于 {d}', |
| 214 | 'dr.noStream': '还没有事件', |
| 215 | 'dr.aboutBody': |
| 216 | 'evidenceHash = keccak256(规范化证据 JSON:提案栏位、proposalDigest、判定与规则,键名排序)。工作流用它的 attester 私钥对这份证据签出 EIP-712 判定;部署到 DON 时,节点还必须对同一份判定达成共识才会签出报告。这里的判定由官方 CRE CLI 在一台服务器上运行这个工作流产生,经 Chainlink 的 Sepolia 测试 forwarder 写上链。AirlockReceiver 验过 forwarder、EIP-712 签名与 proposalDigest 才会放行或否决。任何人都能用链上数据重算这个哈希。', |
| 217 | 'src.sepolia': 'Ethereum Sepolia', |
| 218 | 'src.fork': '本机 Sepolia 分叉', |
| 219 | 'link.ws': 'WebSocket newHeads', |
| 220 | 'link.poll': 'HTTP 轮询,每 {s} 秒', |
| 221 | 'link.down': '重新连接中', |
| 222 | 'link.logsWs': 'WebSocket 订阅', |
| 223 | 'link.logsPoll': 'eth_getLogs 游标 #{c}', |
| 224 | 'link.connecting': '连接中…', |
| 225 | 'link.offline': '不连任何网络', |
| 226 | 'bt.measured': '实测 {s} 秒/块', |
| 227 | 'bt.fast': '快进挖块(分叉)', |
| 228 | 'bt.measuring': '测量中…', |
| 229 | // help overlay |
| 230 | 'help.title': '讲者快捷键', |
| 231 | 'help.prev': '上一个(较旧)提案', |
| 232 | 'help.next': '下一个(较新)提案', |
| 233 | 'help.newest': '跳到最新提案', |
| 234 | 'help.replay': '重播选中提案的判定瞬间', |
| 235 | 'help.follow': '自动跟随新提案并播放判定(离线:回放循环)', |
| 236 | 'help.loop': '播放/暂停回放循环', |
| 237 | 'help.details': '开发者详情抽屉', |
| 238 | 'help.lang': '切换语言', |
| 239 | 'help.help': '显示/隐藏这份说明', |
| 240 | 'help.esc': '关闭面板', |
| 241 | 'help.space': '空格', |
| 242 | 'help.url': '网址参数', |
| 243 | 'help.urlBody': '?lang=en | zh-CN | zh-TW · ?mode=offline 离线回放 · ?p=<提案编号> · ?clean=1 录制用,不显示讲者控件 · ?rpc=<本机分叉>', |
| 244 | 'help.foot': '按 ? 或 Esc 关闭 · 鼠标静止 3 秒后讲者控件自动隐藏', |
| 245 | // attack picker (lab.ts) |
| 246 | 'lab.title': '选一种攻击,看 Chainlink CRE 怎么拦下它', |
| 247 | 'lab.sub': '每张卡都重播 Ethereum Sepolia 上一笔真实提案', |
| 248 | 'lab.invite': '刚才是「{name}」的真实重播 · 再选一种攻击看看', |
| 249 | 'lab.tabLab': '攻击重播', |
| 250 | 'lab.tabRecent': '链上提案', |
| 251 | 'lab.tabsTitle': '底部显示攻击重播或链上最近的提案', |
| 252 | 'lab.play': '看重播', |
| 253 | 'lab.again': '再看一次', |
| 254 | 'lab.none': '还没有这种提案', |
| 255 | 'lab.loading': '读取中…', |
| 256 | 'lab.live': '实时', |
| 257 | 'lab.liveFoot': '正在链上判定 · 点击查看', |
| 258 | 'lab.now': '正在回放 #{id} · 真实 Sepolia 提案', |
| 259 | 'lab.proposalTx': '提案交易', |
| 260 | 'lab.st.arrive': '提案进外门', |
| 261 | 'lab.st.judge': 'CRE 判定中', |
| 262 | 'lab.st.veto': '否决 · 弹出', |
| 263 | 'lab.st.clear': '放行 · 内门解锁', |
| 264 | 'lab.st.fast': '冷静期快进', |
| 265 | 'lab.st.exec': '执行 · 升级生效', |
| 266 | 'h.replayReal': '回放 · 真实 Sepolia 提案', |
| 267 | 'h.live': '实时', |
| 268 | 'st.replayOf': '回放 · Sepolia 提案 #{id}', |
| 269 | // first-visit intro (before its replay starts) and a replay's lead-in |
| 270 | 'h.introEyebrow': '真实攻击重播', |
| 271 | 'h.introTitle': '正在载入一笔真实攻击…', |
| 272 | 'h.introDesc': '马上从头重播 Ethereum Sepolia 上的一笔真实提案:进外门 → 在气闸舱等待 → Chainlink CRE 攻击模拟后判定', |
| 273 | 'c.intro': '重播即将开始', |
| 274 | 'c.arriving': '提案 #{id} 抵达外门…', |
| 275 | 'b.intro': '内门锁着:只有 Chainlink CRE 放行才会打开', |
| 276 | 'b.arriving': '提案先进外门,再在气闸舱里等冷静期', |
| 277 | 'v.introChip': '等待重播', |
| 278 | 'v.introText': '重播走到判定那一步时,CRE 的结论会出现在这里。', |
| 279 | 'h.slowTitle': '连接 Sepolia 比较慢…', |
| 280 | 'h.slowDesc': '公共 Sepolia RPC 很慢或连不上,页面仍在重试。也可以先看录制好的真实 Sepolia 运行:', |
| 281 | 'h.slowLink': '离线回放', |
| 282 | 'c.slow': '仍在读取链上数据…', |
| 283 | 'c.step': 'CRE · {s}\u00a0{i}/{n}', |
| 284 | 'c.compressed': '回放 · 时间已压缩', |
| 285 | 'c.ff': '冷静期快进 ×{n}', |
| 286 | 'h.onchain': '链上状态 · 提案于 {d}', |
| 287 | 'h.onchainNoDate': '链上状态', |
| 288 | 'v.replayTag': '回放', |
| 289 | 'h.pendEyebrow': '操作员刚刚发起 · 真实交易', |
| 290 | 'h.pendDesc': '交易已送出,正在等待打包进 Ethereum Sepolia 区块;提案一上链就进外门', |
| 291 | 'c.pending': '等待上链…', |
| 292 | 'b.pending': '提案上链后进气闸舱,由 Chainlink CRE 攻击模拟后判定', |
| 293 | 'v.pendChip': '等待上链', |
| 294 | 'v.pendText': '提案上链后,Chainlink CRE 通常在 20–40 秒内给出判定。', |
| 295 | // operator mode (operator-client.ts) |
| 296 | 'op.btn': '实战发起', |
| 297 | 'op.badge': '操作员', |
| 298 | 'op.title': '在 Sepolia 上真实发起攻击', |
| 299 | 'op.note': '以 owner 身份送出一笔真实的 Ethereum Sepolia 交易;Chainlink CRE 通常在 20–40 秒内判定,战情室会自动跟随。', |
| 300 | 'op.gap': '两次发起至少间隔 {n} 块', |
| 301 | 'op.launch': '在 Sepolia 上真实发起', |
| 302 | 'op.launchShort': '真实发起', |
| 303 | 'op.expectVeto': '预期:否决 {rule}', |
| 304 | 'op.expectClear': '预期:放行,冷静期满后可执行', |
| 305 | 'op.confirmQ': '确定在 Sepolia 上发起「{name}」?', |
| 306 | 'op.confirmBody': '这会送出一笔真实交易({fn}),送出后无法撤回。', |
| 307 | 'op.confirm': '确认发起', |
| 308 | 'op.cancel': '取消', |
| 309 | 'op.close': '关闭', |
| 310 | 'op.busy': '上一笔还在路上', |
| 311 | 'op.sending': '正在送出交易…', |
| 312 | 'op.sent': '交易已送出 · 等待上链…', |
| 313 | 'op.slow': '还没看到这笔交易的提案 · 可在 Etherscan 查看', |
| 314 | 'op.onchain': '提案 #{id} 已进气闸 · 等待 Chainlink CRE 判定', |
| 315 | 'op.veto': '提案 #{id} 已否决 · {who}{rule}', |
| 316 | 'op.clear': '提案 #{id} 已放行 · {who}', |
| 317 | 'op.exec': '提案 #{id} 已执行', |
| 318 | 'op.errCard': '这张卡不能发起', |
| 319 | 'op.errAuth': '操作员口令无效 · 已退出操作员模式', |
| 320 | 'op.errCooldown': '冷却中 · 再等 {n} 块(约 {t})', |
| 321 | 'op.errCooldownAny': '冷却中 · 上一笔交易还没确认,稍后再试', |
| 322 | 'op.errRpc': '节点没有接受这笔交易 · 稍后再试', |
| 323 | 'op.errDisabled': '这里的操作员模式已关闭 · 没有送出', |
| 324 | 'op.errReverted': '节点试跑这笔提案会回滚 · 所以没有送出', |
| 325 | 'op.errServer': '操作员服务出错 · 没有送出,稍后再试', |
| 326 | 'op.errMisconfigured': '服务器上的私钥不是这个部署的 owner · 没有送出', |
| 327 | 'op.errForbidden': '操作员服务拒绝了这个来源(只接受本机或同一网站)· 没有送出', |
| 328 | 'op.errUnclear': '操作员服务的回应看不懂 · 若交易已送出,战情室会自动接上', |
| 329 | 'op.errNet': '连不上操作员服务 · 没有收到回应', |
| 330 | 'op.authNone': '本机模式 · 不需要口令', |
| 331 | 'op.authToken': '已用口令登入', |
| 332 | 'op.signOut': '退出操作员模式', |
| 333 | 'op.tokenTitle': '操作员口令', |
| 334 | 'op.tokenBody': '口令只存在这台浏览器里,不会出现在网址中。', |
| 335 | 'op.tokenBad': '口令格式不对', |
| 336 | 'op.tokenSave': '保存', |
| 337 | 'help.lab': '重播第 1–7 种攻击(攻击卡片从左到右)', |
| 338 | 'help.operator': '实战发起面板(操作员)', |
| 339 | // event stream (details drawer) |
| 340 | 'rp.readyCleared': '冷静期满 #{id}:块 {b},已放行,内门可以开', |
| 341 | 'rp.readyWaiting': '冷静期满 #{id}:块 {b},还没有放行,execute 会 revert NotCleared', |
| 342 | 'ev.proposed': 'Proposed #{id}:块 {b},冷静期满 {rb},tx {tx}', |
| 343 | 'ev.vetoed': 'Vetoed #{id}:{who} 在块 {b}{after} · {rule} · 第 {n} 票', |
| 344 | 'ev.cleared': 'Cleared #{id}:{who} 在块 {b}{after} · 证据 {h}', |
| 345 | 'ev.executed': 'Executed #{id}:块 {b},tx {tx}', |
| 346 | 'ev.reason': 'ReasonAttached #{id}:证据哈希 {h}', |
| 347 | 'ev.verdict': 'Verdict #{id}:Chainlink CRE {what} · 证据 {h}', |
| 348 | 'ev.vClear': '放行', |
| 349 | 'ev.vVeto': '否决 {rule}', |
| 350 | 'ev.after': '(提案后 {n} 块)', |
| 351 | 'ev.removed': '链重组:log {tx} 被移除', |
| 352 | 'ev.loaded': '已从链上载入最近 {n} 个提案(nextId {next})', |
| 353 | 'ev.loadFail': '载入最近提案失败(第 {n} 次),稍后重试:{err}', |
| 354 | 'off.loaded': '离线回放:已载入录制的 {n} 个提案({d}),不连任何网络', |
| 355 | 'dur.sec': '{s} 秒', |
| 356 | 'dur.min': '{m} 分 {s} 秒', |
| 357 | 'dur.minOnly': '{m} 分钟', |
| 358 | } |
| 359 | export type Key = keyof typeof ZH_CN |
| 360 | |
| 361 | const ZH_TW: Record<Key, string> = { |
| 362 | 'doc.title': 'Upgrade Airlock 升級氣閘 · 戰情室', |
| 363 | 'brand.sub': '升級氣閘 · 每次代理升級都要過 Chainlink CRE', |
| 364 | 'st.live': '即時 · Ethereum Sepolia', |
| 365 | 'st.fork': '本機 Sepolia 分叉(彩排)', |
| 366 | 'st.offline': '回放 · 錄製自 Ethereum Sepolia', |
| 367 | 'st.offlineFork': '回放 · 錄製自 Sepolia 分叉', |
| 368 | 'st.offlineEmpty': '回放 · 還沒有錄製資料', |
| 369 | 'st.connecting': '正在連線 Sepolia…', |
| 370 | 'st.down': '重新連線中…', |
| 371 | 'st.block': '區塊', |
| 372 | 'cre.by': '判定方', |
| 373 | 'ctl.details': '詳情', |
| 374 | 'ctl.help': '快捷鍵', |
| 375 | 'ctl.langTitle': '切換語言(L)', |
| 376 | 'ctl.detailsTitle': '開發者詳情(D)', |
| 377 | 'ctl.helpTitle': '講者快捷鍵(?)', |
| 378 | 'ph.idle': '空', |
| 379 | 'ph.chamber': '冷靜中', |
| 380 | 'ph.cleared': '已放行', |
| 381 | 'ph.waiting': '待放行', |
| 382 | 'ph.ready': '可執行', |
| 383 | 'ph.executed': '已執行', |
| 384 | 'ph.ejected': '已否決', |
| 385 | 'who.cre': 'Chainlink CRE', |
| 386 | 'who.human': '人工緊急否決', |
| 387 | 'who.approver': 'approver(人工)', |
| 388 | 'who.someone': '未知地址', |
| 389 | 'who.unknown': '未知地址 {a}', |
| 390 | 'h.eyebrow': '提案 #{id} · {kind}', |
| 391 | 'h.replay': '回放 · 冷靜期快轉', |
| 392 | 'h.replayPaused': '回放已暫停', |
| 393 | 'h.dropped': 'log 因鏈重組消失', |
| 394 | 'h.newImpl': '新實作 {a}', |
| 395 | 'h.guardOff': '移除 {who} guardian', |
| 396 | 'h.guardOn': '新增 guardian {who}', |
| 397 | 'h.delay': '把冷靜期改成 {n} 塊', |
| 398 | 'h.paused': '新的暫停實作 {a}', |
| 399 | 'h.nameRemoveCre': '移除 CRE guardian', |
| 400 | 'h.nameGuard': 'guardian 變更', |
| 401 | 'h.nameDelay': '冷靜期變更', |
| 402 | 'h.namePaused': '暫停實作變更', |
| 403 | 'h.descRemoveCre': '想把 Chainlink CRE 從 guardian 名單裡拿掉', |
| 404 | 'h.descGuard': '改的是氣閘本身:由鏈上讀取判定', |
| 405 | 'h.descUnknown': '本頁不認得這個實作', |
| 406 | 'h.empty': '氣閘空著', |
| 407 | 'h.emptyDesc': '提案先在氣閘艙等 {n} 塊(≈ {t});內門只認 Chainlink CRE 的放行', |
| 408 | 'h.loading': '正在讀取 Ethereum Sepolia 上的提案…', |
| 409 | 'h.noSuch': '沒有提案 #{id}', |
| 410 | 'h.noSuchDesc': 'Ethereum Sepolia 上目前最新的是提案 #{n};#{id} 一發起,這頁就會顯示它', |
| 411 | 'h.pending': 'Sepolia 合約尚未部署', |
| 412 | 'h.noRecording': '還沒有錄製資料', |
| 413 | 'g.owner': 'Owner 私鑰', |
| 414 | 'g.ownerSub': '只能提案', |
| 415 | 'g.proxy': 'DemoBTC 代理', |
| 416 | 'g.proxyIdle': '跑現有程式', |
| 417 | 'g.proxySafe': '毫髮無傷', |
| 418 | 'g.proxyLive': '已換上 {name}', |
| 419 | 'c.left': '{n|塊|塊}', |
| 420 | 'c.leftSub': '冷靜期剩餘', |
| 421 | 'c.eta': '≈ {t}', |
| 422 | 'c.empty': '等待提案', |
| 423 | 'c.over': '冷靜期已滿', |
| 424 | 'c.noClear': '鏈上還沒有放行', |
| 425 | 'c.ready': '可以執行', |
| 426 | 'c.readySub': '已放行 · 冷靜期已滿', |
| 427 | 'c.executed': '升級已生效', |
| 428 | 'c.ejected': '已彈出', |
| 429 | 'c.clearStamp': '已放行', |
| 430 | 'c.vetoAt': '提案後 {n} 塊就被否決', |
| 431 | 'c.awaiting': '等待 Chainlink CRE 判定', |
| 432 | 'c.cleared': 'Chainlink CRE 已放行', |
| 433 | 'c.clearedBy': '{who} 已放行', |
| 434 | 'b.idle': '等待升級提案', |
| 435 | 'b.chamber': '已進氣閘 · 等待 Chainlink CRE 判定', |
| 436 | 'b.cleared': '{who} 已放行 · 冷靜期滿內門就開', |
| 437 | 'b.ready': '{who} 已放行 · 可以執行', |
| 438 | 'b.waiting': '冷靜期已滿 · 沒有放行,內門繼續鎖住', |
| 439 | 'b.executed': '已執行 · {name} 上線', |
| 440 | 'b.ejected': '已彈出 · {who} 否決', |
| 441 | 'b.ejectedFar': '已彈出 · 鏈上已否決', |
| 442 | 'd.outer': '外門', |
| 443 | 'd.chamber': '氣閘艙', |
| 444 | 'd.inner': '內門', |
| 445 | 'd.outerIdle': '接受提案', |
| 446 | 'd.outerOn': '提案於區塊 #{b}', |
| 447 | 'd.chamberSub': '冷靜期 {n} 塊 ≈ {t}', |
| 448 | 'd.innerLocked': '鎖住 · 等 Chainlink CRE 放行', |
| 449 | 'd.innerUnlocked': '已解鎖 · 區塊 #{b} 起可開', |
| 450 | 'd.innerReady': '已解鎖 · 冷靜期已滿', |
| 451 | 'd.innerOpen': '區塊 #{b} 開啟', |
| 452 | 'd.innerSealed': '永久封死 · 執行必定回滾', |
| 453 | 'd.innerWaiting': '鎖住 · 沒有放行', |
| 454 | 'v.title': 'Chainlink CRE 判定', |
| 455 | 'v.veto': '否決', |
| 456 | 'v.clear': '放行', |
| 457 | 'v.awaiting': '判定中', |
| 458 | 'v.none': '—', |
| 459 | 'v.noneText': '提案進入氣閘後,判定會以簽章報告的形式上鏈。', |
| 460 | 'v.clearText': '撐過攻擊模擬:每條規則都守住了。', |
| 461 | 'v.clearTextCfg': '設定檢查通過:approver 已預先核准,或這個變更沒有削弱氣閘。', |
| 462 | 'v.awaitText': '放行或否決會以簽章報告上鏈。', |
| 463 | 'v.after': '提案後 <b>{n}</b> 塊 · <b>{t}</b>', |
| 464 | 'v.afterEst': '提案後 <b>{n}</b> 塊 · ≈ <b>{t}</b>', |
| 465 | 'v.human': '由人工緊急否決(值班人員),不是 Chainlink CRE。', |
| 466 | 'v.approver': 'approver 人工放行;Chainlink CRE 仍會補判,否決優先。', |
| 467 | 'v.skipped': '執行前沒有 Chainlink CRE 判定(approver 人工放行)。', |
| 468 | 'v.loadingChip': '讀取中', |
| 469 | 'v.loading': '正在讀取鏈上判決…', |
| 470 | 'v.far': '鏈上已否決(否決 log 不在讀取範圍內)。', |
| 471 | 'v.clearFar': '鏈上已放行(放行 log 不在讀取範圍內)。', |
| 472 | 'p.sim': '攻擊模擬', |
| 473 | 'p.simS': '4 家 RPC 上跑 eth_simulateV1', |
| 474 | 'p.agree': '答案全部一致', |
| 475 | 'p.agreeS': '4 家裡至少 2 家回答,答案全部相同', |
| 476 | 'p.cfg': '設定檢查', |
| 477 | 'p.cfgS': '在提案區塊讀鏈上狀態:approver 是否預先核准', |
| 478 | 'p.static': '核准檢查', |
| 479 | 'p.staticS': '在提案區塊讀鏈上狀態,不需要模擬', |
| 480 | 'p.simFail': '攻擊模擬無法判定', |
| 481 | 'p.simFailS': '沒有明確且一致的答案:fail-closed 否決', |
| 482 | 'p.don': '共識聚合', |
| 483 | 'p.donS': '判定與規則須完全一致才聚合', |
| 484 | 'p.sign': '簽章報告', |
| 485 | 'p.signS': 'EIP-712,綁定這一個提案', |
| 486 | 'p.chain': '上鏈', |
| 487 | 'p.chainS': 'forwarder → AirlockReceiver → AirlockAdmin', |
| 488 | 'p.landVeto': '否決上鏈', |
| 489 | 'p.landClear': '放行上鏈', |
| 490 | 'l.proposal': '提案交易', |
| 491 | 'l.report': '報告交易', |
| 492 | 'l.execute': '執行交易', |
| 493 | 'l.evidence': '證據雜湊', |
| 494 | 's.title': 'Ethereum Sepolia 上的提案', |
| 495 | 's.titleOffline': '錄製的提案', |
| 496 | 's.titleFork': '本機分叉上的提案', |
| 497 | 's.follow': '自動跟隨', |
| 498 | 's.loop': '回放循環', |
| 499 | 's.on': '開', |
| 500 | 's.off': '關', |
| 501 | 's.hint': '← → 切換 · N 最新 · 空白鍵 重播 · ? 快捷鍵', |
| 502 | 's.loading': '正在載入提案…', |
| 503 | 's.none': '還沒有提案', |
| 504 | 's.veto': '否決', |
| 505 | 's.clear': '放行', |
| 506 | 's.after': '+{n} {n|塊|塊}', |
| 507 | 's.left': '剩 {n} 塊', |
| 508 | 's.noClear': '還沒有放行', |
| 509 | 's.executed': '已執行', |
| 510 | 's.ready': '可以執行', |
| 511 | 's.allHeld': '每條規則都守住', |
| 512 | 's.cfgOk': '設定檢查通過', |
| 513 | 's.awaiting': '等待 CRE 判定', |
| 514 | 's.replaying': '回放中', |
| 515 | 'dr.title': '詳情', |
| 516 | 'dr.close': '關閉(D/Esc)', |
| 517 | 'dr.proposal': '這個提案', |
| 518 | 'dr.contracts': '合約', |
| 519 | 'dr.conn': '連線', |
| 520 | 'dr.stream': '事件串流', |
| 521 | 'dr.about': '證據雜湊怎麼來', |
| 522 | 'dr.kind': '類型', |
| 523 | 'dr.target': '目標', |
| 524 | 'dr.codehash': 'codehash', |
| 525 | 'dr.proposer': '提案者', |
| 526 | 'dr.proposed': '提案', |
| 527 | 'dr.cooldown': '冷靜期', |
| 528 | 'dr.cooldownV': '#{a} → #{b}({n} 塊)', |
| 529 | 'dr.verdictTx': '判定交易', |
| 530 | 'dr.cost': '判定交易成本', |
| 531 | 'dr.costV': '{c}(gasUsed {g} × {p})', |
| 532 | 'dr.evidence': '證據雜湊', |
| 533 | 'dr.workflow': 'workflow id', |
| 534 | 'dr.executed': '執行', |
| 535 | 'dr.atBlock': '區塊 #{b}', |
| 536 | 'dr.reading': '讀取中…', |
| 537 | 'dr.verified': '✓ Sourcify 已驗證', |
| 538 | 'dr.heads': '區塊來源', |
| 539 | 'dr.logs': 'log 來源', |
| 540 | 'dr.bt': '出塊時間', |
| 541 | 'dr.endpoint': '節點', |
| 542 | 'dr.recording': '錄製', |
| 543 | 'dr.recordingV': '{src} · 區塊 #{a} – #{b} · 錄製於 {d}', |
| 544 | 'dr.noStream': '還沒有事件', |
| 545 | 'dr.aboutBody': |
| 546 | 'evidenceHash = keccak256(規範化證據 JSON:提案欄位、proposalDigest、判定與規則,鍵名排序)。工作流程用它的 attester 私鑰對這份證據簽出 EIP-712 判定;部署到 DON 時,節點還必須對同一份判定達成共識才會簽出報告。這裡的判定由官方 CRE CLI 在一台伺服器上執行這個工作流程產生,經 Chainlink 的 Sepolia 測試 forwarder 寫上鏈。AirlockReceiver 驗過 forwarder、EIP-712 簽章與 proposalDigest 才會放行或否決。任何人都能用鏈上資料重算這個雜湊。', |
| 547 | 'src.sepolia': 'Ethereum Sepolia', |
| 548 | 'src.fork': '本機 Sepolia 分叉', |
| 549 | 'link.ws': 'WebSocket newHeads', |
| 550 | 'link.poll': 'HTTP 輪詢,每 {s} 秒', |
| 551 | 'link.down': '重新連線中', |
| 552 | 'link.logsWs': 'WebSocket 訂閱', |
| 553 | 'link.logsPoll': 'eth_getLogs 游標 #{c}', |
| 554 | 'link.connecting': '連線中…', |
| 555 | 'link.offline': '不連任何網路', |
| 556 | 'bt.measured': '實測 {s} 秒/塊', |
| 557 | 'bt.fast': '快轉挖塊(分叉)', |
| 558 | 'bt.measuring': '測量中…', |
| 559 | 'help.title': '講者快捷鍵', |
| 560 | 'help.prev': '上一個(較舊)提案', |
| 561 | 'help.next': '下一個(較新)提案', |
| 562 | 'help.newest': '跳到最新提案', |
| 563 | 'help.replay': '重播選取提案的判定瞬間', |
| 564 | 'help.follow': '自動跟隨新提案並播放判定(離線:回放循環)', |
| 565 | 'help.loop': '播放/暫停回放循環', |
| 566 | 'help.details': '開發者詳情抽屜', |
| 567 | 'help.lang': '切換語言', |
| 568 | 'help.help': '顯示/隱藏這份說明', |
| 569 | 'help.esc': '關閉面板', |
| 570 | 'help.space': '空白鍵', |
| 571 | 'help.url': '網址參數', |
| 572 | 'help.urlBody': '?lang=en | zh-CN | zh-TW · ?mode=offline 離線回放 · ?p=<提案編號> · ?clean=1 錄製用,不顯示講者控制項 · ?rpc=<本機分叉>', |
| 573 | 'help.foot': '按 ? 或 Esc 關閉 · 滑鼠靜止 3 秒後講者控制項自動隱藏', |
| 574 | 'lab.title': '選一種攻擊,看 Chainlink CRE 怎麼攔下它', |
| 575 | 'lab.sub': '每張卡都重播 Ethereum Sepolia 上一筆真實提案', |
| 576 | 'lab.invite': '剛才是「{name}」的真實重播 · 再選一種攻擊看看', |
| 577 | 'lab.tabLab': '攻擊重播', |
| 578 | 'lab.tabRecent': '鏈上提案', |
| 579 | 'lab.tabsTitle': '底部顯示攻擊重播或鏈上最近的提案', |
| 580 | 'lab.play': '看重播', |
| 581 | 'lab.again': '再看一次', |
| 582 | 'lab.none': '還沒有這種提案', |
| 583 | 'lab.loading': '讀取中…', |
| 584 | 'lab.live': '即時', |
| 585 | 'lab.liveFoot': '正在鏈上判定 · 點擊查看', |
| 586 | 'lab.now': '正在回放 #{id} · 真實 Sepolia 提案', |
| 587 | 'lab.proposalTx': '提案交易', |
| 588 | 'lab.st.arrive': '提案進外門', |
| 589 | 'lab.st.judge': 'CRE 判定中', |
| 590 | 'lab.st.veto': '否決 · 彈出', |
| 591 | 'lab.st.clear': '放行 · 內門解鎖', |
| 592 | 'lab.st.fast': '冷靜期快轉', |
| 593 | 'lab.st.exec': '執行 · 升級生效', |
| 594 | 'h.replayReal': '回放 · 真實 Sepolia 提案', |
| 595 | 'h.live': '即時', |
| 596 | 'st.replayOf': '回放 · Sepolia 提案 #{id}', |
| 597 | 'h.introEyebrow': '真實攻擊重播', |
| 598 | 'h.introTitle': '正在載入一筆真實攻擊…', |
| 599 | 'h.introDesc': '馬上從頭重播 Ethereum Sepolia 上的一筆真實提案:進外門 → 在氣閘艙等待 → Chainlink CRE 攻擊模擬後判定', |
| 600 | 'c.intro': '重播即將開始', |
| 601 | 'c.arriving': '提案 #{id} 抵達外門…', |
| 602 | 'b.intro': '內門鎖著:只有 Chainlink CRE 放行才會打開', |
| 603 | 'b.arriving': '提案先進外門,再在氣閘艙裡等冷靜期', |
| 604 | 'v.introChip': '等待重播', |
| 605 | 'v.introText': '重播走到判定那一步時,CRE 的結論會出現在這裡。', |
| 606 | 'h.slowTitle': '連線 Sepolia 比較慢…', |
| 607 | 'h.slowDesc': '公共 Sepolia RPC 很慢或連不上,頁面仍在重試。也可以先看錄製好的真實 Sepolia 執行:', |
| 608 | 'h.slowLink': '離線回放', |
| 609 | 'c.slow': '仍在讀取鏈上資料…', |
| 610 | 'c.step': 'CRE · {s}\u00a0{i}/{n}', |
| 611 | 'c.compressed': '回放 · 時間已壓縮', |
| 612 | 'c.ff': '冷靜期快轉 ×{n}', |
| 613 | 'h.onchain': '鏈上狀態 · 提案於 {d}', |
| 614 | 'h.onchainNoDate': '鏈上狀態', |
| 615 | 'v.replayTag': '回放', |
| 616 | 'h.pendEyebrow': '操作員剛剛發起 · 真實交易', |
| 617 | 'h.pendDesc': '交易已送出,正在等待打包進 Ethereum Sepolia 區塊;提案一上鏈就進外門', |
| 618 | 'c.pending': '等待上鏈…', |
| 619 | 'b.pending': '提案上鏈後進氣閘艙,由 Chainlink CRE 攻擊模擬後判定', |
| 620 | 'v.pendChip': '等待上鏈', |
| 621 | 'v.pendText': '提案上鏈後,Chainlink CRE 通常在 20–40 秒內給出判定。', |
| 622 | 'op.btn': '實戰發起', |
| 623 | 'op.badge': '操作員', |
| 624 | 'op.title': '在 Sepolia 上真實發起攻擊', |
| 625 | 'op.note': '以 owner 身分送出一筆真實的 Ethereum Sepolia 交易;Chainlink CRE 通常在 20–40 秒內判定,戰情室會自動跟隨。', |
| 626 | 'op.gap': '兩次發起至少間隔 {n} 塊', |
| 627 | 'op.launch': '在 Sepolia 上真實發起', |
| 628 | 'op.launchShort': '真實發起', |
| 629 | 'op.expectVeto': '預期:否決 {rule}', |
| 630 | 'op.expectClear': '預期:放行,冷靜期滿後可執行', |
| 631 | 'op.confirmQ': '確定在 Sepolia 上發起「{name}」?', |
| 632 | 'op.confirmBody': '這會送出一筆真實交易({fn}),送出後無法撤回。', |
| 633 | 'op.confirm': '確認發起', |
| 634 | 'op.cancel': '取消', |
| 635 | 'op.close': '關閉', |
| 636 | 'op.busy': '上一筆還在路上', |
| 637 | 'op.sending': '正在送出交易…', |
| 638 | 'op.sent': '交易已送出 · 等待上鏈…', |
| 639 | 'op.slow': '還沒看到這筆交易的提案 · 可在 Etherscan 查看', |
| 640 | 'op.onchain': '提案 #{id} 已進氣閘 · 等待 Chainlink CRE 判定', |
| 641 | 'op.veto': '提案 #{id} 已否決 · {who}{rule}', |
| 642 | 'op.clear': '提案 #{id} 已放行 · {who}', |
| 643 | 'op.exec': '提案 #{id} 已執行', |
| 644 | 'op.errCard': '這張卡不能發起', |
| 645 | 'op.errAuth': '操作員口令無效 · 已退出操作員模式', |
| 646 | 'op.errCooldown': '冷卻中 · 再等 {n} 塊(約 {t})', |
| 647 | 'op.errCooldownAny': '冷卻中 · 上一筆交易還沒確認,稍後再試', |
| 648 | 'op.errRpc': '節點沒有接受這筆交易 · 稍後再試', |
| 649 | 'op.errDisabled': '這裡的操作員模式已關閉 · 沒有送出', |
| 650 | 'op.errReverted': '節點試跑這筆提案會回滾 · 所以沒有送出', |
| 651 | 'op.errServer': '操作員服務出錯 · 沒有送出,稍後再試', |
| 652 | 'op.errMisconfigured': '伺服器上的私鑰不是這個部署的 owner · 沒有送出', |
| 653 | 'op.errForbidden': '操作員服務拒絕了這個來源(只接受本機或同一網站)· 沒有送出', |
| 654 | 'op.errUnclear': '操作員服務的回應看不懂 · 若交易已送出,戰情室會自動接上', |
| 655 | 'op.errNet': '連不上操作員服務 · 沒有收到回應', |
| 656 | 'op.authNone': '本機模式 · 不需要口令', |
| 657 | 'op.authToken': '已用口令登入', |
| 658 | 'op.signOut': '退出操作員模式', |
| 659 | 'op.tokenTitle': '操作員口令', |
| 660 | 'op.tokenBody': '口令只存在這台瀏覽器裡,不會出現在網址中。', |
| 661 | 'op.tokenBad': '口令格式不對', |
| 662 | 'op.tokenSave': '儲存', |
| 663 | 'help.lab': '重播第 1–7 種攻擊(攻擊卡片從左到右)', |
| 664 | 'help.operator': '實戰發起面板(操作員)', |
| 665 | 'rp.readyCleared': '冷靜期滿 #{id}:塊 {b},已放行,內門可以開', |
| 666 | 'rp.readyWaiting': '冷靜期滿 #{id}:塊 {b},還沒有放行,execute 會 revert NotCleared', |
| 667 | 'ev.proposed': 'Proposed #{id}:塊 {b},冷靜期滿 {rb},tx {tx}', |
| 668 | 'ev.vetoed': 'Vetoed #{id}:{who} 在塊 {b}{after} · {rule} · 第 {n} 票', |
| 669 | 'ev.cleared': 'Cleared #{id}:{who} 在塊 {b}{after} · 證據 {h}', |
| 670 | 'ev.executed': 'Executed #{id}:塊 {b},tx {tx}', |
| 671 | 'ev.reason': 'ReasonAttached #{id}:證據雜湊 {h}', |
| 672 | 'ev.verdict': 'Verdict #{id}:Chainlink CRE {what} · 證據 {h}', |
| 673 | 'ev.vClear': '放行', |
| 674 | 'ev.vVeto': '否決 {rule}', |
| 675 | 'ev.after': '(提案後 {n} 塊)', |
| 676 | 'ev.removed': '鏈重組:log {tx} 被移除', |
| 677 | 'ev.loaded': '已從鏈上載入最近 {n} 個提案(nextId {next})', |
| 678 | 'ev.loadFail': '載入最近提案失敗(第 {n} 次),稍後重試:{err}', |
| 679 | 'off.loaded': '離線回放:已載入錄製的 {n} 個提案({d}),不連任何網路', |
| 680 | 'dur.sec': '{s} 秒', |
| 681 | 'dur.min': '{m} 分 {s} 秒', |
| 682 | 'dur.minOnly': '{m} 分鐘', |
| 683 | } |
| 684 | |
| 685 | const EN: Record<Key, string> = { |
| 686 | 'doc.title': 'Upgrade Airlock · War Room', |
| 687 | 'brand.sub': 'Every proxy upgrade, cleared by Chainlink CRE', |
| 688 | 'st.live': 'Live · Ethereum Sepolia', |
| 689 | 'st.fork': 'Local Sepolia fork (rehearsal)', |
| 690 | 'st.offline': 'Replay · recorded on Ethereum Sepolia', |
| 691 | 'st.offlineFork': 'Replay · recorded on a Sepolia fork', |
| 692 | 'st.offlineEmpty': 'Replay · no recording yet', |
| 693 | 'st.connecting': 'Connecting to Sepolia…', |
| 694 | 'st.down': 'Reconnecting…', |
| 695 | 'st.block': 'Block', |
| 696 | 'cre.by': 'Verdicts by', |
| 697 | 'ctl.details': 'Details', |
| 698 | 'ctl.help': 'Shortcuts', |
| 699 | 'ctl.langTitle': 'Switch language (L)', |
| 700 | 'ctl.detailsTitle': 'Developer details (D)', |
| 701 | 'ctl.helpTitle': 'Presenter shortcuts (?)', |
| 702 | 'ph.idle': 'Empty', |
| 703 | 'ph.chamber': 'Cooling', |
| 704 | 'ph.cleared': 'Cleared', |
| 705 | 'ph.waiting': 'No clearance', |
| 706 | 'ph.ready': 'Ready', |
| 707 | 'ph.executed': 'Executed', |
| 708 | 'ph.ejected': 'Vetoed', |
| 709 | 'who.cre': 'Chainlink CRE', |
| 710 | 'who.human': 'the emergency guardian', |
| 711 | 'who.approver': 'the approver (by hand)', |
| 712 | 'who.someone': 'an unknown address', |
| 713 | 'who.unknown': 'unknown address {a}', |
| 714 | 'h.eyebrow': 'Proposal #{id} · {kind}', |
| 715 | 'h.replay': 'Replay · cooldown fast-forwarded', |
| 716 | 'h.replayPaused': 'Replay paused', |
| 717 | 'h.dropped': 'log removed by a reorg', |
| 718 | 'h.newImpl': 'New implementation {a}', |
| 719 | 'h.guardOff': 'Remove the {who} guardian', |
| 720 | 'h.guardOn': 'Add {who} as a guardian', |
| 721 | 'h.delay': 'Change the cooldown to {n} blocks', |
| 722 | 'h.paused': 'New break-glass target {a}', |
| 723 | 'h.nameRemoveCre': 'Remove CRE', |
| 724 | 'h.nameGuard': 'Guardian change', |
| 725 | 'h.nameDelay': 'Cooldown change', |
| 726 | 'h.namePaused': 'Break-glass change', |
| 727 | 'h.descRemoveCre': 'Tries to take Chainlink CRE off the guardian list', |
| 728 | 'h.descGuard': 'A change to the airlock itself, judged from chain reads', |
| 729 | 'h.descUnknown': 'An implementation this page does not recognise', |
| 730 | 'h.empty': 'The airlock is empty', |
| 731 | 'h.emptyDesc': 'A proposal waits {n} blocks (≈ {t}) in the chamber; the inner door only opens for a Chainlink CRE Clear', |
| 732 | 'h.loading': 'Reading proposals from Ethereum Sepolia…', |
| 733 | 'h.noSuch': 'No proposal #{id}', |
| 734 | 'h.noSuchDesc': 'The newest proposal on Ethereum Sepolia is #{n}; this page shows #{id} as soon as it is proposed', |
| 735 | 'h.pending': 'Contracts not deployed on Sepolia yet', |
| 736 | 'h.noRecording': 'No recording yet', |
| 737 | 'g.owner': 'Owner key', |
| 738 | 'g.ownerSub': 'can only propose', |
| 739 | 'g.proxy': 'DemoBTC proxy', |
| 740 | 'g.proxyIdle': 'runs current code', |
| 741 | 'g.proxySafe': 'untouched', |
| 742 | 'g.proxyLive': 'now runs {name}', |
| 743 | 'c.left': '{n|block|blocks}', |
| 744 | 'c.leftSub': 'cooldown left', |
| 745 | 'c.eta': '≈ {t}', |
| 746 | 'c.empty': 'Waiting for a proposal', |
| 747 | 'c.over': 'Cooldown over', |
| 748 | 'c.noClear': 'No clearance on chain yet', |
| 749 | 'c.ready': 'Ready to execute', |
| 750 | 'c.readySub': 'Cleared · cooldown over', |
| 751 | 'c.executed': 'Upgrade is live', |
| 752 | 'c.ejected': 'Ejected', |
| 753 | 'c.clearStamp': 'Cleared', |
| 754 | 'c.vetoAt': 'Vetoed {n} {n|block|blocks} after the proposal', |
| 755 | 'c.awaiting': 'Awaiting the Chainlink CRE verdict', |
| 756 | 'c.cleared': 'Cleared by Chainlink CRE', |
| 757 | 'c.clearedBy': 'Cleared by {who}', |
| 758 | 'b.idle': 'Waiting for an upgrade proposal', |
| 759 | 'b.chamber': 'In the airlock · awaiting the Chainlink CRE verdict', |
| 760 | 'b.cleared': 'Cleared by {who} · the inner door opens after the cooldown', |
| 761 | 'b.ready': 'Cleared by {who} · ready to execute', |
| 762 | 'b.waiting': 'Cooldown over · no clearance, the inner door stays locked', |
| 763 | 'b.executed': 'Executed · {name} is live', |
| 764 | 'b.ejected': 'Ejected · vetoed by {who}', |
| 765 | 'b.ejectedFar': 'Ejected · vetoed on chain', |
| 766 | 'd.outer': 'Outer door', |
| 767 | 'd.chamber': 'Chamber', |
| 768 | 'd.inner': 'Inner door', |
| 769 | 'd.outerIdle': 'Open for proposals', |
| 770 | 'd.outerOn': 'Proposed in block #{b}', |
| 771 | 'd.chamberSub': 'Cooldown {n} blocks ≈ {t}', |
| 772 | 'd.innerLocked': 'Locked · until a Chainlink CRE Clear', |
| 773 | 'd.innerUnlocked': 'Unlocked · opens at block #{b}', |
| 774 | 'd.innerReady': 'Unlocked · cooldown over', |
| 775 | 'd.innerOpen': 'Opened in block #{b}', |
| 776 | 'd.innerSealed': 'Sealed for good · execute reverts', |
| 777 | 'd.innerWaiting': 'Locked · no clearance', |
| 778 | 'v.title': 'Chainlink CRE verdict', |
| 779 | 'v.veto': 'Veto', |
| 780 | 'v.clear': 'Clear', |
| 781 | 'v.awaiting': 'Judging', |
| 782 | 'v.none': '—', |
| 783 | 'v.noneText': 'Once a proposal enters the airlock, its verdict lands on chain as a signed report.', |
| 784 | 'v.clearText': 'Survived the attack simulation: every rule held.', |
| 785 | 'v.clearTextCfg': 'The config check passed: the approver pre-approved it, or it does not weaken the airlock.', |
| 786 | 'v.awaitText': 'A Clear or a Veto lands on chain as a signed report.', |
| 787 | 'v.after': '<b>{n}</b> {n|block|blocks} · <b>{t}</b> after the proposal', |
| 788 | 'v.afterEst': '<b>{n}</b> {n|block|blocks} · ≈ <b>{t}</b> after the proposal', |
| 789 | 'v.human': 'Vetoed by the emergency guardian (a person), not by Chainlink CRE.', |
| 790 | 'v.approver': 'Cleared by the approver by hand; Chainlink CRE still judges it, and its veto wins.', |
| 791 | 'v.skipped': 'Executed without a Chainlink CRE verdict (cleared by the approver by hand).', |
| 792 | 'v.loadingChip': 'Reading', |
| 793 | 'v.loading': 'Reading the verdict from chain…', |
| 794 | 'v.far': 'Vetoed on chain (the veto log is out of reach).', |
| 795 | 'v.clearFar': 'Cleared on chain (the clearance log is out of reach).', |
| 796 | 'p.sim': 'Attack simulation', |
| 797 | 'p.simS': 'eth_simulateV1 on 4 RPC providers', |
| 798 | 'p.agree': 'All answers agree', |
| 799 | 'p.agreeS': 'at least 2 of 4 answer · every answer matches', |
| 800 | 'p.cfg': 'Config check', |
| 801 | 'p.cfgS': 'chain reads at the proposal block: approver pre-approval', |
| 802 | 'p.static': 'Approval check', |
| 803 | 'p.staticS': 'chain reads at the proposal block, no simulation needed', |
| 804 | 'p.simFail': 'Simulation undecided', |
| 805 | 'p.simFailS': 'no clear, agreed answer: fail-closed veto', |
| 806 | 'p.don': 'Consensus', |
| 807 | 'p.donS': 'verdict and rule must be identical to aggregate', |
| 808 | 'p.sign': 'Signed report', |
| 809 | 'p.signS': 'EIP-712, bound to this exact proposal', |
| 810 | 'p.chain': 'On chain', |
| 811 | 'p.chainS': 'forwarder → AirlockReceiver → AirlockAdmin', |
| 812 | 'p.landVeto': 'Veto on chain', |
| 813 | 'p.landClear': 'Clear on chain', |
| 814 | 'l.proposal': 'Proposal tx', |
| 815 | 'l.report': 'Report tx', |
| 816 | 'l.execute': 'Execute tx', |
| 817 | 'l.evidence': 'Evidence hash', |
| 818 | 's.title': 'Proposals on Ethereum Sepolia', |
| 819 | 's.titleOffline': 'Recorded proposals', |
| 820 | 's.titleFork': 'Proposals on the local fork', |
| 821 | 's.follow': 'Auto-follow', |
| 822 | 's.loop': 'Replay loop', |
| 823 | 's.on': 'on', |
| 824 | 's.off': 'off', |
| 825 | 's.hint': '← → step · N newest · Space replay · ? shortcuts', |
| 826 | 's.loading': 'Loading proposals…', |
| 827 | 's.none': 'No proposals yet', |
| 828 | 's.veto': 'Veto', |
| 829 | 's.clear': 'Clear', |
| 830 | 's.after': '+{n} {n|block|blocks}', |
| 831 | 's.left': '{n} {n|block|blocks} left', |
| 832 | 's.noClear': 'No clearance yet', |
| 833 | 's.executed': 'executed', |
| 834 | 's.ready': 'ready to execute', |
| 835 | 's.allHeld': 'Every rule held', |
| 836 | 's.cfgOk': 'Config check passed', |
| 837 | 's.awaiting': 'Awaiting the CRE verdict', |
| 838 | 's.replaying': 'Replaying', |
| 839 | 'dr.title': 'Details', |
| 840 | 'dr.close': 'Close (D / Esc)', |
| 841 | 'dr.proposal': 'This proposal', |
| 842 | 'dr.contracts': 'Contracts', |
| 843 | 'dr.conn': 'Connection', |
| 844 | 'dr.stream': 'Event stream', |
| 845 | 'dr.about': 'Where the evidence hash comes from', |
| 846 | 'dr.kind': 'Kind', |
| 847 | 'dr.target': 'Target', |
| 848 | 'dr.codehash': 'Codehash', |
| 849 | 'dr.proposer': 'Proposer', |
| 850 | 'dr.proposed': 'Proposed', |
| 851 | 'dr.cooldown': 'Cooldown', |
| 852 | 'dr.cooldownV': '#{a} → #{b} ({n} blocks)', |
| 853 | 'dr.verdictTx': 'Verdict tx', |
| 854 | 'dr.cost': 'Verdict tx cost', |
| 855 | 'dr.costV': '{c} (gasUsed {g} × {p})', |
| 856 | 'dr.evidence': 'Evidence hash', |
| 857 | 'dr.workflow': 'Workflow id', |
| 858 | 'dr.executed': 'Executed', |
| 859 | 'dr.atBlock': 'block #{b}', |
| 860 | 'dr.reading': 'reading…', |
| 861 | 'dr.verified': '✓ verified on Sourcify', |
| 862 | 'dr.heads': 'Block feed', |
| 863 | 'dr.logs': 'Log feed', |
| 864 | 'dr.bt': 'Block time', |
| 865 | 'dr.endpoint': 'Endpoint', |
| 866 | 'dr.recording': 'Recording', |
| 867 | 'dr.recordingV': '{src} · blocks #{a} – #{b} · recorded {d}', |
| 868 | 'dr.noStream': 'No events yet', |
| 869 | 'dr.aboutBody': |
| 870 | "evidenceHash = keccak256 of the canonical evidence JSON (the proposal fields, proposalDigest, verdict and rule, keys sorted). The workflow signs an EIP-712 verdict over this evidence with its attester key; on a DON deployment the nodes must also agree on the same verdict before a report is signed. These verdicts came from the official CRE CLI running the workflow on a server, written through Chainlink's Sepolia test forwarder. AirlockReceiver checks the forwarder, the EIP-712 signature and the proposalDigest before it clears or vetoes. Anyone can recompute the hash from chain data.", |
| 871 | 'src.sepolia': 'Ethereum Sepolia', |
| 872 | 'src.fork': 'a local Sepolia fork', |
| 873 | 'link.ws': 'WebSocket newHeads', |
| 874 | 'link.poll': 'HTTP polling every {s} s', |
| 875 | 'link.down': 'reconnecting', |
| 876 | 'link.logsWs': 'WebSocket subscription', |
| 877 | 'link.logsPoll': 'eth_getLogs cursor at #{c}', |
| 878 | 'link.connecting': 'connecting…', |
| 879 | 'link.offline': 'no network at all', |
| 880 | 'bt.measured': '{s} s per block (measured)', |
| 881 | 'bt.fast': 'fast-mined fork', |
| 882 | 'bt.measuring': 'measuring…', |
| 883 | 'help.title': 'Presenter shortcuts', |
| 884 | 'help.prev': 'Previous (older) proposal', |
| 885 | 'help.next': 'Next (newer) proposal', |
| 886 | 'help.newest': 'Jump to the newest proposal', |
| 887 | 'help.replay': "Replay the selected proposal's verdict moment", |
| 888 | 'help.follow': 'Auto-follow each new proposal (offline: replay loop)', |
| 889 | 'help.loop': 'Play / pause the replay loop', |
| 890 | 'help.details': 'Developer details drawer', |
| 891 | 'help.lang': 'Switch language', |
| 892 | 'help.help': 'Show / hide this help', |
| 893 | 'help.esc': 'Close panels', |
| 894 | 'help.space': 'Space', |
| 895 | 'help.url': 'URL options', |
| 896 | 'help.urlBody': '?lang=en | zh-CN | zh-TW · ?mode=offline replay · ?p=<proposal id> · ?clean=1 for recording, no presenter controls · ?rpc=<local fork>', |
| 897 | 'help.foot': 'Press ? or Esc to close · the presenter controls hide after 3 s without mouse movement', |
| 898 | 'lab.title': 'Pick an attack and watch Chainlink CRE stop it', |
| 899 | 'lab.sub': 'Each card replays a real proposal on Ethereum Sepolia', |
| 900 | 'lab.invite': 'That was a real replay of "{name}" · pick another attack', |
| 901 | 'lab.tabLab': 'Attack replays', |
| 902 | 'lab.tabRecent': 'On-chain proposals', |
| 903 | 'lab.tabsTitle': 'Show the attack replays or the newest proposals on chain', |
| 904 | 'lab.play': 'Watch the replay', |
| 905 | 'lab.again': 'Watch again', |
| 906 | 'lab.none': 'No proposal of this kind yet', |
| 907 | 'lab.loading': 'Loading…', |
| 908 | 'lab.live': 'Live', |
| 909 | 'lab.liveFoot': 'Being judged on chain · tap to see', |
| 910 | 'lab.now': 'Replaying #{id} · a real Sepolia proposal', |
| 911 | 'lab.proposalTx': 'Proposal tx', |
| 912 | 'lab.st.arrive': 'Enters outer door', |
| 913 | 'lab.st.judge': 'CRE judging', |
| 914 | 'lab.st.veto': 'Vetoed · ejected', |
| 915 | 'lab.st.clear': 'Cleared · unlocked', |
| 916 | 'lab.st.fast': 'Fast-forward', |
| 917 | 'lab.st.exec': 'Upgrade executed', |
| 918 | 'h.replayReal': 'Replay · a real Sepolia proposal', |
| 919 | 'h.live': 'Live', |
| 920 | 'st.replayOf': 'Replay · Sepolia proposal #{id}', |
| 921 | 'h.introEyebrow': 'Real attack replay', |
| 922 | 'h.introTitle': 'Loading a real attack…', |
| 923 | 'h.introDesc': 'In a moment a real proposal on Ethereum Sepolia replays from the start: outer door → chamber → Chainlink CRE attack-simulates it and rules', |
| 924 | 'c.intro': 'Replay starting…', |
| 925 | 'c.arriving': 'Proposal #{id} reaching the outer door…', |
| 926 | 'b.intro': 'The inner door stays locked: only a Chainlink CRE Clear opens it', |
| 927 | 'b.arriving': 'A proposal enters through the outer door, then waits out the cooldown in the chamber', |
| 928 | 'v.introChip': 'Awaiting the replay', |
| 929 | 'v.introText': 'The CRE verdict appears here when the replay reaches it.', |
| 930 | 'h.slowTitle': 'Sepolia is slow to answer…', |
| 931 | 'h.slowDesc': 'The public Sepolia RPCs are slow or unreachable; the page keeps retrying. Or watch the recorded real Sepolia run:', |
| 932 | 'h.slowLink': 'offline replay', |
| 933 | 'c.slow': 'Still reading the chain…', |
| 934 | 'c.step': 'CRE · {s}\u00a0{i}/{n}', |
| 935 | 'c.compressed': 'replay · time compressed', |
| 936 | 'c.ff': 'Cooldown fast-forward ×{n}', |
| 937 | 'h.onchain': 'On-chain state · proposed {d}', |
| 938 | 'h.onchainNoDate': 'On-chain state', |
| 939 | 'v.replayTag': 'Replay', |
| 940 | 'h.pendEyebrow': 'Just launched by the operator · a real transaction', |
| 941 | 'h.pendDesc': 'The transaction is out, waiting to be mined into an Ethereum Sepolia block; then the proposal enters the outer door', |
| 942 | 'c.pending': 'Waiting to be mined…', |
| 943 | 'b.pending': 'Once mined, the proposal waits in the chamber while Chainlink CRE attack-simulates it', |
| 944 | 'v.pendChip': 'Waiting for the block', |
| 945 | 'v.pendText': 'Once the proposal is on chain, Chainlink CRE usually rules within 20–40 s.', |
| 946 | 'op.btn': 'Launch live', |
| 947 | 'op.badge': 'Operator', |
| 948 | 'op.title': 'Launch a real attack on Sepolia', |
| 949 | 'op.note': 'Sends a real Ethereum Sepolia transaction as the owner; Chainlink CRE usually judges it within 20–40 s and the war room follows it.', |
| 950 | 'op.gap': 'At least {n} {n|block|blocks} between launches', |
| 951 | 'op.launch': 'Launch on Sepolia', |
| 952 | 'op.launchShort': 'Launch', |
| 953 | 'op.expectVeto': 'Expected: veto {rule}', |
| 954 | 'op.expectClear': 'Expected: clear, executable after the cooldown', |
| 955 | 'op.confirmQ': 'Launch "{name}" on Sepolia?', |
| 956 | 'op.confirmBody': 'This sends a real transaction ({fn}); it cannot be taken back.', |
| 957 | 'op.confirm': 'Confirm launch', |
| 958 | 'op.cancel': 'Cancel', |
| 959 | 'op.close': 'Close', |
| 960 | 'op.busy': 'The last one is still on its way', |
| 961 | 'op.sending': 'Sending the transaction…', |
| 962 | 'op.sent': 'Transaction sent · waiting to be mined…', |
| 963 | 'op.slow': 'No proposal from this transaction yet · check it on Etherscan', |
| 964 | 'op.onchain': 'Proposal #{id} is in the airlock · awaiting the Chainlink CRE verdict', |
| 965 | 'op.veto': 'Proposal #{id} vetoed · {who}{rule}', |
| 966 | 'op.clear': 'Proposal #{id} cleared · {who}', |
| 967 | 'op.exec': 'Proposal #{id} executed', |
| 968 | 'op.errCard': 'This card cannot be launched', |
| 969 | 'op.errAuth': 'Operator token rejected · operator mode is off', |
| 970 | 'op.errCooldown': 'Cooling down · wait {n} more {n|block|blocks} (≈ {t})', |
| 971 | 'op.errCooldownAny': 'Cooling down · the last transaction is not confirmed yet, try again shortly', |
| 972 | 'op.errRpc': 'The node did not take the transaction · try again shortly', |
| 973 | 'op.errDisabled': 'Operator mode is switched off here · nothing was sent', |
| 974 | 'op.errReverted': "The node's dry run of this proposal reverted · nothing was sent", |
| 975 | 'op.errServer': 'The operator service failed · nothing was sent, try again shortly', |
| 976 | 'op.errMisconfigured': "The server's key is not this deployment's owner · nothing was sent", |
| 977 | 'op.errForbidden': 'The operator service refused this origin (same site or this machine only) · nothing was sent', |
| 978 | 'op.errUnclear': 'Unreadable answer from the operator service · if it was sent, the war room picks it up', |
| 979 | 'op.errNet': 'Cannot reach the operator service · no answer', |
| 980 | 'op.authNone': 'Local mode · no token needed', |
| 981 | 'op.authToken': 'Signed in with a token', |
| 982 | 'op.signOut': 'Leave operator mode', |
| 983 | 'op.tokenTitle': 'Operator token', |
| 984 | 'op.tokenBody': 'The token stays in this browser and never in the address bar.', |
| 985 | 'op.tokenBad': 'That does not look like a token', |
| 986 | 'op.tokenSave': 'Save', |
| 987 | 'help.lab': 'Replay attack 1–7 (the picker cards, left to right)', |
| 988 | 'help.operator': 'Live launch panel (operator)', |
| 989 | 'rp.readyCleared': 'Cooldown over #{id}: block {b}, cleared, the inner door can open', |
| 990 | 'rp.readyWaiting': 'Cooldown over #{id}: block {b}, not cleared yet, execute reverts NotCleared', |
| 991 | 'ev.proposed': 'Proposed #{id}: block {b}, cooldown ends {rb}, tx {tx}', |
| 992 | 'ev.vetoed': 'Vetoed #{id}: {who} in block {b}{after} · {rule} · vote {n}', |
| 993 | 'ev.cleared': 'Cleared #{id}: {who} in block {b}{after} · evidence {h}', |
| 994 | 'ev.executed': 'Executed #{id}: block {b}, tx {tx}', |
| 995 | 'ev.reason': 'ReasonAttached #{id}: evidence hash {h}', |
| 996 | 'ev.verdict': 'Verdict #{id}: Chainlink CRE {what} · evidence {h}', |
| 997 | 'ev.vClear': 'Clear', |
| 998 | 'ev.vVeto': 'Veto {rule}', |
| 999 | 'ev.after': ' ({n} {n|block|blocks} after the proposal)', |
| 1000 | 'ev.removed': 'Reorg: log {tx} removed', |
| 1001 | 'ev.loaded': 'Loaded the newest {n} proposals from the chain (nextId {next})', |
| 1002 | 'ev.loadFail': 'Loading recent proposals failed (try {n}), retrying: {err}', |
| 1003 | 'off.loaded': 'Offline replay: {n} recorded proposals loaded ({d}), no network', |
| 1004 | 'dur.sec': '{s} s', |
| 1005 | 'dur.min': '{m} min {s} s', |
| 1006 | 'dur.minOnly': '{m} min', |
| 1007 | } |
| 1008 | |
| 1009 | // ---------- open sets: rules, proposal kinds, demo implementations ---------- |
| 1010 | type Table = Record<string, string> |
| 1011 | /** What the rule caught, in one plain sentence (README rules table) */ |
| 1012 | const RULES: Record<Lang, Table> = { |
| 1013 | 'zh-CN': { |
| 1014 | R1_TOTAL_SUPPLY: '陌生地址的一次调用就改变了总供应量。', |
| 1015 | R2_OWNER: '陌生地址的一次调用就改变了 owner。', |
| 1016 | R3_TRANSFER_CONSERVATION: '持有人转出的金额和实际移动的金额不一致。', |
| 1017 | R3B_THIRD_PARTY_FUNDS: '外人没有授权就搬走了持有人的代币。', |
| 1018 | R4_TRANSFER_LIVENESS: '持有人的正常转账失败了。', |
| 1019 | R5_STRICT_CODEHASH: '严格模式:approver 还没核准这个 codehash。', |
| 1020 | R6_SIM_FAILURE: '模拟无法判定(RPC 不一致、回答太少或模拟失败),fail-closed 否决。', |
| 1021 | R7_CONFIG_GUARD: '没有 approver 预先核准的 guardian、冷静期或 break-glass 变更。', |
| 1022 | R8_INIT_DATA: '升级夹带了 approver 没核准的初始化数据。', |
| 1023 | R9_NEW_OPCODES: '代码含有在真实区块和模拟里行为不同的 opcode(如 BASEFEE)。', |
| 1024 | }, |
| 1025 | 'zh-TW': { |
| 1026 | R1_TOTAL_SUPPLY: '陌生地址的一次呼叫就改變了總供應量。', |
| 1027 | R2_OWNER: '陌生地址的一次呼叫就改變了 owner。', |
| 1028 | R3_TRANSFER_CONSERVATION: '持有人轉出的金額和實際移動的金額不一致。', |
| 1029 | R3B_THIRD_PARTY_FUNDS: '外人沒有授權就搬走了持有人的代幣。', |
| 1030 | R4_TRANSFER_LIVENESS: '持有人的正常轉帳失敗了。', |
| 1031 | R5_STRICT_CODEHASH: '嚴格模式:approver 還沒核准這個 codehash。', |
| 1032 | R6_SIM_FAILURE: '模擬無法判定(RPC 不一致、回答太少或模擬失敗),fail-closed 否決。', |
| 1033 | R7_CONFIG_GUARD: '沒有 approver 預先核准的 guardian、冷靜期或 break-glass 變更。', |
| 1034 | R8_INIT_DATA: '升級夾帶了 approver 沒核准的初始化資料。', |
| 1035 | R9_NEW_OPCODES: '程式碼含有在真實區塊和模擬裡行為不同的 opcode(如 BASEFEE)。', |
| 1036 | }, |
| 1037 | en: { |
| 1038 | R1_TOTAL_SUPPLY: "A stranger's call changed the total supply.", |
| 1039 | R2_OWNER: "A stranger's call changed the owner.", |
| 1040 | R3_TRANSFER_CONSERVATION: "A holder's transfer did not move exactly the amount sent.", |
| 1041 | R3B_THIRD_PARTY_FUNDS: "An outsider moved a holder's tokens without an allowance.", |
| 1042 | R4_TRANSFER_LIVENESS: "A plain holder transfer failed.", |
| 1043 | R5_STRICT_CODEHASH: 'Strict mode: the approver has not approved this codehash.', |
| 1044 | R6_SIM_FAILURE: 'The simulation could not decide (providers disagreed, too few answered or it failed): fail-closed.', |
| 1045 | R7_CONFIG_GUARD: "A guardian, cooldown or break-glass change without the approver's pre-approval.", |
| 1046 | R8_INIT_DATA: 'The upgrade carries init data the approver has not approved.', |
| 1047 | R9_NEW_OPCODES: 'The code uses an opcode that acts differently on a real block than in a simulation (e.g. BASEFEE).', |
| 1048 | }, |
| 1049 | } |
| 1050 | /** Rule names short enough for a proposal card */ |
| 1051 | const RULES_SHORT: Record<Lang, Table> = { |
| 1052 | 'zh-CN': { |
| 1053 | R1_TOTAL_SUPPLY: '总供应量被改', |
| 1054 | R2_OWNER: 'owner 被夺', |
| 1055 | R3_TRANSFER_CONSERVATION: '转账不守恒', |
| 1056 | R3B_THIRD_PARTY_FUNDS: '持有人的币被搬', |
| 1057 | R4_TRANSFER_LIVENESS: '转账失效', |
| 1058 | R5_STRICT_CODEHASH: 'codehash 未核准', |
| 1059 | R6_SIM_FAILURE: '模拟无法判定', |
| 1060 | R7_CONFIG_GUARD: '配置变更未核准', |
| 1061 | R8_INIT_DATA: '初始化数据未核准', |
| 1062 | R9_NEW_OPCODES: '规避模拟的 opcode', |
| 1063 | }, |
| 1064 | 'zh-TW': { |
| 1065 | R1_TOTAL_SUPPLY: '總供應量被改', |
| 1066 | R2_OWNER: 'owner 被奪', |
| 1067 | R3_TRANSFER_CONSERVATION: '轉帳不守恆', |
| 1068 | R3B_THIRD_PARTY_FUNDS: '持有人的幣被搬', |
| 1069 | R4_TRANSFER_LIVENESS: '轉帳失效', |
| 1070 | R5_STRICT_CODEHASH: 'codehash 未核准', |
| 1071 | R6_SIM_FAILURE: '模擬無法判定', |
| 1072 | R7_CONFIG_GUARD: '設定變更未核准', |
| 1073 | R8_INIT_DATA: '初始化資料未核准', |
| 1074 | R9_NEW_OPCODES: '規避模擬的 opcode', |
| 1075 | }, |
| 1076 | en: { |
| 1077 | R1_TOTAL_SUPPLY: 'Supply changed', |
| 1078 | R2_OWNER: 'Owner seized', |
| 1079 | R3_TRANSFER_CONSERVATION: 'Transfer not conserved', |
| 1080 | R3B_THIRD_PARTY_FUNDS: 'Holder funds moved', |
| 1081 | R4_TRANSFER_LIVENESS: 'Transfers broken', |
| 1082 | R5_STRICT_CODEHASH: 'Codehash not approved', |
| 1083 | R6_SIM_FAILURE: 'Simulation undecided', |
| 1084 | R7_CONFIG_GUARD: 'Config not approved', |
| 1085 | R8_INIT_DATA: 'Unapproved init data', |
| 1086 | R9_NEW_OPCODES: 'Evades simulation', |
| 1087 | }, |
| 1088 | } |
| 1089 | const KINDS: Record<Lang, Table> = { |
| 1090 | 'zh-CN': { '0': '升级', '1': 'guardian 变更', '2': '冷静期变更', '3': '暂停实现变更' }, |
| 1091 | 'zh-TW': { '0': '升級', '1': 'guardian 變更', '2': '冷靜期變更', '3': '暫停實作變更' }, |
| 1092 | en: { '0': 'Upgrade', '1': 'Guardian change', '2': 'Cooldown change', '3': 'Break-glass change' }, |
| 1093 | } |
| 1094 | const CARD_DESC: Record<Lang, Record<CardId, string>> = { |
| 1095 | 'zh-CN': { |
| 1096 | evilMint: '任何人都能 mint:无限增发,selector 与 v1 完全相同', |
| 1097 | evilOwner: '新增 claimOwner():任何人都能夺走 owner', |
| 1098 | evilFee: 'selector 完全不变,每笔转账偷抽 10% 给 owner', |
| 1099 | evilBaseFee: '模拟里(basefee 为 0)表现得像 v1,上了真实区块任何人都能 mint', |
| 1100 | evilDrain: '新增 rescue():任何人都能不经授权搬走持有人的币', |
| 1101 | v2: '诚实的升级:只多一个 version()', |
| 1102 | }, |
| 1103 | 'zh-TW': { |
| 1104 | evilMint: '任何人都能 mint:無限增發,selector 與 v1 完全相同', |
| 1105 | evilOwner: '新增 claimOwner():任何人都能奪走 owner', |
| 1106 | evilFee: 'selector 完全不變,每筆轉帳偷抽 10% 給 owner', |
| 1107 | evilBaseFee: '模擬裡(basefee 為 0)表現得像 v1,上了真實區塊任何人都能 mint', |
| 1108 | evilDrain: '新增 rescue():任何人都能不經授權搬走持有人的幣', |
| 1109 | v2: '誠實的升級:只多一個 version()', |
| 1110 | }, |
| 1111 | en: { |
| 1112 | evilMint: 'Anyone can mint: unlimited supply behind the same selectors as v1', |
| 1113 | evilOwner: 'Adds claimOwner(): anyone can seize ownership', |
| 1114 | evilFee: 'Same selectors; every transfer skims 10% to the owner', |
| 1115 | evilBaseFee: 'Acts like v1 in a simulation (basefee 0); on a real block anyone can mint', |
| 1116 | evilDrain: "Adds rescue(): anyone can move a holder's tokens without an allowance", |
| 1117 | v2: 'The honest upgrade: it only adds version()', |
| 1118 | }, |
| 1119 | } |
| 1120 | |
| 1121 | /** Attack picker: each card's short name and one plain sentence of what the attack does (lab.ts LAB_CARDS) */ |
| 1122 | const LAB_NAME: Record<Lang, Record<LabId, string>> = { |
| 1123 | 'zh-CN': { |
| 1124 | 'evil-mint': '任意增发', |
| 1125 | 'evil-owner': '抢走 owner', |
| 1126 | 'evil-fee': '转账抽成', |
| 1127 | 'evil-basefee': '只在真实区块作恶', |
| 1128 | 'evil-drain': '搬走持有人的币', |
| 1129 | 'remove-receiver': '先撤掉 CRE', |
| 1130 | v2: '诚实升级', |
| 1131 | }, |
| 1132 | 'zh-TW': { |
| 1133 | 'evil-mint': '任意增發', |
| 1134 | 'evil-owner': '搶走 owner', |
| 1135 | 'evil-fee': '轉帳抽成', |
| 1136 | 'evil-basefee': '只在真實區塊作惡', |
| 1137 | 'evil-drain': '搬走持有人的幣', |
| 1138 | 'remove-receiver': '先撤掉 CRE', |
| 1139 | v2: '誠實升級', |
| 1140 | }, |
| 1141 | en: { |
| 1142 | 'evil-mint': 'Infinite mint', |
| 1143 | 'evil-owner': 'Steal ownership', |
| 1144 | 'evil-fee': 'Skim transfers', |
| 1145 | 'evil-basefee': 'On-chain-only evil', |
| 1146 | 'evil-drain': 'Drain holders', |
| 1147 | 'remove-receiver': 'Remove CRE first', |
| 1148 | v2: 'Honest upgrade', |
| 1149 | }, |
| 1150 | } |
| 1151 | const LAB_LINE: Record<Lang, Record<LabId, string>> = { |
| 1152 | 'zh-CN': { |
| 1153 | 'evil-mint': '升级后任何人都能凭空增发代币', |
| 1154 | 'evil-owner': '升级后任何人都能把合约的 owner 抢走', |
| 1155 | 'evil-fee': '每笔转账偷抽 10% 给 owner,函数一个都没变', |
| 1156 | 'evil-basefee': '模拟里装成好人,上了真实区块才让人增发', |
| 1157 | 'evil-drain': '新增 rescue():不经授权就能搬走别人的币', |
| 1158 | 'remove-receiver': '先把 CRE 移出 guardian 名单,再来就没人拦', |
| 1159 | v2: '只多一个 version() 的诚实升级:放行后执行', |
| 1160 | }, |
| 1161 | 'zh-TW': { |
| 1162 | 'evil-mint': '升級後任何人都能憑空增發代幣', |
| 1163 | 'evil-owner': '升級後任何人都能把合約的 owner 搶走', |
| 1164 | 'evil-fee': '每筆轉帳偷抽 10% 給 owner,函式一個都沒變', |
| 1165 | 'evil-basefee': '模擬裡裝成好人,上了真實區塊才讓人增發', |
| 1166 | 'evil-drain': '新增 rescue():不經授權就能搬走別人的幣', |
| 1167 | 'remove-receiver': '先把 CRE 移出 guardian 名單,再來就沒人攔', |
| 1168 | v2: '只多一個 version() 的誠實升級:放行後執行', |
| 1169 | }, |
| 1170 | en: { |
| 1171 | 'evil-mint': 'After the upgrade anyone can mint tokens out of thin air', |
| 1172 | 'evil-owner': 'After the upgrade anyone can take over the owner role', |
| 1173 | 'evil-fee': 'Every transfer quietly skims 10% to the owner', |
| 1174 | 'evil-basefee': 'Plays nice in a simulation, lets anyone mint on a real block', |
| 1175 | 'evil-drain': "Adds rescue(): anyone can move other people's tokens", |
| 1176 | 'remove-receiver': 'Takes CRE off the guardian list before the real attack', |
| 1177 | v2: 'Only adds version(): cleared, then executed', |
| 1178 | }, |
| 1179 | } |
| 1180 | |
| 1181 | const prefix = (pre: string, t: Table) => Object.fromEntries(Object.entries(t).map(([k, v]) => [pre + k, v])) |
| 1182 | const BASE: Record<Lang, Record<Key, string>> = { 'zh-CN': ZH_CN, 'zh-TW': ZH_TW, en: EN } |
| 1183 | export const DICT: Record<Lang, Table> = Object.fromEntries( |
| 1184 | LANGS.map((l) => [ |
| 1185 | l, |
| 1186 | { ...BASE[l], ...prefix('rule.', RULES[l]), ...prefix('ruleS.', RULES_SHORT[l]), ...prefix('kind.', KINDS[l]), ...prefix('card.', CARD_DESC[l]), ...prefix('labN.', LAB_NAME[l]), ...prefix('labL.', LAB_LINE[l]) }, |
| 1187 | ]), |
| 1188 | ) as unknown as Record<Lang, Table> |
| 1189 | |
| 1190 | // ---------- current language ---------- |
| 1191 | let cur: Lang = 'zh-CN' |
| 1192 | const listeners = new Set<() => void>() |
| 1193 | |
| 1194 | export const isLang = (x: unknown): x is Lang => typeof x === 'string' && (LANGS as readonly string[]).includes(x) |
| 1195 | export const lang = (): Lang => cur |
| 1196 | export function onLangChange(fn: () => void) { |
| 1197 | listeners.add(fn) |
| 1198 | } |
| 1199 | |
| 1200 | function apply() { |
| 1201 | if (typeof document === 'undefined') return |
| 1202 | try { |
| 1203 | document.title = t('doc.title') |
| 1204 | if (document.documentElement) document.documentElement.lang = cur |
| 1205 | } catch { |
| 1206 | /* the Node test DOM has none of this */ |
| 1207 | } |
| 1208 | } |
| 1209 | |
| 1210 | /** Once on page load: ?lang= first, then localStorage, else zh-CN */ |
| 1211 | export function initLang(): Lang { |
| 1212 | let pick: Lang = 'zh-CN' |
| 1213 | try { |
| 1214 | const q = new URLSearchParams(globalThis.location?.search ?? '').get('lang') |
| 1215 | if (isLang(q)) pick = q |
| 1216 | else { |
| 1217 | const s = globalThis.localStorage?.getItem(LS_KEY) |
| 1218 | if (isLang(s)) pick = s |
| 1219 | } |
| 1220 | } catch { |
| 1221 | /* private mode etc.: default */ |
| 1222 | } |
| 1223 | cur = pick |
| 1224 | apply() |
| 1225 | return cur |
| 1226 | } |
| 1227 | |
| 1228 | export function setLang(l: Lang, persist = true) { |
| 1229 | if (!isLang(l)) return |
| 1230 | cur = l |
| 1231 | apply() |
| 1232 | if (persist) { |
| 1233 | try { |
| 1234 | globalThis.localStorage?.setItem(LS_KEY, l) |
| 1235 | } catch { |
| 1236 | /* ignore */ |
| 1237 | } |
| 1238 | // keep ?lang= in the address bar in step, so a reload keeps the language (no navigation) |
| 1239 | try { |
| 1240 | const u = new URL(location.href) |
| 1241 | if (u.searchParams.has('lang')) { |
| 1242 | u.searchParams.set('lang', l) |
| 1243 | history.replaceState(history.state, '', u) |
| 1244 | } |
| 1245 | } catch { |
| 1246 | /* ignore */ |
| 1247 | } |
| 1248 | } |
| 1249 | for (const fn of listeners) fn() |
| 1250 | } |
| 1251 | |
| 1252 | export function cycleLang(): Lang { |
| 1253 | setLang(LANGS[(LANGS.indexOf(cur) + 1) % LANGS.length]) |
| 1254 | return cur |
| 1255 | } |
| 1256 | |
| 1257 | // ---------- translation ---------- |
| 1258 | /** {name} is replaced by the param; {name|one|many} picks a word by the param's number (English plurals) */ |
| 1259 | function fill(l: Lang, s: string, p?: Record<string, Param>): string { |
| 1260 | if (!p) return s |
| 1261 | return s.replace(/\{(\w+)(?:\|([^|{}]*)\|([^|{}]*))?\}/g, (all, name: string, one?: string, many?: string) => { |
| 1262 | const v = p[name] |
| 1263 | if (v === undefined || v === null) return all |
| 1264 | if (one !== undefined) return Number(v) === 1 ? one : (many ?? '') |
| 1265 | return typeof v === 'object' ? tIn(l, v.k, v.p) : String(v) |
| 1266 | }) |
| 1267 | } |
| 1268 | |
| 1269 | /** Translate in a given language (missing: zh-CN, then the key itself) */ |
| 1270 | export function tIn(l: Lang, key: string, p?: Record<string, Param>): string { |
| 1271 | return fill(l, DICT[l][key] ?? DICT['zh-CN'][key] ?? key, p) |
| 1272 | } |
| 1273 | |
| 1274 | /** Current language. Params go in as they are: the caller esc()s outside values that end up in innerHTML */ |
| 1275 | export function t(key: Key, p?: Record<string, Param>): string { |
| 1276 | return tIn(cur, key, p) |
| 1277 | } |
| 1278 | export const tm = (m: Msg): string => tIn(cur, m.k, m.p) |
| 1279 | |
| 1280 | const tOr = (key: string, fallback: string): string => DICT[cur][key] ?? DICT['zh-CN'][key] ?? fallback |
| 1281 | export const ruleText = (rule: string): string => tOr(`rule.${rule}`, rule) |
| 1282 | export const ruleShortText = (rule: string): string => tOr(`ruleS.${rule}`, rule) |
| 1283 | export const kindText = (kind: string): string => tOr(`kind.${kind}`, `kind ${kind}`) |
| 1284 | export const cardDesc = (id: CardId): string => tOr(`card.${id}`, id) |
| 1285 | export const labName = (id: LabId): string => tOr(`labN.${id}`, id) |
| 1286 | export const labLine = (id: LabId): string => tOr(`labL.${id}`, id) |