From 352ca69bec50f0d1259d5681f8db98ea52fccdc1 Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 11:36:47 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fb5=20UI-TW:=20the=20Traditional=20Chinese?= =?UTF-8?q?=20interface=20=E2=80=94=20all=201=20035=20msgids=20of=20en.jso?= =?UTF-8?q?n=20converted=20from=20the=20Simplified=20Chinese=20with=20Open?= =?UTF-8?q?CC=20(cn=20=E2=86=92=20twp,=20Taiwan=20phrases),=20the=20offici?= =?UTF-8?q?al=20TW=20client's=20terms=20kept=20where=20the=20phrase=20step?= =?UTF-8?q?=20words=20a=20game=20concept=20differently=20(=E8=AA=BF?= =?UTF-8?q?=E5=BA=A6=E4=B8=AD=E5=BF=83=20not=20=E6=8E=92=E7=A8=8B=E4=B8=AD?= =?UTF-8?q?=E5=BF=83,=20=E6=9B=B4=E6=96=B0=20for=20the=20shop=20refresh,?= =?UTF-8?q?=20=E5=95=9F=E5=8B=95=20for=20bonds,=20=E9=81=A0=E7=A8=8B?= =?UTF-8?q?=E4=BD=8D,=20=E9=87=8D=E8=A3=9D,=20=E6=9F=A5=E7=9C=8B,=20?= =?UTF-8?q?=E7=9B=AE=E5=89=8D,=20=E7=8D=B2=E5=BE=97,=20=E6=8B=96=E6=9B=B3,?= =?UTF-8?q?=20=E6=9C=80=E5=A4=9A,=20=E9=8A=B7=E6=AF=80,=20=E5=9B=9E?= =?UTF-8?q?=E5=BE=A9,=20=E7=8D=8E=E6=9D=AF,=20=E5=93=81=E8=B3=AA,=20?= =?UTF-8?q?=E8=BA=AB=E5=88=86,=20=E9=AB=98=E5=8F=B0,=20=E7=B6=AD=E5=BC=8F?= =?UTF-8?q?=E9=87=8D=E9=8E=9A;=2047=20whole=20strings=20taken=20from=20the?= =?UTF-8?q?=20TW=20tables,=20e.g.=20=E6=99=82=E9=96=93=E8=BC=83=E7=9F=AD?= =?UTF-8?q?=E7=9A=84=E6=A8=A1=E6=93=AC=E8=A8=93=E7=B7=B4),=20then=20read?= =?UTF-8?q?=20through=20by=20hand=20(=E5=8F=AA=20/=20=E9=9A=BB,=20?= =?UTF-8?q?=E7=B6=B2=E7=B5=A1=20in=20the=20company=20name,=20=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E6=95=B4=E7=90=86=20for=20a=20page=20refresh,=20?= =?UTF-8?q?=E9=80=A3=E7=B7=9A=E8=87=B3=E4=BC=BA=E6=9C=8D=E5=99=A8,=20?= =?UTF-8?q?=E5=85=A8=E8=9E=A2=E5=B9=95,=20=E6=96=87=E5=AD=97=E6=96=B9?= =?UTF-8?q?=E5=A1=8A,=20=E5=B7=A5=E4=BD=9C=E9=9A=8E=E6=AE=B5=20=E2=80=A6);?= =?UTF-8?q?=20=5Fmeta:=20complete,=20machineTranslated,=20the=20credits=20?= =?UTF-8?q?say=20how=20it=20was=20made;=20fallback=20unchanged=20(Simplifi?= =?UTF-8?q?ed=20Chinese)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/i18n/zh-TW.json | 1043 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 1040 insertions(+), 3 deletions(-) diff --git a/public/i18n/zh-TW.json b/public/i18n/zh-TW.json index eb3c1765..867bc748 100644 --- a/public/i18n/zh-TW.json +++ b/public/i18n/zh-TW.json @@ -9,11 +9,1048 @@ "authors": [ "the Stronghold Protocol contributors" ], - "credits": "Game texts (data/i18n/zh-TW.json): the official TW client data (© Hypergryph) via ArknightsAssets/ArknightsGamedata. The UI strings are not translated yet — they show in Simplified Chinese until a community translation fills them in (docs/I18N.md §5).", + "credits": "Game texts (data/i18n/zh-TW.json): the official TW client data (© Hypergryph) via ArknightsAssets/ArknightsGamedata. Interface strings machine-converted from the Simplified Chinese on 2026-10-07 (OpenCC, Taiwan phrases) using the official TW game terms, then read through by hand; corrections welcome on GitHub (docs/I18N.md §5).", "fallback": [], "numberUnits": [ "萬", "億" - ] - } + ], + "complete": true, + "machineTranslated": true + }, + "你已在其他同盟中,请先离开当前同盟": "你已在其他同盟中,請先離開目前的同盟", + "你已不在该同盟中": "你已不在該同盟中", + "创建者已离开,同盟已解散": "建立者已離開,同盟已解散", + "由于长时间断开连接,你已离开同盟": "由於長時間斷線,你已離開同盟", + "同盟已解散": "同盟已解散", + "你已被移出同盟": "你已被移出同盟", + "模拟已结束": "模擬已結束", + "同盟已过期": "同盟已過期", + "服务器维护中,同盟已关闭": "伺服器維護中,同盟已關閉", + "该身份已在其他页面登录,本页已断开": "該身分已在其他頁面登入,本頁已斷線", + "同盟已关闭:{reason}": "同盟已關閉:{reason}", + "同盟已关闭": "同盟已關閉", + "界面发生错误": "介面發生錯誤", + "重新加载界面": "重新載入介面", + "启动失败,请刷新页面重试": "啟動失敗,請重新整理頁面重試", + "时长较短的模拟训练": "時間較短的模擬訓練", + "可以快速完成作战": "可以快速完成作戰", + "常规奖励": "常規獎勵", + "敌方攻击强度较高的模拟训练": "敵方攻擊強度較高的模擬訓練", + "可使用盟约数增加": "可使用盟約數增加", + "大幅增加奖励": "大幅增加獎勵", + "敌方攻击强度极高的模拟训练": "敵方攻擊強度極高的模擬訓練", + "作战环境困难": "作戰環境困難", + "出现更加危险的敌人": "出現更加危險的敵人", + "敌方攻击强度到达极限的模拟训练": "敵方攻擊強度到達極限的模擬訓練", + "作战环境无比困难": "作戰環境無比困難", + "出现极度危险的敌人": "出現極度危險的敵人", + "敌方攻击强度较低的模拟训练": "敵方攻擊強度較低的模擬訓練", + "作战环境较为温和": "作戰環境較為溫和", + "战场固定为 {name}": "戰場固定為 {name}", + "战场固定": "戰場固定", + "战场随机(共{n}张)": "戰場隨機(共{n}張)", + "战场随机(共{stages}张)": "戰場隨機(共{stages}張)", + "独立模拟": "獨立模擬", + "独自调配资金与干员,以自己的节奏完成整场模拟。": "獨自調配資金與幹員,以自己的節奏完成整場模擬。", + "1 名博士": "1 名博士", + "休整期与机变阶段不限时": "休整期與機變階段不限時", + "同盟模拟": "同盟模擬", + "联防阶段 · 最终攻势合并生命值": "聯防階段 · 最終攻勢合併生命值", + "联合模拟在选择策略时可以进行一次跳过": "聯合模擬在選擇策略時可以進行一次跳過", + "调度中心即使冻结,依然可以主动刷新": "調度中心即使凍結,依然可以主動更新", + "两件同名装备可以合成一件更强力的装备": "兩件同名裝備可以合成一件更強力的裝備", + "只有达成完美作战的队友可以进行联防": "只有達成完美作戰的隊友可以進行聯防", + "作战提示": "作戰提示", + "上一条": "上一條", + "下一条": "下一條", + "已选定": "已選定", + "回合": "回合", + "+ 隐秘核心": "+ 隱秘核心", + "尚未连接到服务器,请稍候": "尚未連線到伺服器,請稍候", + "同盟密钥为 {ROOM_CODE_LEN} 位字母或数字": "同盟金鑰為 {ROOM_CODE_LEN} 位字母或數字", + "返回标题": "返回標題", + "返回": "返回", + "选择模拟协议": "選擇模擬協議", + "玩法说明": "玩法說明", + "干员调配": "幹員調配", + "博士": "博士", + "模拟方式": "模擬方式", + "加入同盟": "加入同盟", + "输入同盟密钥 / 粘贴邀请链接": "輸入同盟金鑰 / 貼上邀請連結", + "观战": "觀戰", + "最近的同盟": "最近的同盟", + "向同伴索取 {ROOM_CODE_LEN} 位同盟密钥,或直接打开邀请链接": "向同伴索取 {ROOM_CODE_LEN} 位同盟金鑰,或直接開啟邀請連結", + "模拟难度": "模擬難度", + "正在连接服务器…": "正在連線至伺服器…", + "开始独立模拟": "開始獨立模擬", + "创建同盟": "建立同盟", + "创建后即可开始模拟": "建立後即可開始模擬", + "创建后可邀请好友或添加 AI 队友": "建立後可邀請好友或新增 AI 隊友", + "等待博士加入": "等待博士加入", + "添加 AI 队友": "新增 AI 隊友", + "空位": "空位", + "创建者": "建立者", + "AI 队友": "AI 隊友", + "你": "你", + "已就绪": "已就緒", + "连接中断": "連線中斷", + "待命中": "待命中", + "准备中": "準備中", + "移除该 AI 队友": "移除該 AI 隊友", + "移除 AI 队友": "移除 AI 隊友", + "已复制同盟密钥 {code}": "已複製同盟金鑰 {code}", + "已复制邀请链接": "已複製邀請連結", + "{name}邀请你加入卫戍协议:盟约【{difficulty}】": "{name}邀請你加入衛戍協議:盟約【{difficulty}】", + "复制失败,请手动复制": "複製失敗,請手動複製", + "同盟密钥": "同盟金鑰", + "同盟密钥 {code}": "同盟金鑰 {code}", + "复制密钥": "複製金鑰", + "复制链接": "複製連結", + "由创建者选择": "由建立者選擇", + "连接中断,请稍候重试": "連線中斷,請稍候重試", + "离开同盟": "離開同盟", + "你是同盟的创建者,离开后创建者身份将移交或同盟解散。确定离开吗?": "你是同盟的建立者,離開後建立者身分將移交或同盟解散。確定離開嗎?", + "离开": "離開", + "连接中断,正在重连…": "連線中斷,正在重連…", + "*模拟协议已就绪,准许进入模拟": "*模擬協議已就緒,准許進入模擬", + "*同盟人数达标,准许进入模拟": "*同盟人數達標,准許進入模擬", + "等待所有博士准备就绪": "等待所有博士準備就緒", + "已就绪 · 等待创建者开始模拟": "已就緒 · 等待建立者開始模擬", + "准备就绪后,创建者即可开始模拟": "準備就緒後,建立者即可開始模擬", + "当前延迟": "目前延遲", + "仅限 1 名博士": "僅限 1 名博士", + ",满足条件时进入隐秘核心": ",滿足條件時進入隱秘核心", + "独立模拟中休整期与机变阶段不限时": "獨立模擬中休整期與機變階段不限時", + "仍有博士未准备就绪": "仍有博士未準備就緒", + "开始模拟": "開始模擬", + "准备就绪": "準備就緒", + "准备连接": "準備連線", + "正在连接服务器": "正在連線至伺服器", + "已连接服务器": "已連線至伺服器", + "正在验证身份": "正在驗證身分", + "连接中断,正在重连": "連線中斷,正在重連", + "连接已关闭": "連線已關閉", + "请输入博士代号": "請輸入博士代號", + "卫戍协议": "衛戍協議", + "盟约": "盟約", + "卫戍协议:盟约 · STRONGHOLD PROTOCOL": "衛戍協議:盟約 · STRONGHOLD PROTOCOL", + "调配资金与干员,与同伴协同布防,抵御多波次进攻,直至击败敌方领袖。": "調配資金與幹員,與同伴協同佈防,抵禦多波次進攻,直至擊敗敵方領袖。", + "收到同盟邀请": "收到同盟邀請", + "· 输入代号后将自动加入": "· 輸入代號後將自動加入", + "博士代号": "博士代號", + "输入你的代号(最多 {NAME_MAX_LEN} 字)": "輸入你的代號(最多 {NAME_MAX_LEN} 字)", + "开始": "開始", + "非官方同人复刻 · 游戏素材版权归 上海鹰角网络 / Yostar 所有": "非官方同人復刻 · 遊戲素材版權歸 上海鷹角網絡 / Yostar 所有", + "高": "高", + "中": "中", + "低": "低", + "刷新": "更新", + "冻结": "凍結", + "升级": "升級", + "设置": "設定", + "完成": "完成", + "背景音乐": "背景音樂", + "干员语音": "幹員語音", + "音效": "音效", + "静音": "靜音", + "显示伤害数字": "顯示傷害數字", + "画面质量": "畫面品質", + "触屏操作:点击单位选中(撤退 / 出售)· 长按单位或卡牌查看详情 · 拖动部署后滑动选择朝向": "觸控操作:點擊單位選中(撤退 / 出售)· 長按單位或卡牌查看詳情 · 拖動部署後滑動選擇朝向", + "快捷键": "快捷鍵", + "发生未知错误": "發生未知錯誤", + "标准模拟": "標準模擬", + "险境模拟": "險境模擬", + "绝境模拟": "絕境模擬", + "终极模拟": "終極模擬", + "无效的请求": "無效的請求", + "操作过于频繁": "操作過於頻繁", + "你不在房间中": "你不在房間中", + "未找到该同盟密钥对应的房间": "未找到該同盟金鑰對應的房間", + "房间已满": "房間已滿", + "模拟已开始": "模擬已開始", + "只有房主可以操作": "只有房主可以操作", + "仍有玩家未就绪": "仍有玩家未就緒", + "当前阶段无法进行该操作": "目前階段無法進行該操作", + "资金不足": "資金不足", + "整备区已满": "整備區已滿", + "已达到部署上限": "已達到部署上限", + "无法部署在该位置": "無法部署在該位置", + "无效的目标": "無效的目標", + "已售出": "已售出", + "调度中心已达最高等级": "調度中心已達最高等級", + "尚未轮到你": "尚未輪到你", + "已完成该操作": "已完成該操作", + "临时整备区不为空": "臨時整備區不為空", + "你已被淘汰": "你已被淘汰", + "服务器内部错误": "伺服器內部錯誤", + "请求超时,请重试": "請求超時,請重試", + "未连接到服务器": "未連線到伺服器", + "连接已断开,请重试": "連線已斷開,請重試", + "该身份已在其他页面登录": "該身分已在其他頁面登入", + "客户端版本与服务器不一致,请刷新页面": "客戶端版本與伺服器不一致,請重新整理頁面", + "队友": "隊友", + "该队友已被淘汰,无法查看": "該隊友已被淘汰,無法查看", + "该队友当前没有战场": "該隊友目前沒有戰場", + "队友与你在同一战场,使用 ‹ › 切换视角": "隊友與你在同一戰場,使用 ‹ › 切換視角", + "无法查看另一组队友的战场": "無法查看另一組隊友的戰場", + "作战中无法查看队友,作战结束后可前往查看": "作戰中無法查看隊友,作戰結束後可前往查看", + "无人在家": "無人在家", + "你自己": "你自己", + "全景": "全景", + "未知错误": "未知錯誤", + "战场": "戰場", + "本局禁用": "本局禁用", + "{skip};本局不限时": "{skip};本局不限時", + "每位博士限时决策": "每位博士限時決策", + ",等待其他博士": ",等待其他博士", + "轮到你决策": "輪到你決策", + "{turnName} 决策中": "{turnName} 決策中", + "等待轮到你": "等待輪到你", + "华法琳": "華法琳", + "选择策略": "選擇策略", + "查看禁用盟约与干员": "查看禁用盟約與幹員", + "策略": "策略", + "决策顺序": "決策順序", + "已选择": "已選擇", + "决策中": "決策中", + "等待中": "等待中", + "队友已选": "隊友已選", + "初始生命值": "初始生命值", + "选择一个策略查看详情": "選擇一個策略查看詳情", + "队友已选,请选择其他策略": "隊友已選,請選擇其他策略", + "{turnName} 正在决策…": "{turnName} 正在決策…", + "跳过本轮,稍后再选": "跳過本輪,稍後再選", + "跳过次数已用完": "跳過次數已用完", + "(已用)": "(已用)", + "确认选择": "確認選擇", + "战场随机(共{poolN}张)": "戰場隨機(共{poolN}張)", + "确认本局信息": "確認本局資訊", + "ENEMY LEADER // 敌方领袖": "ENEMY LEADER // 敵方領袖", + "特训敌人": "特訓敵人", + "未知领袖": "未知領袖", + "本局没有特训敌人": "本局沒有特訓敵人", + "本局模拟已结束": "本局模擬已結束", + "同盟已返回等待室": "同盟已返回等待室", + "返回同盟": "返回同盟", + "暂停中": "暫停中", + "作战已暂停,计时与敌人行动均已停止": "作戰已暫停,計時與敵人行動均已停止", + "放弃模拟": "放棄模擬", + "继续作战": "繼續作戰", + "正在载入模拟数据…": "正在載入模擬資料…", + "正在进入模拟…": "正在進入模擬…", + "当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)": "目前的裝置無法啟用 3D / WebGL 渲染,已切換為簡化檢視(功能不受影響)", + "已配发的装备无法销毁": "已配發的裝備無法銷毀", + "销毁道具": "銷毀道具", + "道具": "道具", + "销毁": "銷毀", + "正在查看": "正在查看", + "返回自己": "返回自己", + "你已被淘汰 · 可继续观战队友": "你已被淘汰 · 可繼續觀戰隊友", + "已调整": "已調整", + "不装备模组": "不裝備模組", + "未知技能": "未知技能", + "默认": "預設", + "已装备": "已裝備", + "初始": "初始", + "消耗": "消耗", + "持续": "持續", + "充能": "充能", + "不装备模组:精锐干员以基础属性、特性与天赋作战。": "不裝備模組:精銳幹員以基礎屬性、特性與天賦作戰。", + "特性": "特性", + "特性追加": "特性追加", + "属性": "屬性", + "无属性加成": "無屬性加成", + "天赋": "天賦", + "局内数值": "局內數值", + "数值版本": "數值版本", + "普通": "普通", + "精锐": "精銳", + "不含技能发动、装备、盟约等局内加成": "不含技能發動、裝備、盟約等局內加成", + "数值含所选模组;": "數值含所選模組;", + "普通干员没有模组,所选模组在「精锐」中生效;": "普通幹員沒有模組,所選模組在「精銳」中生效;", + "没有符合条件的干员": "沒有符合條件的幹員", + "恢复默认": "恢復預設", + "技能": "技能", + "技能等级": "技能等級", + "选择技能": "選擇技能", + "默认配置": "預設配置", + "模组": "模組", + "仅精锐干员装备 · 模组等级": "僅精銳幹員裝備 · 模組等級", + "选择模组": "選擇模組", + "不装备": "不裝備", + "本局的调配已锁定,修改将在下一局生效": "本局的調配已鎖定,修改將在下一局生效", + "阶级": "階級", + "全部": "全部", + "搜索干员 / 职业 / 盟约": "搜尋幹員 / 職業 / 盟約", + "职业": "職業", + "按盟约筛选": "按盟約篩選", + "全部盟约": "全部盟約", + "仅看已调整": "僅看已調整", + "保存中…": "儲存中…", + "同步中…": "同步中…", + "已同步": "已同步", + "本局已锁定 · 下一局生效": "本局已鎖定 · 下一局生效", + "同步失败": "同步失敗", + "全部恢复默认": "全部恢復預設", + "已复制到剪贴板": "已複製到剪貼簿", + "复制失败,请在文本框中手动全选复制": "複製失敗,請在文字方塊中手動全選複製", + "文件过大,请选择「导出」下载的调配文件": "檔案過大,請選擇「匯出」下載的調配檔案", + "读取文件失败": "讀取檔案失敗", + "干员数据仍在载入,请稍候再导入": "幹員資料仍在載入,請稍候再匯入", + "导入失败:这份数据在当前版本没有可用的调配,未做任何改动": "匯入失敗:這份資料在目前版本沒有可用的調配,未做任何改動", + "确认本局信息阶段结束前可调整本局配置": "確認本局資訊階段結束前可調整本局配置", + "开始模拟前可调整干员携带的技能与模组,干员等级不可调整": "開始模擬前可調整幹員攜帶的技能與模組,幹員等級不可調整", + "返回 (Esc)": "返回 (Esc)", + "导出当前调配(可复制或下载)": "匯出目前的調配(可複製或下載)", + "导出": "匯出", + "导入调配(粘贴或选择文件)": "匯入調配(貼上或選擇檔案)", + "导入": "匯入", + "调配截止": "調配截止", + "本局的调配已锁定(确认本局信息后无法修改),修改将在下一局生效": "本局的調配已鎖定(確認本局資訊後無法修改),修改將在下一局生效", + "正在载入干员数据(打开页面后仅载入一次)…": "正在載入幹員資料(開啟頁面後僅載入一次)…", + "干员列表": "幹員列表", + "导出干员调配": "匯出幹員調配", + "导入干员调配": "匯入幹員調配", + "关闭": "關閉", + "复制": "複製", + "下载文件": "下載檔案", + "取消": "取消", + "选择文件": "選擇檔案", + "把导出的内容粘贴到下方,或点「选择文件」。": "把匯出的內容貼到下方,或點「選擇檔案」。", + "覆盖": "覆蓋", + "在此粘贴导出的调配内容…": "在此貼上匯出的調配內容…", + "造成伤害": "造成傷害", + "击倒敌人": "擊倒敵人", + "领袖伤害": "領袖傷害", + "盟约层数": "盟約層數", + "晋升次数": "晉升次數", + "配发装备": "配發裝備", + "消耗资金": "消耗資金", + "完美作战": "完美作戰", + "刷新次数": "更新次數", + "未击倒": "未擊倒", + "损失生命": "損失生命", + "阵容已撤离": "陣容已撤離", + "获得奖杯": "獲得獎杯", + "卫戍认证": "衛戍認證", + "评语": "評語", + "最终攻势起全队共享目标生命值": "最終攻勢起全隊共享目標生命值", + "同盟剩余生命": "同盟剩餘生命", + "剩余生命": "剩餘生命", + "通过回合": "通過回合", + "同盟成员": "同盟成員", + "模拟完成": "模擬完成", + "模拟失败": "模擬失敗", + "成功卫戍 · 敌方领袖已被击败": "成功衛戍 · 敵方領袖已被擊敗", + "防线已被突破": "防線已被突破", + "敌方领袖": "敵方領袖", + "隐秘核心": "隱秘核心", + "返回大厅": "返回大廳", + "暂无结算数据": "暫無結算資料", + "服务器会话已重置,上一局模拟已结束": "伺服器工作階段已重置,上一局模擬已結束", + "服务器会话已重置,已返回大厅": "伺服器工作階段已重置,已返回大廳", + "正在查看 {owner} 的盟约": "正在查看 {owner} 的盟約", + "{owner} 尚未激活盟约": "{owner} 尚未啟動盟約", + "部署干员以激活盟约": "部署幹員以啟動盟約", + "{owner} 的盟约": "{owner} 的盟約", + "我的盟约": "我的盟約", + "(含调和 +{harmony})": "(含調和 +{harmony})", + "层数叠加已禁用": "層數疊加已禁用", + "调和": "調和", + "层数": "層數", + "盟约效果": "盟約效果", + "成员": "成員", + "{owner} 的盟约:{name}": "{owner} 的盟約:{name}", + "盟约:{name}": "盟約:{name}", + "CORE BOND // 核心盟约": "CORE BOND // 核心盟約", + "ADD-ON BOND // 附加盟约": "ADD-ON BOND // 附加盟約", + "在场": "在場", + "(含整备区)": "(含整備區)", + "(含{harmonyName} +{harmony})": "(含{harmonyName} +{harmony})", + "未激活": "未啟動", + "本模式下该盟约不会激活(其干员仍可能因所属的其他盟约出现)": "本模式下該盟約不會啟動(其幹員仍可能因所屬的其他盟約出現)", + "名及以下": "名及以下", + "当前效果": "目前效果", + "({layers} 层)": "({layers} 層)", + "(变形同构体:视为本盟约成员)": "(變形同構體:視為本盟約成員)", + "(本局禁用)": "(本局禁用)", + "(在场)": "(在場)", + "(整备区)": "(整備區)", + "同构": "同構", + "机变": "機變", + "机变阶段": "機變階段", + "CONTINGENCY // 机变阶段": "CONTINGENCY // 機變階段", + "选择一项": "選擇一項", + "倒计时结束后仍未选定将自动分配": "倒計時結束後仍未選定將自動分配", + "选择一项(无时间限制)": "選擇一項(無時間限制)", + " · 点击卡牌选中,再次点击确认": " · 點擊卡牌選中,再次點擊確認", + "已完成选择": "已完成選擇", + "当前轮到你决策": "目前輪到你決策", + "确认选择「{armedName}」(再次点击卡牌亦可)": "確認選擇「{armedName}」(再次點擊卡牌亦可)", + "{name},已选中,再次点击确认": "{name},已選中,再次點擊確認", + "{name},{takerName}已选择": "{name},{takerName}已選擇", + "全队获得": "全隊獲得", + "赏金 {coin}": "賞金 {coin}", + "{name} 已选择": "{name} 已選擇", + "再次点击": "再次點擊", + "选择中": "選擇中", + "队友作战进度": "隊友作戰進度", + "作战结束": "作戰結束", + "作战结束,等待队友完成作战": "作戰結束,等待隊友完成作戰", + "你已被淘汰,正在观战": "你已被淘汰,正在觀戰", + "上一个战场": "上一個戰場", + "下一个战场": "下一個戰場", + "你已被淘汰,可点击队友头像前往查看": "你已被淘汰,可點擊隊友頭像前往查看", + "返回战场": "返回戰場", + "左侧战场": "左側戰場", + "右侧战场": "右側戰場", + "禁用": "禁用", + "无倒计时": "無倒計時", + "剩余{remain}秒": "剩餘{remain}秒", + "确认": "確認", + "加载中": "載入中", + "当前延迟 {ms}ms": "目前延遲 {ms}ms", + "未连接": "未連線", + "正在同步同盟状态…": "正在同步同盟狀態…", + "服务器已更新,本局结束后刷新": "伺服器已更新,本局結束後重新整理", + "刷新页面": "重新整理頁面", + "与服务器的连接已中断,正在重连": "與伺服器的連線已中斷,正在重連", + "立即重连": "立即重連", + "在此页面继续": "在此頁面繼續", + "重新连接": "重新連線", + "重试": "重試", + "第 {attempt} 次 · {secs}s": "第 {attempt} 次 · {secs}s", + "先锋": "先鋒", + "近卫": "近衛", + "重装": "重裝", + "狙击": "狙擊", + "术师": "術師", + "医疗": "醫療", + "辅助": "輔助", + "特种": "特種", + "召唤物": "召喚物", + "自动回复": "自動回復", + "攻击回复": "攻擊回復", + "受击回复": "受擊回復", + "被动": "被動", + "自动触发": "自動觸發", + "精英": "精英", + "领袖": "領袖", + "物理": "物理", + "法术": "法術", + "治疗": "治療", + "真实": "真實", + "无": "無", + "攻击范围": "攻擊範圍", + "全场": "全場", + "当前作战中的实时数值(绿色为增益,红色为减益)": "目前作戰中的即時數值(綠色為增益,紅色為減益)", + "下一场作战开始时的数值:已计入装备、盟约层数、特质、策略与机变效果(不含技能与作战中的临时效果)": "下一場作戰開始時的數值:已計入裝備、盟約層數、特質、策略與機變效果(不含技能與作戰中的臨時效果)", + "实时": "即時", + "开战时": "開戰時", + "搭配以下装备时,携带者视为对应盟约的成员:": "搭配以下裝備時,攜帶者視為對應盟約的成員:", + "变形同构体对应关系": "變形同構體對應關係", + "已搭配": "已搭配", + "生效中": "生效中", + "暂未生效:需与上表中的一件装备一同携带": "暫未生效:需與上表中的一件裝備一同攜帶", + "所属盟约": "所屬盟約", + "(变形同构体)": "(變形同構體)", + ",已激活 {tier} 阶": ",已啟動 {tier} 階", + ",未激活": ",未啟動", + "特质": "特質", + "生命上限": "生命上限", + "攻击": "攻擊", + "防御": "防禦", + "法术抗性": "法術抗性", + "攻击间隔": "攻擊間隔", + "阻挡数": "阻擋數", + "部署费用": "部署費用", + "再部署": "再部署", + "近战位": "近戰位", + "远程位": "遠程位", + "技能 {skSlot}": "技能 {skSlot}", + "干员调配中选择的技能": "幹員調配中選擇的技能", + "已调配": "已調配", + "技力": "技力", + "· 消耗": "· 消耗", + "干员调配中选择的模组": "幹員調配中選擇的模組", + "装备": "裝備", + "拖拽装备至该干员以配发(最多 2 件)": "拖曳裝備至該幹員以配發(最多 2 件)", + "出售": "出售", + "可晋升:{hint}": "可晉升:{hint}", + "效果": "效果", + "进阶": "進階", + "奇术": "奇術", + "将其拖拽至战场上的格子使用": "將其拖曳至戰場上的格子使用", + ";2 件相同装备自动合成进阶装备": ";2 件相同裝備自動合成進階裝備", + "效果获得": "效果獲得", + "晕眩": "暈眩", + "沉默": "沉默", + "沉睡": "沉睡", + "浮空": "浮空", + "移动速度": "移動速度", + "目标价值": "目標價值", + "空中": "空中", + "地面": "地面", + "近战": "近戰", + "能力": "能力", + "说明": "說明", + "作战开始时在摆放的位置部署": "作戰開始時在擺放的位置部署", + "作战开始时在摆放的位置部署一次,之后所属干员每次发动技能时再次出现(未摆放则不会出现)": "作戰開始時在擺放的位置部署一次,之後所屬幹員每次發動技能時再次出現(未擺放則不會出現)", + "所属干员发动技能时才在摆放的位置出现(未摆放则不会出现)": "所屬幹員發動技能時才在擺放的位置出現(未擺放則不會出現)", + "出售精锐干员": "出售精銳幹員", + "详情": "詳情", + "退出全屏": "退出全螢幕", + "全屏": "全螢幕", + "团队增益": "團隊增益", + "生效中的效果": "生效中的效果", + "表情": "表情", + "交流": "交流", + "上一组表情": "上一組表情", + "下一组表情": "下一組表情", + "表情主题": "表情主題", + "悬赏": "懸賞", + "护卫": "護衛", + "暂无敌方情报": "暫無敵方情報", + "本局禁用干员情况": "本局禁用幹員情況", + "我的策略": "我的策略", + "战场:": "戰場:", + "该盟约中被禁用的干员数": "該盟約中被禁用的幹員數", + "本局没有禁用盟约": "本局沒有禁用盟約", + "本局信息": "本局資訊", + "敌方情报": "敵方情報", + "该干员": "該幹員", + "将被销毁": "將被銷毀", + "替换装备": "替換裝備", + "被替换的装备将被销毁": "被替換的裝備將被銷毀", + "新装备": "新裝備", + "选择被替换的装备": "選擇被替換的裝備", + "点击选择要替换的装备": "點擊選擇要替換的裝備", + "上": "上", + "右": "右", + "下": "下", + "左": "左", + "点击取消": "點擊取消", + "拖回中心区域取消": "拖回中心區域取消", + "干员": "幹員", + "整备区": "整備區", + "召唤": "召喚", + "目标生命值": "目標生命值", + "结算时扣除 {p}": "結算時扣除 {p}", + "已淘汰": "已淘汰", + "已离开": "已離開", + "{name}:本局禁用(该盟约不会激活)": "{name}:本局禁用(該盟約不會啟動)", + "行动中": "行動中", + "作战中": "作戰中", + "联防中": "聯防中", + "未装备模组": "未裝備模組", + "协议启动": "協議啟動", + "模拟即将开始": "模擬即將開始", + "第 {r} 回合": "第 {r} 回合", + "资金已到账": "資金已到帳", + "依次选择机变": "依次選擇機變", + "休整期": "休整期", + "部署干员,准备迎敌": "部署幹員,準備迎敵", + "作战开始": "作戰開始", + "各自行动阶段": "各自行動階段", + "联防阶段": "聯防階段", + "完美作战的博士迎战突破防线的敌人": "完美作戰的博士迎戰突破防線的敵人", + "最终攻势": "最終攻勢", + "击败敌方领袖": "擊敗敵方領袖", + "被源石侵蚀的假想敌": "被源石侵蝕的假想敵", + "回合开始": "回合開始", + "回合结算": "回合結算", + "休息一下": "休息一下", + "找不到该单位": "找不到該單位", + "无法放置在该位置": "無法放置在該位置", + "位置未变化": "位置未變化", + "该道具需要放置在战场上使用": "該道具需要放置在戰場上使用", + "交换后的单位无法部署在原位置": "交換後的單位無法部署在原位置", + "请将装备拖拽至干员身上": "請將裝備拖曳至幹員身上", + "近战单位只能部署在地面": "近戰單位只能部署在地面", + "只能部署在召唤者攻击范围内": "只能部署在召喚者攻擊範圍內", + "只能部署在召唤者攻击范围外": "只能部署在召喚者攻擊範圍外", + "只能部署在远程位": "只能部署在遠程位", + "该位置已有单位": "該位置已有單位", + "召唤者尚未部署": "召喚者尚未部署", + "装备只能配发给干员": "裝備只能配發給幹員", + "临时整备区无法放入单位": "臨時整備區無法放入單位", + "之后还有 {queued} 项": "之後還有 {queued} 項", + "晋升奖励": "晉升獎勵", + "免费选择 1 名": "免費選擇 1 名", + "晋升奖励待选择": "晉升獎勵待選擇", + "装备补给": "裝備補給", + "特殊招募": "特殊招募", + "免费选择 1 件": "免費選擇 1 件", + "{title}待选择": "{title}待選擇", + "尚未就绪": "尚未就緒", + "临时整备区不为空,请先处理溢出的资源": "臨時整備區不為空,請先處理溢出的資源", + "当前无法选择": "目前無法選擇", + "已准备就绪,取消准备后才能操作": "已準備就緒,取消準備後才能操作", + "该队友已被淘汰,无法查看其阵地": "該隊友已被淘汰,無法查看其陣地", + "自己": "自己", + "联防(自己)": "聯防(自己)", + "联防阵地": "聯防陣地", + "领袖战场": "領袖戰場", + "基础规则": "基礎規則", + "卫戍协议已运行": "衛戍協議已運行", + "攻防战": "攻防戰", + "休整期 · 区域": "休整期 · 區域", + "休整期 · 资金与调度": "休整期 · 資金與調度", + "限时战斗": "限時戰鬥", + "作战期": "作戰期", + "协同战斗": "協同戰鬥", + "调度手册": "調度手冊", + "干员晋级": "幹員晉級", + "加成情况": "加成情況", + "卫戍能力": "衛戍能力", + "助战及自选编队": "助戰及自選編隊", + "进阶图鉴": "進階圖鑑", + "敌人类型": "敵人類型", + "盟约激活与叠加": "盟約啟動與疊加", + "追加盟约": "追加盟約", + "策略与轮选": "策略與輪選", + "TIPS // 模拟要点": "TIPS // 模擬要點", + "暂无说明内容": "暫無說明內容", + "关闭 (Esc)": "關閉 (Esc)", + "章节": "章節", + "上一页": "上一頁", + "下一页": "下一頁", + "页码": "頁碼", + "联防": "聯防", + "剩余敌人 {v}": "剩餘敵人 {v}", + "剩余不足 {cap} 个后,队友每击倒一个少扣 1 点": "剩餘不足 {cap} 個後,隊友每擊倒一個少扣 1 點", + "队友每击倒一个就少扣 1 点": "隊友每擊倒一個就少扣 1 點", + "目标生命值 {lp}:联防中,你漏过的敌人还剩 {left} 个,{each};按现在结算扣除 {pending} 点(每回合至多 {cap} 点)": "目標生命值 {lp}:聯防中,你漏過的敵人還剩 {left} 個,{each};按現在結算扣除 {pending} 點(每回合最多 {cap} 點)", + "目标生命值 {lp}:联防中,队友正在迎战你漏过的敌人,结算时按联防后剩余的敌人扣除(至多 {pending} 点)": "目標生命值 {lp}:聯防中,隊友正在迎戰你漏過的敵人,結算時按聯防後剩餘的敵人扣除(最多 {pending} 點)", + "放入整备区或战场、配发或使用后才能准备就绪;休整期结束时仍留在临时整备区的单位将被销毁": "放入整備區或戰場、配發或使用後才能準備就緒;休整期結束時仍留在臨時整備區的單位將被銷毀", + "取消准备": "取消準備", + "本局信息(策略 / 禁用盟约 / 干员)": "本局資訊(策略 / 禁用盟約 / 幹員)", + "查看即将迎击的敌方单位": "查看即將迎擊的敵方單位", + "休整期可以查看即将迎击的敌方单位": "休整期可以查看即將迎擊的敵方單位", + "已流失": "已流失", + "暂停": "暫停", + "继续作战({key})": "繼續作戰({key})", + "暂停作战({key})": "暫停作戰({key})", + "离开 / 暂离": "離開 / 暫離", + "部署费用(再部署消耗)": "部署費用(再部署消耗)", + "攻击力": "攻擊力", + "防御力": "防禦力", + "攻击速度": "攻擊速度", + "再部署时间": "再部署時間", + "秒": "秒", + "每秒回复": "每秒回復", + "内容过长,无法导入": "內容過長,無法匯入", + "没有可导入的内容": "沒有可匯入的內容", + "无法识别的内容": "無法識別的內容", + "无法识别的格式": "無法識別的格式", + "这不是干员调配的数据": "這不是幹員調配的資料", + "里面没有有效的调配条目": "裡面沒有有效的調配條目", + "弹药": "彈藥", + "本局的干员调配已锁定,修改将在下一局生效": "本局的幹員調配已鎖定,修改將在下一局生效", + "独立模拟的休整期没有时间限制,你可以随时继续。": "獨立模擬的休整期沒有時間限制,你可以隨時繼續。", + "放弃模拟将立即结束本局并返回大厅,本局进度不会保留,也不会进行结算。": "放棄模擬將立即結束本局並返回大廳,本局進度不會保留,也不會進行結算。", + "离开模拟": "離開模擬", + "暂离(AI 托管)": "暫離(AI 託管)", + "暂离": "暫離", + "AI 托管中": "AI 託管中", + "AUTOPILOT // AI 托管": "AUTOPILOT // AI 託管", + "AI 正在代为操作你的席位": "AI 正在代為操作你的席位", + "返回模拟": "返回模擬", + "该盟约中无法出现的干员数": "該盟約中無法出現的幹員數", + ",或本模式禁用": ",或本模式禁用", + "本局禁用干员": "本局禁用幹員", + "本局没有禁用干员": "本局沒有禁用幹員", + "核心盟约": "核心盟約", + "附加盟约": "附加盟約", + "折扣价(原价 {basePrice})": "折扣價(原價 {basePrice})", + "加价(原价 {basePrice})": "加價(原價 {basePrice})", + "价格": "價格", + "精锐干员将出现在作战区原位置": "精銳幹員將出現在作戰區原位置", + "精锐干员将进入整备区": "精銳幹員將進入整備區", + "无法购买": "無法購買", + "确认购买": "確認購買", + ",无法购买": ",無法購買", + ",再次点击确认": ",再次點擊確認", + "已拥有 {copies}/{need}": "已擁有 {copies}/{need}", + "可晋升": "可晉升", + "(已调配)": "(已調配)", + " · 模组:{name}": " · 模組:{name}", + " · 未装备模组": " · 未裝備模組", + "已购买": "已購買", + "已招募": "已招募", + "再次点击确认升级({price} 资金)": "再次點擊確認升級({price} 資金)", + "升级调度中心({price} 资金) · {key}": "升級調度中心({price} 資金) · {key}", + "已满级": "已滿級", + "确认升级": "確認升級", + "稍后选择(回合结束后消失)": "稍後選擇(回合結束後消失)", + "稍后": "稍後", + "展开商店": "展開商店", + "调度中心": "調度中心", + "剩余可放置角色:": "剩餘可放置角色:", + "目前资金": "目前資金", + "收起": "收起", + "解冻商店 · {key}": "解凍商店 · {key}", + "冻结商店(下回合保留) · {key}": "凍結商店(下回合保留) · {key}", + "解冻": "解凍", + "刷新商店 · {key}": "更新商店 · {key}", + "免费 ×{free}": "免費 ×{free}", + "查看 {name} 的战场": "查看 {name} 的戰場", + "与你在同一战场": "與你在同一戰場", + "查看自己的阵地": "查看自己的陣地", + "查看 {name} 的阵地": "查看 {name} 的陣地", + "连接已断开": "連線已斷開", + "前往查看": "前往查看", + "单位": "單位", + "撤退": "撤退", + "撤退至整备区": "撤退至整備區", + "出售,获得 {sell} 资金": "出售,獲得 {sell} 資金", + "已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)": "已準備就緒後進入的單位保留到下個休整期,屆時仍在此處的將被銷毀(取消準備則在本休整期結束時銷毀)", + "放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁": "放入整備區或戰場、配發或使用後才能準備;休整期結束時仍在此處的將被銷毀", + "结算": "結算", + "模拟结束": "模擬結束", + "表情套组:卫戍协议": "表情套組:衛戍協議", + "开心": "開心", + "害怕": "害怕", + "对不起": "對不起", + "谢谢": "謝謝", + "思考": "思考", + "合作愉快": "合作愉快", + "表情套组:虫动": "表情套組:蟲動", + "合作愉快!": "合作愉快!", + "谢谢!": "謝謝!", + "对不起!": "對不起!", + "再见!": "再見!", + "很快就好!": "很快就好!", + "没问题!": "沒問題!", + "敬礼!": "敬禮!", + "欢呼!": "歡呼!", + "酷!": "酷!", + "伤心": "傷心", + "快死了": "快死了", + "表情套组:博士士": "表情套組:博士士", + "博士士 1": "博士士 1", + "博士士 2": "博士士 2", + "博士士 3": "博士士 3", + "博士士 4": "博士士 4", + "博士士 5": "博士士 5", + "博士士 6": "博士士 6", + "表情套组:米米子": "表情套組:米米子", + "米米子 1": "米米子 1", + "米米子 2": "米米子 2", + "米米子 3": "米米子 3", + "米米子 4": "米米子 4", + "米米子 5": "米米子 5", + "米米子 6": "米米子 6", + "表情套组:维维美": "表情套組:維維美", + "维维美 1": "維維美 1", + "维维美 2": "維維美 2", + "维维美 3": "維維美 3", + "维维美 4": "維維美 4", + "维维美 5": "維維美 5", + "维维美 6": "維維美 6", + "精打细算玩偶": "精打細算玩偶", + "悬赏决策": "懸賞決策", + "道具补给": "道具補給", + "机密商店": "機密商店", + "战术决策": "戰術決策", + "隐秘核心已解锁": "隱秘核心已解鎖", + "{name}博士中途退出了模拟": "{name}博士中途退出了模擬", + "你的目标生命值耗尽,已被淘汰": "你的目標生命值耗盡,已被淘汰", + "{name}博士的目标生命值已耗尽": "{name}博士的目標生命值已耗盡", + "整备区已满,获得的干员已返还": "整備區已滿,獲得的幹員已返還", + "整备区已满,晋升的精锐干员无法放入": "整備區已滿,晉升的精銳幹員無法放入", + "整备区已满,获得的装备已销毁": "整備區已滿,獲得的裝備已銷毀", + "整备区已满,合成的装备已销毁": "整備區已滿,合成的裝備已銷毀", + "发生意外错误:{error}": "發生意外錯誤:{error}", + "战场#{no}": "戰場#{no}", + "与至多 {n} 名博士组成同盟,共享干员池,联防协作抵御敌潮。": "與最多 {n} 名博士組成同盟,共享幹員池,聯防協作抵禦敵潮。", + "1–{n} 名博士 · 可由 AI 队友补位": "1–{n} 名博士 · 可由 AI 隊友補位", + "没有找到密钥 {k} 对应的同盟:请和房主核对密钥(同盟结束后密钥即失效)": "沒有找到金鑰 {k} 對應的同盟:請和房主核對金鑰(同盟結束後金鑰即失效)", + "你已经是该同盟的博士:先离开同盟,才能以观战身份进入": "你已經是該同盟的博士:先離開同盟,才能以觀戰身分進入", + "以观战者身份进入:不占博士席位,只能观看(每个同盟最多 {MAX_SPECTATORS} 名,模拟进行中也可进入)": "以觀戰者身分進入:不佔博士席位,只能觀看(每個同盟最多 {MAX_SPECTATORS} 名,模擬進行中也可進入)", + "填入密钥(不会直接加入)": "填入金鑰(不會直接加入)", + "将该博士移出同盟": "將該博士移出同盟", + "移出该博士": "移出該博士", + "观战席": "觀戰席", + "移出观战者 {name}": "移出觀戰者 {name}", + "移出该观战者": "移出該觀戰者", + "入座": "入座", + "移出同盟": "移出同盟", + "确定将「{name}」移出同盟吗?对方可以凭同盟密钥重新加入。": "確定將「{name}」移出同盟嗎?對方可以憑同盟金鑰重新加入。", + "移出": "移出", + "观战中 · 不占博士席位,模拟开始后可切换观看各位博士": "觀戰中 · 不佔博士席位,模擬開始後可切換觀看各位博士", + "共": "共", + "观战中": "觀戰中", + "toggle::开启": "開啟", + "toggle::关闭": "關閉", + "撤退选中干员": "撤退選中幹員", + "出售选中干员": "出售選中幹員", + "取消 / 关闭": "取消 / 關閉", + "语言": "語言", + "右键查看详情": "右鍵查看詳情", + "刷新商店": "更新商店", + "冻结 / 解冻商店": "凍結 / 解凍商店", + "升级调度中心": "升級調度中心", + "准备就绪 / 暂停(独立模拟)": "準備就緒 / 暫停(獨立模擬)", + "不可更改": "不可更改", + "按下新按键…": "按下新按鍵…", + "更改「{action}」的快捷键(当前:{key})": "更改「{action}」的快捷鍵(目前:{key})", + "请按下「{action}」的新按键(Esc 取消)": "請按下「{action}」的新按鍵(Esc 取消)", + "「{action}」已改为 {key}": "「{action}」已改為 {key}", + "「{action}」已改为 {key};「{other}」原来用 {key},已换成 {old}": "「{action}」已改為 {key};「{other}」原來用 {key},已換成 {old}", + "{key} 不能设为快捷键:Esc、Tab、Enter、方向键和功能键留给界面使用": "{key} 不能設為快捷鍵:Esc、Tab、Enter、方向鍵和功能鍵留給介面使用", + "快捷键只能是单个按键,不能搭配 Ctrl、Alt 或 ⌘": "快捷鍵只能是單個按鍵,不能搭配 Ctrl、Alt 或 ⌘", + "已取消,「{action}」仍是 {key}": "已取消,「{action}」仍是 {key}", + "已恢复默认快捷键": "已恢復預設快捷鍵", + "快捷键需要实体键盘;触屏设备连接键盘后可用": "快捷鍵需要實體鍵盤;觸控裝置連接鍵盤後可用", + "查看 / 更改快捷键": "查看 / 更改快捷鍵", + "观战中无法进行该操作": "觀戰中無法進行該操作", + "标准": "標準", + "险境": "險境", + "绝境": "絕境", + "终极": "終極", + "rounds::回合": "回合", + "{who}:获得{name}": "{who}:獲得{name}", + "获得{name}": "獲得{name}", + "联防阶段:{names} 迎战突破防线的敌人": "聯防階段:{names} 迎戰突破防線的敵人", + "地形变化:{names}无法停留在原位置,已撤回整备区": "地形變化:{names}無法停留在原位置,已撤回整備區", + "{names}只能部署在召唤者攻击范围内,已退回整备区": "{names}只能部署在召喚者攻擊範圍內,已退回整備區", + "{names}只能部署在召唤者攻击范围内,整备区已满,下回合返还": "{names}只能部署在召喚者攻擊範圍內,整備區已滿,下回合返還", + "{names}只能部署在召唤者攻击范围外,已退回整备区": "{names}只能部署在召喚者攻擊範圍外,已退回整備區", + "{names}只能部署在召唤者攻击范围外,整备区已满,下回合返还": "{names}只能部署在召喚者攻擊範圍外,整備區已滿,下回合返還", + "【{label}】层数达成,下回合开始时获得{gain}资金": "【{label}】層數達成,下回合開始時獲得{gain}資金", + "【{label}】层数达成,获得{gain}资金": "【{label}】層數達成,獲得{gain}資金", + "【奇迹】下次刷新不消耗资金": "【奇蹟】下次更新不消耗資金", + "【维多利亚】获得{n}件维式重锤": "【維多利亞】獲得{n}件維式重鎚", + "【远见】所有干员购买价格永久降低": "【遠見】所有幹員購買價格永久降低", + "【远见】远见干员购买价格永久降低": "【遠見】遠見幹員購買價格永久降低", + "原型": "原型", + "已持有": "已持有", + "{tier}阶 自选": "{tier}階 自選", + "{slot}:选择干员": "{slot}:選擇幹員", + "选择干员": "選擇幹員", + "调度中心等级 {level} 起出现在商店": "調度中心等級 {level} 起出現在商店", + "这项自选在当前版本不可用,开局时会被移除": "這項自選在目前版本不可用,開局時會被移除", + "(锁定)": "(鎖定)", + "(精锐时生效)": "(精銳時生效)", + "更换": "更換", + "清空": "清空", + "已在{tier}阶自选中": "已在{tier}階自選中", + "锁定": "鎖定", + "6★ 原型干员与持有的 6★ 干员,另有 4★ 原型干员(先锋、特种除外)": "6★ 原型幹員與持有的 6★ 幹員,另有 4★ 原型幹員(先鋒、特種除外)", + "6★ 原型干员与持有的 6★ 干员": "6★ 原型幹員與持有的 6★ 幹員", + "筛选": "篩選", + "可选干员": "可選幹員", + "连接服务器后才能选择自选干员": "連線至伺服器後才能選擇自選幹員", + "原型干员的技能与补位时一致,不可更改": "原型幹員的技能與補位時一致,不可更改", + "精锐时生效 · 模组等级 {stage}": "精銳時生效 · 模組等級 {stage}", + "从左侧选择一名干员": "從左側選擇一名幹員", + "5阶与6阶各有两个自选名额:选择持有的 6★ 干员(任选技能与模组)或原型干员(技能锁定)。自选干员只出现在你自己的商店里,调度中心达到该阶等级后才会刷新,价格与同阶干员相同;盟约按干员实际所属阵营分配,没有特质。": "5階與6階各有兩個自選名額:選擇持有的 6★ 幹員(任選技能與模組)或原型幹員(技能鎖定)。自選幹員只出現在你自己的商店裡,調度中心達到該階等級後才會更新,價格與同階幹員相同;盟約按幹員實際所屬陣營分配,沒有特質。", + "{tier}阶": "{tier}階", + "阶": "階", + "自选编队:{name}(只在你的商店出现)": "自選編隊:{name}(只在你的商店出現)", + "自选": "自選", + "{names}的盟约本局全部禁用,不会出现在你的商店": "{names}的盟約本局全部禁用,不會出現在你的商店", + "自选编队:所选技能与模组,没有特质,盟约按所属阵营分配": "自選編隊:所選技能與模組,沒有特質,盟約按所屬陣營分配", + "自选编队:{name}": "自選編隊:{name}", + "替补": "替補", + "{name}的替补": "{name}的替補", + "未持有{name}:由替补干员 {standIn} 上场(盟约、特质、阶级与价格不变)": "未持有{name}:由替補幹員 {standIn} 上場(盟約、特質、階級與價格不變)", + "未持有的干员由官方指定的替补干员上场:商店、整备区、作战区与结算都显示替补干员(模型、技能、天赋、攻击范围),盟约、特质、阶级、价格与合成仍按原干员。预设干员无论是否持有都由本人上场,不在此列。": "未持有的幹員由官方指定的替補幹員上場:商店、整備區、作戰區與結算都顯示替補幹員(模型、技能、天賦、攻擊範圍),盟約、特質、階級、價格與合成仍按原幹員。預設幹員無論是否持有都由本人上場,不在此列。", + "活性源石": "活性源石", + "特殊地形": "特殊地形", + "攻击力 +{atk}%": "攻擊力 +{atk}%", + "攻击速度 +{aspd}": "攻擊速度 +{aspd}", + "部署于其上的我方单位、经过的敌方单位,每秒受到 {dmg} 点真实伤害(无来源)": "部署於其上的我方單位、經過的敵方單位,每秒受到 {dmg} 點真實傷害(無來源)", + "在其上的我方单位与经过的敌方单位持续受到伤害": "在其上的我方單位與經過的敵方單位持續受到傷害", + "同时获得:{mods}": "同時獲得:{mods}", + "效果持续 {sec} 秒;离开地块后仍然保留,再次接触会重新计时": "效果持續 {sec} 秒;離開地塊後仍然保留,再次接觸會重新計時", + "沼泽": "沼澤", + "留在沼泽里的单位每 {sec} 秒获得 1 层「陷入沼泽」:攻击速度 {aspd},敌方单位还有移动速度 {move}": "留在沼澤裡的單位每 {sec} 秒獲得 1 層「陷入沼澤」:攻擊速度 {aspd},敵方單位還有移動速度 {move}", + "留在沼泽里的单位每 {sec} 秒获得 1 层「陷入沼泽」:攻击速度 {aspd}": "留在沼澤裡的單位每 {sec} 秒獲得 1 層「陷入沼澤」:攻擊速度 {aspd}", + "重量 ≥ {heavy} 的敌人一次获得 2 层": "重量 ≥ {heavy} 的敵人一次獲得 2 層", + "最多 {max} 层;离开沼泽后解除": "最多 {max} 層;離開沼澤後解除", + "排气格栅": "排氣格柵", + "站在排气格栅上的干员不会被敌方的远程攻击选中(效果相当于隐匿)": "站在排氣格柵上的幹員不會被敵方的遠程攻擊選中(效果相當於隱匿)", + "但挡住敌人的干员仍会被它攻击到": "但擋住敵人的幹員仍會被它攻擊到", + "深水区": "深水區", + "敌人每秒受到 {dmg} 点伤害": "敵人每秒受到 {dmg} 點傷害", + "攻击速度 {aspd}、移动速度 ×{move}": "攻擊速度 {aspd}、移動速度 ×{move}", + "攻击速度 {aspd}": "攻擊速度 {aspd}", + "移动速度 ×{move}": "移動速度 ×{move}", + "溺水伤害属于无来源伤害(不吃干员的增伤、穿透与装备加成),也不归类为环境伤害": "溺水傷害屬於無來源傷害(不吃幹員的增傷、穿透與裝備加成),也不歸類為環境傷害", + "拒绝部署(特制水上平台可以让这一格变得可部署)": "拒絕部署(特製水上平台可以讓這一格變得可部署)", + "红门": "紅門", + "敌方入口": "敵方入口", + "敌方单位从这里出场": "敵方單位從這裡出場", + "蓝门": "藍門", + "保护目标": "保護目標", + "敌人走进这里会扣你的目标生命值(LP),一回合至多 10 点": "敵人走進這裡會扣你的目標生命值(LP),一回合最多 10 點", + "传送入口": "傳送入口", + "敌人走到这里会从场上消失": "敵人走到這裡會從場上消失", + "传送出口": "傳送出口", + "消失的敌人会从这里重新出现": "消失的敵人會從這裡重新出現", + "可部署": "可部署", + "仅近战位可部署": "僅近戰位可部署", + "仅远程位可部署": "僅遠程位可部署", + "不可部署": "不可部署", + "高台": "高台", + "只有空中单位能通过": "只有空中單位能通過", + "地面单位可通过": "地面單位可通過", + "地形机制": "地形機制", + "这一格": "這一格", + "全部清空": "全部清空", + "清空全部 {n} 个自选名额?": "清空全部 {n} 個自選名額?", + "文件过大,请选择「导出」下载的自选编队文件": "檔案過大,請選擇「匯出」下載的自選編隊檔案", + "导入失败:{error}": "匯入失敗:{error}", + "已导入 {n} 个自选名额": "已匯入 {n} 個自選名額", + "(另有 {n} 项不可用,未导入)": "(另有 {n} 項不可用,未匯入)", + "已导入:全部名额为空": "已匯入:全部名額為空", + "自选编队是局外设置:本局按开局时的设置进行,修改将在下一局生效": "自選編隊是局外設定:本局按開局時的設定進行,修改將在下一局生效", + "局外设置,下一局生效 · 联机时只进入你自己的商店": "局外設定,下一局生效 · 連線時只進入你自己的商店", + "干员调配 / 干员持有 / 自选编队": "幹員調配 / 幹員持有 / 自選編隊", + "5阶、6阶的自选名额:持有的 6★ 干员或原型干员": "5階、6階的自選名額:持有的 6★ 幹員或原型幹員", + "自选编队": "自選編隊", + "{n} 个自选名额已选择": "{n} 個自選名額已選擇", + "已选": "已選", + "导出自选编队(可复制或下载)": "匯出自選編隊(可複製或下載)", + "导入自选编队(粘贴或选择文件)": "匯入自選編隊(貼上或選擇檔案)", + "共 {n} 个自选名额已选择。复制或下载这份数据,即可在别的设备或浏览器上导入。": "共 {n} 個自選名額已選擇。複製或下載這份資料,即可在別的裝置或瀏覽器上匯入。", + "把导出的自选编队数据粘贴到下方,或点「选择文件」。导入会覆盖当前的自选编队。": "把匯出的自選編隊資料貼到下方,或點「選擇檔案」。匯入會覆蓋目前的自選編隊。", + "在此粘贴导出的自选编队内容…": "在此貼上匯出的自選編隊內容…", + "导出自选编队": "匯出自選編隊", + "导入自选编队": "匯入自選編隊", + "自选编队是局外设置,修改将在下一局生效": "自選編隊是局外設定,修改將在下一局生效", + "下一局生效": "下一局生效", + "这份数据来自更新的版本(v{v}),请先更新游戏": "這份資料來自更新的版本(v{v}),請先更新遊戲", + "这不是自选编队的数据": "這不是自選編隊的資料", + "里面没有自选编队的数据": "裡面沒有自選編隊的資料", + "本局禁用{names}盟约,此策略效果可能无法发挥": "本局禁用{names}盟約,此策略效果可能無法發揮", + "【{name}】": "【{name}】", + "每位博士有 {n} 秒": "每位博士有 {n} 秒", + "{skip};{clock}": "{skip};{clock}", + "{skip};{clock},超时将自动选择当前选中的「{autoName}」": "{skip};{clock},超時將自動選擇目前選中的「{autoName}」", + "{skip};{clock},超时将自动选择「{autoName}」": "{skip};{clock},超時將自動選擇「{autoName}」", + "已选择「{name}」": "已選擇「{name}」", + "跳过": "跳過", + "第 {r} 回合 · 最终攻势": "第 {r} 回合 · 最終攻勢", + "道具无法出售。确定要销毁「{name}」吗?": "道具無法出售。確定要銷毀「{name}」嗎?", + "展开盟约": "展開盟約", + "收起盟约": "收起盟約", + "正在查看 {name} 的阵地(只读)": "正在查看 {name} 的陣地(只讀)", + "观战中 · 点击左侧成员头像切换查看": "觀戰中 · 點擊左側成員頭像切換查看", + "未持有(干员持有):由替补干员上场": "未持有(幹員持有):由替補幹員上場", + "未持有": "未持有", + "{name} 调配": "{name} 調配", + "干员持有中标记为未持有:此棋子由替补干员 {name} 上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效": "幹員持有中標記為未持有:此棋子由替補幹員 {name} 上場,技能與模組固定(補位幹員技能不可更改);這裡的調配在改回「持有」後生效", + "干员持有中标记为未持有:此棋子由替补干员上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效": "幹員持有中標記為未持有:此棋子由替補幹員上場,技能與模組固定(補位幹員技能不可更改);這裡的調配在改回「持有」後生效", + "全部持有": "全部持有", + "将 {nNotOwned} 名未持有的干员恢复为持有(由本人上场)?": "將 {nNotOwned} 名未持有的幹員恢復為持有(由本人上場)?", + "将 {nChanged} 名干员的技能与模组恢复为默认配置?": "將 {nChanged} 名幹員的技能與模組恢復為預設配置?", + "文件过大,请选择「导出」下载的干员持有文件": "檔案過大,請選擇「匯出」下載的幹員持有檔案", + "已导入:{applied} 名干员未持有(另有 {dropped} 项无效,未导入)": "已匯入:{applied} 名幹員未持有(另有 {dropped} 項無效,未匯入)", + "已导入:{applied} 名干员未持有": "已匯入:{applied} 名幹員未持有", + "已导入:全部持有": "已匯入:全部持有", + "已导入 {applied} 名干员(另有 {dropped} 项未导入)": "已匯入 {applied} 名幹員(另有 {dropped} 項未匯入)", + "已导入 {applied} 名干员的调配": "已匯入 {applied} 名幹員的調配", + "干员持有是局外设置:本局按开局时的设置进行,修改将在下一局生效": "幹員持有是局外設定:本局按開局時的設定進行,修改將在下一局生效", + "局外设置,下一局生效 · 联机时只影响你自己的棋子 · 默认全部持有": "局外設定,下一局生效 · 連線時只影響你自己的棋子 · 預設全部持有", + "标记未持有的干员:由官方指定的替补干员上场": "標記未持有的幹員:由官方指定的替補幹員上場", + "干员持有": "幹員持有", + "{nNotOwned} 名未持有": "{nNotOwned} 名未持有", + "导出干员持有(可复制或下载)": "匯出幹員持有(可複製或下載)", + "导入干员持有(粘贴或选择文件)": "匯入幹員持有(貼上或選擇檔案)", + "导出干员持有": "匯出幹員持有", + "导入干员持有": "匯入幹員持有", + "共 {n} 名干员未持有。复制或下载这份数据,即可在别的设备或浏览器上导入。": "共 {n} 名幹員未持有。複製或下載這份資料,即可在別的裝置或瀏覽器上匯入。", + "把导出的干员持有数据粘贴到下方,或点「选择文件」。导入会{overwrite}当前的干员持有设置。": "把匯出的幹員持有資料貼到下方,或點「選擇檔案」。匯入會{overwrite}目前的幹員持有設定。", + "共 {n} 名干员已调整。复制或下载这份数据,即可在别的设备或浏览器上导入。": "共 {n} 名幹員已調整。複製或下載這份資料,即可在別的裝置或瀏覽器上匯入。", + "导入会{overwrite}当前的 {nChanged} 名干员调配。": "匯入會{overwrite}目前的 {nChanged} 名幹員調配。", + "在此粘贴导出的干员持有内容…": "在此貼上匯出的幹員持有內容…", + "调整干员携带的技能与模组 · 干员持有": "調整幹員攜帶的技能與模組 · 幹員持有", + "{n} 名干员已调整": "{n} 名幹員已調整", + "{off} 名干员未持有,由替补干员上场": "{off} 名幹員未持有,由替補幹員上場", + "{off} 名干员未持有(由替补干员上场)": "{off} 名幹員未持有(由替補幹員上場)", + "{name}:持有": "{name}:持有", + "{name}:未持有,由 {standIn} 上场": "{name}:未持有,由 {standIn} 上場", + "替补干员": "替補幹員", + "点击标记为未持有:由 {standIn} 上场": "點擊標記為未持有:由 {standIn} 上場", + "点击恢复为持有": "點擊恢復為持有", + "无替补数据": "無替補資料", + "持有": "持有", + "仅看未持有": "僅看未持有", + "全部持有:没有由替补干员上场的棋子": "全部持有:沒有由替補幹員上場的棋子", + "没有可下掉的干员": "沒有可下掉的幹員", + "{name}({note})": "{name}({note})", + "本局耗时 {n} 分钟": "本局耗時 {n} 分鐘", + "{tip} · {n} 名成员": "{tip} · {n} 名成員", + "{names} 在场:核心盟约激活人数 +{harmony}": "{names} 在場:核心盟約啟動人數 +{harmony}", + "{harmonyName}已激活:核心盟约激活人数 +{harmony}": "{harmonyName}已啟動:核心盟約啟動人數 +{harmony}", + "层数不显示": "層數不顯示", + "已激活 · {tier} 阶": "已啟動 · {tier} 階", + "已激活": "已啟動", + "bond-threshold::名": "名", + "({note})": "({note})", + "观战中,可点击左侧成员头像前往查看": "觀戰中,可點擊左側成員頭像前往查看", + "基础 {v}": "基礎 {v}", + "{items} → 【{name}】": "{items} → 【{name}】", + "list::、": "、", + "与{morph}一同装备时,携带者视为【{bond}】成员": "與{morph}一同裝備時,攜帶者視為【{bond}】成員", + ":在场 {count}/{cap}": ":在場 {count}/{cap}", + ":在场 {count}": ":在場 {count}", + "拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)": "拖曳至幹員身上進行配發(每名幹員最多 2 件,配發後無法取下)", + "调度中心不出售 · 获取途径:{source}": "調度中心不出售 · 獲得途徑:{source}", + "免疫:{list}": "免疫:{list}", + "确定要出售精锐干员「{name}」吗?出售后获得 {price} 资金。": "確定要出售精銳幹員「{name}」嗎?出售後獲得 {price} 資金。", + "之后的每场作战": "之後的每場作戰", + "还剩 {left} 场作战": "還剩 {left} 場作戰", + "第 {r} 回合 · 即将迎击 {n} 名敌人": "第 {r} 回合 · 即將迎擊 {n} 名敵人", + "特训敌人·": "特訓敵人·", + "{bandOwner} 的策略": "{bandOwner} 的策略", + "确认替换": "確認替換", + "「{name}」的装备栏已满(2/2)。选择一件装备进行替换,{warn}。": "「{name}」的裝備欄已滿(2/2)。選擇一件裝備進行替換,{warn}。", + "将销毁「{old}」并配发「{item}」": "將銷毀「{old}」並配發「{item}」", + "{name} 漏过的敌人还剩 {n} 个(联防中)": "{name} 漏過的敵人還剩 {n} 個(聯防中)", + "你漏过的敌人还剩 {n} 个(队友正在迎战)": "你漏過的敵人還剩 {n} 個(隊友正在迎戰)", + "{name} 漏过的敌人已全部被击倒": "{name} 漏過的敵人已全部被擊倒", + "你漏过的敌人已全部被击倒": "你漏過的敵人已全部被擊倒", + "目标生命值 {lp}:本回合已有 {cap} 个以上敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)": "目標生命值 {lp}:本回合已有 {cap} 個以上敵人進入藍門,結算時扣除 {pending} 點(每回合最多 {cap} 點)", + "目标生命值 {lp}:本回合已有 {pending} 个敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)": "目標生命值 {lp}:本回合已有 {pending} 個敵人進入藍門,結算時扣除 {pending} 點(每回合最多 {cap} 點)", + "临时整备区还有 {count} 个单位:{rule}": "臨時整備區還有 {count} 個單位:{rule}", + "临时整备区 {n} 个单位待处理": "臨時整備區 {n} 個單位待處理", + "{n} 秒后全队生命值开始流失": "{n} 秒後全隊生命值開始流失", + "超时 · 生命值 {n}/秒": "超時 · 生命值 {n}/秒", + "这份调配来自更新的版本(v{v}),请先更新游戏": "這份調配來自更新的版本(v{v}),請先更新遊戲", + "{n}秒": "{n}秒", + "干员持有是局外设置,修改将在下一局生效": "幹員持有是局外設定,修改將在下一局生效", + "离开观战": "離開觀戰", + "离开观战席并返回大厅,本局模拟不受影响;观战席空着时可以凭同盟密钥再次观战。": "離開觀戰席並返回大廳,本局模擬不受影響;觀戰席空著時可以憑同盟金鑰再次觀戰。", + "{away}:由 AI 托管你的席位(自动部署、准备与选择),随时可以返回。": "{away}:由 AI 託管你的席位(自動部署、準備與選擇),隨時可以返回。", + "{quit}:离开后无法返回本局,你的干员将回到共享卡池。": "{quit}:離開後無法返回本局,你的幹員將回到共享卡池。", + "灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)": "灰色:部分盟約所含幹員陣容不完整(仍可透過其他盟約的幹員或裝備啟動)", + "这不是干员持有的数据": "這不是幹員持有的資料", + "里面没有干员持有的数据": "裡面沒有幹員持有的資料", + "{name},价格 {price}": "{name},價格 {price}", + "技能 {slot}:{name}": "技能 {slot}:{name}", + "技能:{name}": "技能:{name}", + "目标生命值 {lp},联防中:漏过的敌人还剩 {left} 个,按现在结算扣除 {pending} 点(每回合至多 {cap} 点)": "目標生命值 {lp},聯防中:漏過的敵人還剩 {left} 個,按現在結算扣除 {pending} 點(每回合最多 {cap} 點)", + "目标生命值 {lp},联防中,结算时扣除至多 {pending} 点": "目標生命值 {lp},聯防中,結算時扣除最多 {pending} 點", + "目标生命值 {lp},结算时扣除 {pending} 点": "目標生命值 {lp},結算時扣除 {pending} 點", + "{name} 操作": "{name} 操作", + "撤退至整备区({key})": "撤退至整備區({key})", + "撤退[{key}]": "撤退[{key}]", + "出售(+{sell} 资金,{key})": "出售(+{sell} 資金,{key})", + "出售[{key}]": "出售[{key}]", + "临时整备区 {n} 件道具待处理": "臨時整備區 {n} 件道具待處理", + "{name}:部分盟约所含干员阵容不完整({bannedN} 名干员无法出现)": "{name}:部分盟約所含幹員陣容不完整({bannedN} 名幹員無法出現)", + "{name}:部分盟约所含干员阵容不完整": "{name}:部分盟約所含幹員陣容不完整", + "联防:{names}": "聯防:{names}", + "剩余资金": "剩餘資金", + "继续整备": "繼續整備", + "还有 {funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?": "還有 {funds} 資金未使用。休整期結束時,本回合的剩餘資金將清零。確定準備就緒嗎?", + "选择「{name}」的朝向": "選擇「{name}」的朝向", + "选择朝向": "選擇朝向", + "朝向:{dir}": "朝向:{dir}", + "开发版 · 不稳定,请勿用于公开服务器": "開發版 · 不穩定,請勿用於公開伺服器" } From 6613c3d0fb68281444a55eb3c3dd3e1f65579fa1 Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 11:38:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fb5=20UI-TW:=20five=20msgids=20the=20integr?= =?UTF-8?q?ation=20branch=20gained=20after=20this=20base,=20converted=20th?= =?UTF-8?q?e=20same=20way=20and=20appended=20(=E5=AF=BC=E5=85=A5=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=EF=BC=9A=E6=B8=B8=E6=88=8F=E6=95=B0=E6=8D=AE=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E8=BD=BD=E5=85=A5=E2=80=A6,=20=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=B2=A1=E6=9C=89=E8=BD=BD=E5=85=A5,=20{file?= =?UTF-8?q?s}=20=E6=B2=A1=E6=9C=89=E4=B8=8B=E8=BD=BD=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E2=80=A6,=20=E5=85=A8=E5=91=98=E6=97=A0=E4=BC=A4=EF=BC=81,=20?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=80=BC=E5=87=8F=E5=B0=91=20=E2=88=92{n});?= =?UTF-8?q?=20=E5=B9=BF=E5=91=8A=E6=8B=A6=E6=88=AA=E6=8F=92=E4=BB=B6=20?= =?UTF-8?q?=E2=86=92=20=E5=BB=A3=E5=91=8A=E6=94=94=E6=88=AA=E6=93=B4?= =?UTF-8?q?=E5=85=85=E5=8A=9F=E8=83=BD=20(twp's=20=E5=A4=96=E6=8E=9B=20rea?= =?UTF-8?q?ds=20as=20"cheat"=20in=20a=20game)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/i18n/zh-TW.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/i18n/zh-TW.json b/public/i18n/zh-TW.json index 867bc748..5690059c 100644 --- a/public/i18n/zh-TW.json +++ b/public/i18n/zh-TW.json @@ -1052,5 +1052,10 @@ "选择「{name}」的朝向": "選擇「{name}」的朝向", "选择朝向": "選擇朝向", "朝向:{dir}": "朝向:{dir}", - "开发版 · 不稳定,请勿用于公开服务器": "開發版 · 不穩定,請勿用於公開伺服器" + "开发版 · 不稳定,请勿用于公开服务器": "開發版 · 不穩定,請勿用於公開伺服器", + "导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络": "匯入失敗:遊戲資料沒有載入,未做任何改動。請重新整理頁面;仍不行時,請檢查廣告攔截擴充功能和網路", + "游戏数据没有载入": "遊戲資料沒有載入", + "{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。": "{files} 沒有下載成功,這一頁無法顯示;已儲存的設定不受影響。請重新整理頁面;仍不行時,請檢查廣告攔截擴充功能和網路。", + "全员无伤!": "全員無傷!", + "生命值减少 −{n}": "生命值減少 −{n}" } From c11ba03da4ca6b5143953900789886aa81bf11db Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 11:47:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fb5=20UI-TW:=20the=20machine-translation=20?= =?UTF-8?q?mark=20=E2=80=94=20`=5Fmeta.machineTranslated:=20true`,=20a=20n?= =?UTF-8?q?ew=20optional=20manifest=20field=20of=20a=20language=20pack=20(?= =?UTF-8?q?docs/PACKS.md):=20shared/i18nPacks.js=20langFields=20reads=20it?= =?UTF-8?q?=20(true=20or=20false),=20shared/packs.js=20normalizeManifest?= =?UTF-8?q?=20warns=20about=20a=20value=20that=20is=20not=20a=20boolean=20?= =?UTF-8?q?(read=20as=20false;=20the=20pack=20still=20loads)=20and=20packI?= =?UTF-8?q?ndexEntry=20passes=20it=20into=20the=20index=20entry=20like=20`?= =?UTF-8?q?complete`=20(only=20when=20true),=20so=20the=20client's=20regis?= =?UTF-8?q?try=20has=20it=20from=20the=20index=20(langMetaOf=20=E2=86=92?= =?UTF-8?q?=20registerLangs=20=E2=86=92=20langInfo)=20or=20from=20the=20pa?= =?UTF-8?q?ck's=20own=20`=5Fmeta`=20without=20one;=20=E8=AE=BE=E7=BD=AE=20?= =?UTF-8?q?shows=20a=20note=20under=20the=20language=20switch=20while=20th?= =?UTF-8?q?e=20CURRENT=20language's=20pack=20has=20it=20(ui/lang.js=20mach?= =?UTF-8?q?ineTranslationNote,=20msgid=20=E3=80=8C=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E7=9A=84=E7=95=8C=E9=9D=A2=E6=96=87=E5=AD=97?= =?UTF-8?q?=E4=B8=BA=E6=9C=BA=E5=99=A8=E7=BF=BB=E8=AF=91=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E4=B8=8D=E5=A4=9F=E5=87=86=E7=A1=AE=EF=BC=8C=E6=AC=A2?= =?UTF-8?q?=E8=BF=8E=E5=9C=A8=20GitHub=20=E4=B8=8A=E6=8C=87=E6=AD=A3?= =?UTF-8?q?=E3=80=82=E3=80=8D=20=E2=80=94=20English=20in=20en.json,=20?= =?UTF-8?q?=E7=9B=AE=E5=89=8D=E8=AA=9E=E8=A8=80=E7=9A=84=E4=BB=8B=E9=9D=A2?= =?UTF-8?q?=E6=96=87=E5=AD=97=E7=82=BA=E6=A9=9F=E5=99=A8=E7=BF=BB=E8=AD=AF?= =?UTF-8?q?=E2=80=A6=20in=20zh-TW.json;=20ja=20/=20ko=20leave=20it=20to=20?= =?UTF-8?q?the=20integrator);=20tests:=20test/i18n-packs.test.js=20(the=20?= =?UTF-8?q?field=20from=20the=20manifest=20through=20the=20index=20to=20la?= =?UTF-8?q?ngInfo,=20the=20warning,=20the=20shipped=20zh-TW=20manifest,=20?= =?UTF-8?q?the=20note=20shown=20/=20hidden=20by=20language),=20test/ui/i18?= =?UTF-8?q?n.e2e.test.js=20(the=20shipped=20packs'=20own=20UI=20strings=20?= =?UTF-8?q?read=20from=20their=20files=20=E2=80=94=20zh-TW=20now=20?= =?UTF-8?q?=E9=96=8B=E5=A7=8B=20=E2=80=94=20and=20the=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=20note,=20none=20in=20Chinese);=20docs/I18N.md=20says=20how=20?= =?UTF-8?q?the=20zh-TW=20pack=20is=20made=20(OpenCC=20twp,=20the=20officia?= =?UTF-8?q?l=20TW=20terms,=20a=20manual=20pass;=20a=20later=20msgid=20the?= =?UTF-8?q?=20same=20way)=20and=20that=20the=20ja=20/=20ko=20strings=20are?= =?UTF-8?q?=20machine=20translations=20open=20to=20corrections=20(edit=20p?= =?UTF-8?q?ublic/i18n/.json,=20node=20tools/i18n.mjs=20check=20=20--strict)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/I18N.md | 40 ++++++++++++++------- docs/PACKS.md | 9 +++-- public/i18n/en.json | 1 + public/i18n/zh-TW.json | 1 + public/js/ui/lang.js | 15 +++++++- public/js/ui/settings.js | 7 ++-- shared/i18n.js | 8 +++-- shared/i18nPacks.js | 10 ++++-- shared/packs.js | 12 +++++-- test/i18n-packs.test.js | 75 ++++++++++++++++++++++++++++++++++++---- test/ui/i18n.e2e.test.js | 33 ++++++++++++++---- 11 files changed, 173 insertions(+), 38 deletions(-) diff --git a/docs/I18N.md b/docs/I18N.md index 0d772eb0..187d4815 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -4,9 +4,9 @@ Chinese is the default; every other language is a switch (the owner's decision o opens in Chinese until the player picks a language on the title screen (top right) or in 设置 / Settings. Every language but Chinese is a **language pack** — a file in the language folder, nothing else (the owner's decision of 2026-10-07; §5 "Adding a language"): the menu lists every pack the server finds, each under its own name (buttons up to -four languages, a list beyond). English is a pack like any other; it ships with the game and is the complete one. -Japanese, Korean and Traditional Chinese ship too, with the official game texts and no UI strings yet (§5 "Shipped -packs"). The choice is kept in `localStorage` (`sp.pref.lang`); `?lang=` in the address bar (`?lang=en`, `?lang=zh`) sets it +four languages, a list beyond). English is a pack like any other; it ships with the game, complete. Japanese, Korean +and Traditional Chinese ship too, with the official game texts and complete UI strings made by machine, open to +corrections (§5 "Shipped packs"). The choice is kept in `localStorage` (`sp.pref.lang`); `?lang=` in the address bar (`?lang=en`, `?lang=zh`) sets it once and is then removed; a stored language whose pack is gone opens in Chinese. A switch re-renders the page in place (no reload). Language packs are the first type of the content packs (docs/PACKS.md: the manifest, the registry, the index, the pack folders). @@ -240,13 +240,24 @@ which are bilingual on purpose. ## 5. Adding a language -**Shipped packs** (0.2.0; the owner's decision of 2026-10-07): English (complete) and three packs that carry the -official game texts but no UI strings yet — 日本語 (`ja`: the JP client, 96.5 % of the game texts), 한국어 (`ko`: KR, -97.4 %) and 繁體中文 (`zh-TW`: TW, 91.1 %; its data is older, and a Traditional text equal to the Simplified one counts as -untranslated). Their UI falls back to English (`ja`, `ko`: `"fallback": ["en"]`) or, for `zh-TW`, to the Simplified -Chinese msgids (no fallback — the owner's choice); `zh-TW` counts large numbers in 萬 / 億. **UI translations are -welcome from the community**: start from the shipped file — `node tools/i18n.mjs template ` keeps its `_meta` and -adds every msgid with an empty value — then follow the steps below and send a pull request. +**Shipped packs** (0.2.0; the owner's decisions of 2026-10-07): English and three packs with the official game texts +and complete UI strings — 日本語 (`ja`: the JP client, 96.5 % of the game texts), 한국어 (`ko`: KR, 97.4 %) and 繁體中文 +(`zh-TW`: TW, 91.1 %; its data is older, and a Traditional text equal to the Simplified one counts as untranslated). +The three are made by machine and marked so (`"machineTranslated": true`: 设置 shows a note under the language switch +while one is in use); a string a pack lacks falls back to English (`ja`, `ko`: `"fallback": ["en"]`) or, for `zh-TW`, to +the Simplified Chinese msgid (no fallback — the owner's choice); `zh-TW` counts large numbers in 萬 / 億. + +- **繁體中文** is a conversion, not a free translation: each msgid converted from the Simplified Chinese with OpenCC + (`cn` → `twp`, Traditional characters with Taiwan phrases), the official TW client's word kept where the phrase step + words a game concept differently (調度中心, not 排程中心; 更新 for the shop refresh; 啟動 for a bond; 遠程位; 查看; 目前 …; + a whole string the TW tables have, such as 時間較短的模擬訓練, as it is there), then a manual pass over the result + (one-to-many characters such as 只 / 隻, 重新整理 for a page refresh …). A msgid added later is converted the same way. +- **日本語 / 한국어** are machine translations that use the official JP / KR game terms. **Corrections are welcome**, to + any pack: edit the string in `public/i18n/.json`, run `node tools/i18n.mjs check --strict` (0 errors, + 0 missing) and send a pull request on GitHub. + +A new language is welcome too: `node tools/i18n.mjs template ` starts its pack (run on an existing pack, it keeps +the translations and `_meta` and adds every missing msgid with an empty value); then follow the steps below. A language is a file in the language folder; the server lists it on the next page load (no restart, no build step, no code change). For a community translator: @@ -257,7 +268,8 @@ code change). For a community translator: and every msgid (about 1 000) with an empty value, in the order of the English pack (`--fill en` fills in the English to overwrite). Fill in `_meta`: `name` (the language in itself, shown in the menu), `englishName`, `authors`, `version`; keep `"fallback": ["en"]` to show English where a string is still empty (drop it to show Chinese); set - `numberUnits` if the language counts large numbers in 10⁴ steps like Chinese (`["万", "億"]`). + `numberUnits` if the language counts large numbers in 10⁴ steps like Chinese (`["万", "億"]`), and + `"machineTranslated": true` when the strings come from a machine (players then see a note in 设置; docs/PACKS.md). 3. **Translate** the values (empty = not yet: it falls back). Keep every `{placeholder}` of the msgid; plural forms as in §1; `public/i18n/en.json` is the reference for meaning and context (`ctx::msgid` keys are the same Chinese text in another context). Use the official terms of your language's client where it has one. @@ -273,8 +285,10 @@ code change). For a community translator: again: it keeps the translations and adds the new msgids. The tests: `test/i18n-packs.test.js` (discovery, the fallback chain, placeholders, plurals in English and Russian, the -template and check commands, and that the Chinese and English output of `t()` is what it was before packs: every msgid -of `public/i18n/en.json` rendered with sample params, compared with the renderer of 0.2.0 before packs). +template and check commands, the `machineTranslated` mark — manifest, index, the client's registry, the 设置 note — and +that the Chinese and English output of `t()` is what it was before packs: every msgid of `public/i18n/en.json` rendered +with sample params, compared with the renderer of 0.2.0 before packs); `test/ui/i18n.e2e.test.js` (`SP_E2E=1`: the +shipped packs in the browser, their UI strings and the 设置 note). ## Credits diff --git a/docs/PACKS.md b/docs/PACKS.md index f6d29462..7f6b48ec 100644 --- a/docs/PACKS.md +++ b/docs/PACKS.md @@ -63,7 +63,8 @@ would see one folder). | `name`, `englishName` | the name in its own language (a language pack: the language's own name, shown in the menu) and in English (the menu's tooltip) | | `authors`, `credits`, `license` | who made it; free text (`license`: an SPDX id or text) | | `files` | a folder pack: role → path inside the folder. Each type lists its roles and their extensions (`lang`: `ui` required, `data` optional, both `.json`) | -| the type's fields | `lang`: `lang`, `base`, `fallback`, `complete`, `numberUnits` (docs/I18N.md) | +| the type's fields | `lang`: `lang`, `base`, `fallback`, `complete`, `machineTranslated`, `numberUnits` (docs/I18N.md) | +| `machineTranslated` | `lang`, optional: `true` when the pack's UI strings come from a machine (a translation, or a conversion such as the Traditional Chinese pack's). The index entry carries it (only when true, like `complete`) and the client shows a note under the language switch in 设置 while that language is in use (`public/js/ui/lang.js` `machineTranslationNote`). A value other than `true` / `false` is a warning and reads as false | A **problem** keeps a pack from loading (unknown type, no `lang`, a file missing or outside the folder, broken JSON, an id or a language already taken — the language folders' files come first, then the folders by id); a **warning** does @@ -158,7 +159,9 @@ that ship. `test/package.test.js` checks both layouts and that a local pack stay ## Tests `test/i18n-packs.test.js` (discovery of both layouts, broken / planned / duplicate packs, a pack dropped in or removed -while the server runs, the file allowlist, the manifest and app ranges, the fallback chain, placeholders, plurals, +while the server runs, the file allowlist, the manifest and app ranges, the `machineTranslated` mark from the manifest +to the 设置 note, the fallback chain, placeholders, plurals, the per-text game-text fallback, the client loader and menu, the tools, build-i18n `--lang` / `--dict`), `test/package.test.js`, `test/ui/i18n.e2e.test.js` -(`SP_E2E=1`: a pack file and a pack folder dropped in while the server runs show in the menu and switch). +(`SP_E2E=1`: a pack file and a pack folder dropped in while the server runs show in the menu and switch; the shipped +packs with their UI strings and the 设置 note of a machine-translated one). diff --git a/public/i18n/en.json b/public/i18n/en.json index 487f74e7..3e2c6c1d 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -784,6 +784,7 @@ "出售选中干员": "Sell selected Operator", "取消 / 关闭": "Cancel / Close", "语言": "Language", + "当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。": "The interface text in this language is machine-translated and may be inaccurate. Corrections are welcome on GitHub.", "右键查看详情": "Right-click for details", "刷新商店": "Refresh Shop", "冻结 / 解冻商店": "Freeze / Unfreeze Shop", diff --git a/public/i18n/zh-TW.json b/public/i18n/zh-TW.json index 5690059c..24023b66 100644 --- a/public/i18n/zh-TW.json +++ b/public/i18n/zh-TW.json @@ -788,6 +788,7 @@ "出售选中干员": "出售選中幹員", "取消 / 关闭": "取消 / 關閉", "语言": "語言", + "当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。": "目前語言的介面文字為機器翻譯,可能不夠準確,歡迎在 GitHub 上指正。", "右键查看详情": "右鍵查看詳情", "刷新商店": "更新商店", "冻结 / 解冻商店": "凍結 / 解凍商店", diff --git a/public/js/ui/lang.js b/public/js/ui/lang.js index ede78361..5f95bfbf 100644 --- a/public/js/ui/lang.js +++ b/public/js/ui/lang.js @@ -15,7 +15,7 @@ import { useEffect, useState } from '../../vendor/hooks.module.js'; import { - DEFAULT_LANG, normalizeLang, getLang, setLang, onLangChange, addMessages, setNameResolver, tName, format, translateWire, t, + DEFAULT_LANG, normalizeLang, getLang, setLang, onLangChange, addMessages, setNameResolver, tName, format, translateWire, t, N_, registerLangs, getLangs, langInfo, langChain, onLangsChange, setI18nWarn, } from '../../../shared/i18n.js'; import { canonicalLang, computeChain, scriptOf } from '../../../shared/i18nPacks.js'; @@ -270,6 +270,19 @@ export function langMenuModel(langs, current) { return { kind: items.length > SEGMENTED_MAX ? 'select' : 'buttons', items }; } +/** The note itself (a msgid: each pack words it in its own language). */ +const MACHINE_TRANSLATION_NOTE = N_('当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。'); + +/** + * The note under the language switch in 设置 (ui/settings.js): while the current language's pack is marked as a machine + * translation (`_meta.machineTranslated`, docs/PACKS.md) it says so, in that language; null otherwise (Chinese, English, + * a pack translated by hand). Only the current pack counts, not its base or fallbacks. + * @returns {string | null} + */ +export function machineTranslationNote() { + return langInfo(getLang())?.machineTranslated === true ? t(MACHINE_TRANSLATION_NOTE) : null; +} + /** * The language menu: 中文 | English | … as buttons, or a list when there are many packs. * @param {{ class?: string }} props diff --git a/public/js/ui/settings.js b/public/js/ui/settings.js index bae6117c..d7db406d 100644 --- a/public/js/ui/settings.js +++ b/public/js/ui/settings.js @@ -1,6 +1,7 @@ // Player settings (BGM/SFX/voice volume, mute, damage numbers, render quality, the shortcut keys): a tiny observable // store persisted in localStorage (`sp.pref.settings`), applied to the audio manager on every change, plus -// the settings modal — which also holds the language switch (ui/lang.js; kept apart in `sp.pref.lang`) and the 快捷键 +// the settings modal — which also holds the language switch (ui/lang.js; kept apart in `sp.pref.lang`; under it a note +// while the current language's pack is a machine translation, `_meta.machineTranslated`) and the 快捷键 // section that rebinds the in-match shortcuts (the key map: ui/gameLogic/shortcuts.js; the community request // 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07). @@ -11,7 +12,7 @@ import { sanitizeSettings, HOTKEY_ACTIONS, DEFAULT_HOTKEYS, hotkeyLabel, rebindH import { audio } from '../audio.js'; import { openGuide } from './guide.js'; import { detectFeatures } from './device.js'; -import { LangToggle } from './lang.js'; +import { LangToggle, machineTranslationNote } from './lang.js'; import { t, tc, N_ } from '../../../shared/i18n.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); @@ -151,6 +152,7 @@ export function SettingsModal({ open, onClose }) { const s = useSettings(); const [tested, setTested] = useState(false); const [touchUi] = useState(() => detectFeatures().coarse && !detectFeatures().fine); + const mtNote = machineTranslationNote(); // a pack marked as machine translation says so under the switch return html`<${Modal} open=${open} onClose=${onClose} title=${t('设置')} micro="SETTINGS" width="7.4rem" actions=${html`<${Button} variant="secondary" icon="book" class="set-guide" onClick=${() => openGuide(0)}>${t('玩法说明')} <${Button} variant="primary" icon="check" onClick=${onClose}>${t('完成')}`}> @@ -159,6 +161,7 @@ export function SettingsModal({ open, onClose }) { ${t('语言')}<${MicroLabel}>LANGUAGE <${LangToggle} class="set-lang" /> + ${mtNote ? html`

${mtNote}

` : null} <${Slider} label=${t('背景音乐')} micro="BGM" icon="play" value=${s.bgm} onInput=${(v) => updateSettings({ bgm: v })} /> <${Slider} label=${t('干员语音')} micro="VOICE" icon="mic" value=${s.voice} onInput=${(v) => updateSettings({ voice: v })} /> <${Slider} label=${t('音效')} micro="SFX" icon="signal" value=${s.sfx} diff --git a/shared/i18n.js b/shared/i18n.js index e521abb9..499614b8 100644 --- a/shared/i18n.js +++ b/shared/i18n.js @@ -43,7 +43,7 @@ export const DEFAULT_LANG = SOURCE_LANG; /** The source language: built in, no pack, no catalog. */ const SOURCE_META = Object.freeze({ code: SOURCE_LANG, name: '中文', englishName: 'Chinese (Simplified)', authors: [], credits: '', version: '', app: '', // i18n-ignore: its own name - base: null, fallback: [], complete: true, numberUnits: ['万', '亿'], builtin: true, // i18n-ignore: the Chinese number units + base: null, fallback: [], complete: true, machineTranslated: false, numberUnits: ['万', '亿'], builtin: true, // i18n-ignore: the Chinese number units }); /** @type {Map} code → metadata: the source, the packs of the index, languages given messages */ @@ -109,7 +109,11 @@ export function getLangs() { return [registry.get(SOURCE_LANG), ...rest]; } -/** The metadata of a known language, or null. @param {string} [lang] */ +/** + * The metadata of a known language (its names, chain fields, `complete`, `machineTranslated` …: what registerLangs or its + * pack's `_meta` gave), or null. + * @param {string} [lang] + */ export const langInfo = (lang = current) => registry.get(String(lang)) || null; /** diff --git a/shared/i18nPacks.js b/shared/i18nPacks.js index 17ac0723..9fb8bf2f 100644 --- a/shared/i18nPacks.js +++ b/shared/i18nPacks.js @@ -20,6 +20,8 @@ // pack for it — pt-BR → pt; never the Chinese source, so zh-TW has none); null switches it off // fallback packs tried after the base, before the Chinese msgid (["en"]: untranslated strings show English) // complete true: `node tools/i18n.mjs check` requires every msgid (English declares it) +// machineTranslated true: the UI strings are machine translations (or a machine conversion, zh-TW); the 设置 dialog +// says so under the language switch while the language is in use (ui/lang.js machineTranslationNote) // numberUnits two units for numbers counted in 10⁴ / 10⁸ steps (Chinese 万 / 亿; e.g. a Japanese pack ["万", "億"]); // without them large numbers use K / M / B (ui/gameLogic/format.js fmtNum) // The common fields (id, version, app, name, englishName, authors, credits, license) are shared/packs.js's. @@ -84,7 +86,8 @@ export function langList(v) { } /** - * @typedef {{ lang: string, base: string|null|undefined, fallback: string[], complete: boolean, numberUnits: string[]|null }} LangFields + * @typedef {{ lang: string, base: string|null|undefined, fallback: string[], complete: boolean, machineTranslated: boolean, + * numberUnits: string[]|null }} LangFields */ /** @@ -101,7 +104,10 @@ export function langFields(code, m) { else if (typeof o.base === 'string') base = canonicalLang(o.base); if (base === SOURCE_LANG || base === code) base = null; const units = Array.isArray(o.numberUnits) && o.numberUnits.length === 2 && o.numberUnits.every((u) => str(u, 8)) ? o.numberUnits.map((u) => str(u, 8)) : null; - return { lang: code, base, fallback: langList(o.fallback).filter((c) => c !== code), complete: o.complete === true, numberUnits: units }; + return { + lang: code, base, fallback: langList(o.fallback).filter((c) => c !== code), complete: o.complete === true, + machineTranslated: o.machineTranslated === true, numberUnits: units, + }; } /** diff --git a/shared/packs.js b/shared/packs.js index ab9046ba..129ac85f 100644 --- a/shared/packs.js +++ b/shared/packs.js @@ -23,13 +23,14 @@ // englishName its English name // authors a string or a list; credits free text; license an SPDX id or free text // files a folder pack: role → path inside the folder (type "lang": { "ui": "ui.json", "data": "data.json" }) -// … the type's own fields (type "lang": lang, base, fallback, complete, numberUnits — shared/i18nPacks.js) +// … the type's own fields (type "lang": lang, base, fallback, complete, machineTranslated, numberUnits — +// shared/i18nPacks.js; a machineTranslated that is not a boolean is warned about and reads as false) // // Index (GET /packs/index.json — the server's live list; `node tools/packs.mjs index` writes packs/index.json for a // static host): { version: 1, app: "", packs: [entry …] } — the packs of supported types, // sorted by type then id. An entry: id, type, name, englishName, version?, app?, compatible, authors?, credits?, // license?, files (role → URL on this server) and the type's fields; a language entry: lang, base?, fallback, -// complete?, numberUnits?, strings (how many translations its UI file holds). +// complete?, machineTranslated?, numberUnits?, strings (how many translations its UI file holds). import { APP_VERSION } from './constants.js'; import { SOURCE_LANG, canonicalLang, isLangCode, languageName, langFields } from './i18nPacks.js'; @@ -134,7 +135,8 @@ export function appVersionMatches(range, version = APP_VERSION) { /** * @typedef {{ id: string, type: string, name: string, englishName: string, version: string, app: string, * compatible: boolean, authors: string[], credits: string, license: string, files: Record, - * lang?: string, base?: string|null, fallback?: string[], complete?: boolean, numberUnits?: string[]|null }} PackManifest + * lang?: string, base?: string|null, fallback?: string[], complete?: boolean, machineTranslated?: boolean, + * numberUnits?: string[]|null }} PackManifest */ /** @@ -178,6 +180,9 @@ export function normalizeManifest(raw, ctx) { else { if (m.lang !== undefined && !isLangCode(m.lang) && !ctx.lang) warnings.push(`"lang": "${m.lang}" read as ${code}`); Object.assign(out, langFields(code, m)); + // anything but true reads as false: say so when another value was meant + const mt = m.machineTranslated; + if (mt !== undefined && typeof mt !== 'boolean') warnings.push(`"machineTranslated": ${String(JSON.stringify(mt)).slice(0, 40)} is not true or false (read as false)`); out.name ||= languageName(code); out.englishName ||= languageName(code, 'en'); } @@ -220,6 +225,7 @@ export function packIndexEntry(manifest, extra) { if (manifest.base !== undefined) e.base = manifest.base; e.fallback = manifest.fallback || []; if (manifest.complete) e.complete = true; + if (manifest.machineTranslated) e.machineTranslated = true; if (manifest.numberUnits) e.numberUnits = manifest.numberUnits; e.strings = extra.strings ?? 0; } diff --git a/test/i18n-packs.test.js b/test/i18n-packs.test.js index 5b23715f..7ea69335 100644 --- a/test/i18n-packs.test.js +++ b/test/i18n-packs.test.js @@ -3,9 +3,10 @@ // allowlist), the manifest (shared/packs.js: types, app ranges), the fallback chain (shared/i18n.js: base, fallback, the // Chinese msgid), placeholder validation, plural categories (Intl.PluralRules: English unchanged, Russian named forms), // the per-text fallback of game texts (shared/i18nData.js onto, data.js chains), the client loader and menu -// (ui/lang.js), the translator tools (tools/i18n.mjs template / check, tools/packs.mjs index), and that the Chinese and -// English output of t() is what it was before packs. The example packs live only here, in temporary folders ('qaa' … -// 'qtz' are the ISO 639 codes reserved for local use). +// (ui/lang.js), the translator tools (tools/i18n.mjs template / check, tools/packs.mjs index), the machine-translation +// mark (`_meta.machineTranslated`: the manifest, the index, the client's registry, the 设置 note), and that the Chinese +// and English output of t() is what it was before packs. The example packs live only here, in temporary folders ('qaa' +// … 'qtz' are the ISO 639 codes reserved for local use). import { test, afterEach } from 'node:test'; import assert from 'node:assert/strict'; @@ -19,8 +20,8 @@ import { t, tc, tParts, setLang, getLang, addMessages, setMessages, setI18nWarn, registerLangs, getLangs, langChain, langInfo, normalizeLang, checkTranslation, parsePluralForms, pluralCategory, format, } from '../shared/i18n.js'; -import { canonicalLang, computeChain, scriptOf, langFields } from '../shared/i18nPacks.js'; -import { normalizeManifest, appVersionMatches, isVersionRange, readPackIndex, langMetaOf, PACK_TYPES, isPackPath } from '../shared/packs.js'; +import { canonicalLang, computeChain, scriptOf, langFields, packMeta } from '../shared/i18nPacks.js'; +import { normalizeManifest, appVersionMatches, isVersionRange, readPackIndex, langMetaOf, PACK_TYPES, isPackPath, packIndexEntry } from '../shared/packs.js'; import { buildRecordOverlay, applyFileOverlay, applyRecordOverlay } from '../shared/i18nData.js'; import { buildOverlay, LANG_SOURCES } from '../tools/build-i18n.mjs'; import { scanPacks, createPackRegistry, packIndexOf } from '../server/packs.js'; @@ -30,6 +31,9 @@ import { writePackIndex } from '../tools/packs.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const EN = JSON.parse(fs.readFileSync(path.join(ROOT, 'public/i18n/en.json'), 'utf8')); +const ZH_TW = JSON.parse(fs.readFileSync(path.join(ROOT, 'public/i18n/zh-TW.json'), 'utf8')); +/** The 设置 note of a pack marked as machine translation (ui/lang.js machineTranslationNote). */ +const MT_NOTE = '当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。'; afterEach(() => { setLang('zh'); @@ -172,6 +176,44 @@ test('manifest: common fields, type fields, app ranges; English is a pack like a assert.ok(en.manifest.authors.some((a) => a.includes('@YuriRestia')), 'PR #70 credited'); }); +test('manifest: machineTranslated — a boolean, carried into the index entry like complete and into the client\'s registry (from the index or the pack\'s own _meta); the shipped zh-TW pack has it', () => { + const ctx = { id: 'qam', type: 'lang', lang: 'qam', app: '0.2.0' }; + const on = normalizeManifest({ name: 'Qam', machineTranslated: true }, ctx); + assert.deepEqual([on.problems, on.warnings, on.manifest.machineTranslated], [[], [], true]); + assert.equal(normalizeManifest({ name: 'Qam' }, ctx).manifest.machineTranslated, false, 'optional: absent reads as false'); + const odd = normalizeManifest({ name: 'Qam', machineTranslated: 'yes' }, ctx); + assert.deepEqual(odd.problems, [], 'a warning, not a problem: the pack still loads'); + assert.deepEqual(odd.warnings, ['"machineTranslated": "yes" is not true or false (read as false)']); + assert.equal(odd.manifest.machineTranslated, false); + assert.match(normalizeManifest({ machineTranslated: 1 }, ctx).warnings.join(), /"machineTranslated": 1 is not true or false/); + // the index entry: only when true (like complete) + const files = { ui: '/i18n/qam.json' }; + assert.equal(packIndexEntry(on.manifest, { files, strings: 1 }).machineTranslated, true); + assert.equal(Object.hasOwn(packIndexEntry(odd.manifest, { files }), 'machineTranslated'), false); + // a checkout's packs → the index → readPackIndex / langMetaOf → the client's registry (langInfo) + const root = tmpdir('mt'); + try { + put(root, 'public/i18n/qam.json', { _meta: { name: 'Qam', fallback: ['en'], machineTranslated: true }, 开始: 'Qam-Start' }); + put(root, 'public/i18n/qan.json', { _meta: { name: 'Qan' }, 开始: 'Qan-Start' }); + const index = packIndexOf(scanPacks(dirsOf(root), { app: '0.2.0' }).packs, { app: '0.2.0' }); + const entries = readPackIndex(index, 'lang'); + assert.deepEqual(entries.map((e) => [e.id, e.machineTranslated]), [['qam', true], ['qan', undefined]]); + registerLangs(entries.map(langMetaOf)); + assert.deepEqual([langInfo('qam').machineTranslated, langInfo('qan').machineTranslated, langInfo('zh').machineTranslated], [true, false, false]); + } finally { fs.rmSync(root, { recursive: true, force: true }); } + // without an index the pack's own `_meta` registers it (addMessages → packMeta) + assert.equal(packMeta('qao', { _meta: { machineTranslated: true } }).machineTranslated, true); + addMessages('qao', { _meta: { name: 'Qao', machineTranslated: true }, 开始: 'Qao-Start' }); + assert.equal(langInfo('qao').machineTranslated, true); + // the shipped Traditional Chinese pack: a machine conversion (OpenCC, the official TW terms, a manual pass), complete, + // no fallback (the Simplified Chinese msgids), the note in Traditional Chinese + const tw = normalizeManifest(ZH_TW._meta, { id: 'zh-TW', type: 'lang', lang: 'zh-TW', app: '0.2.0' }); + assert.deepEqual([tw.problems, tw.warnings], [[], []]); + assert.deepEqual([tw.manifest.machineTranslated, tw.manifest.complete, tw.manifest.fallback, tw.manifest.numberUnits], [true, true, [], ['萬', '億']]); + assert.equal(ZH_TW[MT_NOTE], '目前語言的介面文字為機器翻譯,可能不夠準確,歡迎在 GitHub 上指正。'); + assert.ok(EN[MT_NOTE], 'English has the note'); +}); + test('fallback chain: the pack, its base (pt-BR → pt; zh-TW none), its fallbacks, then the Chinese msgid; cycles end', () => { const metas = { 'pt-BR': { fallback: ['en'] }, pt: { fallback: ['es'] }, es: {}, en: {}, 'zh-TW': { fallback: [] }, a: { fallback: ['b'] }, b: { fallback: ['a'] } }; const known = (c) => Object.hasOwn(metas, c); @@ -346,10 +388,31 @@ test('client: the index registers the language packs; the menu lists them (butto assert.deepEqual(lang.initialLang('', () => 'qzz', { tentative: true }), { lang: 'qzz', fromUrl: false }, 'without an index: tried by its code'); assert.deepEqual([scriptOf('Stronghold Protocol'), scriptOf('卫戍协议'), scriptOf('堅守協定'), scriptOf('위수 협의'), scriptOf('Протокол')], ['alphabetic', 'cjk', 'cjk', 'cjk', 'alphabetic']); assert.deepEqual(langMetaOf(readPackIndex(index, 'lang')[1]).numberUnits, ['W', 'Y']); - assert.deepEqual(langFields('qab', { base: 'zh', fallback: 'en' }), { lang: 'qab', base: null, fallback: ['en'], complete: false, numberUnits: null }); + assert.deepEqual(langFields('qab', { base: 'zh', fallback: 'en' }), { lang: 'qab', base: null, fallback: ['en'], complete: false, machineTranslated: false, numberUnits: null }); assert.equal(canonicalLang('zh-hant-tw'), 'zh-Hant-TW'); }); +test('设置: the machine-translation note shows while the current language\'s pack is marked (ui/lang.js machineTranslationNote), in that language; none for Chinese, English or an unmarked pack', async () => { + const { machineTranslationNote } = await import('../public/js/ui/lang.js'); + registerLangs([{ code: 'qap', name: 'Qap', fallback: ['en'], machineTranslated: true }, { code: 'qaq', name: 'Qaq', fallback: ['en'] }, { code: 'en', name: 'English' }]); + addMessages('en', { [MT_NOTE]: EN[MT_NOTE] }); + addMessages('qap', { [MT_NOTE]: 'Qap-Hinweis' }); + assert.equal(machineTranslationNote(), null, 'Chinese: no note'); + setLang('qap'); + assert.equal(machineTranslationNote(), 'Qap-Hinweis', 'the marked pack, in its own words'); + setMessages('qap', {}); + assert.equal(machineTranslationNote(), EN[MT_NOTE], 'a marked pack without the string: the next language of its chain'); + setLang('qaq'); + assert.equal(machineTranslationNote(), null, 'a pack that is not marked'); + setLang('en'); + assert.equal(machineTranslationNote(), null, 'English is not marked'); + // the shipped zh-TW pack (no index here: its `_meta` registers it) + addMessages('zh-TW', ZH_TW); + setLang('zh-TW'); + assert.equal(machineTranslationNote(), ZH_TW[MT_NOTE]); + assert.equal(t('设置'), '設定', 'and the rest of the interface in Traditional Chinese'); +}); + test('tools: template writes a skeleton (or adds the missing msgids to a pack), check reports coverage and errors per pack; packs index writes the static index', () => { const used = new Map([['开始', { where: 'a.js:1', params: [] }], ['还剩 {n} 秒', { where: 'b.js:2', params: new Set(['n']) }], ['{n} 名', { where: 'c.js:3', params: new Set(['n', 'count']) }], ['表里的', { where: 'd.js:4', params: null }]]); const skel = packTemplate('qaa', { msgids: [...used.keys()] }); diff --git a/test/ui/i18n.e2e.test.js b/test/ui/i18n.e2e.test.js index 298586f0..dfd76425 100644 --- a/test/ui/i18n.e2e.test.js +++ b/test/ui/i18n.e2e.test.js @@ -7,8 +7,9 @@ // game-data overlay (data/i18n/en.json) is applied; `?lang=zh` switches back and leaves the address bar. Language packs // (docs/I18N.md "Adding a language", docs/PACKS.md): a pack file dropped into public/i18n/ and a pack folder dropped // into packs/ while the server runs show in the menu and switch, with English filling what they lack; a removed pack -// sends a stored choice back to Chinese. The shipped Japanese, Korean and Traditional Chinese packs (the owner's decision of -// 2026-10-07) switch to their official game texts with the UI of their fallback (English; Simplified Chinese for zh-TW). +// sends a stored choice back to Chinese. The shipped Japanese, Korean and Traditional Chinese packs (the owner's decisions of +// 2026-10-07) switch to their official game texts and their own UI strings (what a pack lacks shows its fallback: English +// for ja / ko, the Simplified Chinese for zh-TW); a pack marked `machineTranslated` says so in 设置 (none in Chinese). // With more than four languages the menu is a list (ui/lang.js SEGMENTED_MAX): the helpers read and pick either form. // No console / page / request errors. docs/I18N.md. @@ -50,6 +51,23 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E= else await page.click(`[data-testid="lang-toggle"] button[data-lang="${code}"]`); }; const SHIPPED = [['zh', '中文'], ['en', 'English'], ['ja', '日本語'], ['ko', '한국어'], ['zh-TW', '繁體中文']]; + const pack = (code) => JSON.parse(fs.readFileSync(path.join(ROOT, `public/i18n/${code}.json`), 'utf8')); + /** What a shipped pack shows for a msgid: its own string, else its fallbacks' (English for ja / ko), else the Chinese msgid. */ + const uiText = (code, msgid) => { + const own = pack(code); + for (const p of [own, ...(own._meta?.fallback || []).map(pack)]) if (typeof p[msgid] === 'string' && p[msgid]) return p[msgid]; + return msgid; + }; + const MT_NOTE = '当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。'; + /** The note under the language switch in 设置 (null when there is none); the dialog opens from the title screen and closes. */ + const settingsNote = async (page) => { + await page.click('.title-settings'); + await page.waitForSelector('.modal .set-list'); + const note = await page.$eval('.modal [data-testid="lang-mt-note"]', (el) => el.textContent.trim()).catch(() => null); + await page.click('.modal__actions .btn--primary'); // 完成 (an Esc right after the dialog shows may beat its key listener) + await page.waitForSelector('.modal', { hidden: true }); + return note; + }; /** A game text of a shipped pack's overlay (data/i18n/.json; a leaf is a string or { _s }). */ const gameText = (code) => { const v = JSON.parse(fs.readFileSync(path.join(ROOT, `data/i18n/${code}.json`), 'utf8')).files.config.modes.mode_multi_abyss.name; return typeof v === 'string' ? v : v._s; }; @@ -139,7 +157,7 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E= await page.close(); } }); - test('the shipped 日本語 / 한국어 / 繁體中文 packs: the official game texts, the UI of their fallback (English; Simplified Chinese for zh-TW)', async () => { + test('the shipped 日本語 / 한국어 / 繁體中文 packs: the official game texts, their UI strings, the 设置 note of a machine-translated pack', async () => { const page = await browser.newPage(); await page.setViewport({ width: 1600, height: 900 }); const problems = []; @@ -148,13 +166,16 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E= page.on('response', (r) => { if (r.status() >= 400 && !/fonts\.(googleapis|gstatic)/.test(r.url())) problems.push(`http ${r.status()}: ${r.url()}`); }); await page.goto(`${base}/?lang=zh`, { waitUntil: 'networkidle0' }); await page.waitForSelector('.title-screen [data-testid="lang-toggle"]'); - for (const [code, ui, htmlLang] of [['ja', 'Start', 'ja'], ['ko', 'Start', 'ko'], ['zh-TW', '开始', 'zh-TW']]) { + assert.equal(await settingsNote(page), null, 'Chinese: no machine-translation note'); + for (const code of ['ja', 'ko', 'zh-TW']) { await pick(page, code); await page.waitForFunction((c) => globalThis.__SP__?.data?.locale() === c, { timeout: 8000 }, code); assert.equal(await page.evaluate(() => globalThis.__SP__.data.get('config').modes.mode_multi_abyss.name), gameText(code), `${code}: the official game text`); - assert.equal(await text(page, '.title-login .btn--primary'), ui, `${code}: the fallback UI`); - assert.equal(await page.evaluate(() => document.documentElement.lang), htmlLang); + assert.equal(await text(page, '.title-login .btn--primary'), uiText(code, '开始'), `${code}: the UI`); + assert.equal(await page.evaluate(() => document.documentElement.lang), code); + assert.equal(await settingsNote(page), pack(code)._meta.machineTranslated === true ? uiText(code, MT_NOTE) : null, `${code}: the 设置 note`); } + assert.equal(uiText('zh-TW', '开始'), '開始', 'zh-TW: its own Traditional Chinese UI'); assert.deepEqual(problems, []); await page.close(); });