feedback5: Japanese, Korean and Traditional Chinese ship — the official game texts of the JP / KR / TW clients (data/i18n/ja.json 96.5 %, ko.json 97.4 %, zh-TW.json 91.1 % of 22 114 texts, built with build-i18n --lang from ArknightsAssets/ArknightsGamedata) and minimal UI packs public/i18n/ja.json, ko.json, zh-TW.json (a _meta manifest, no UI strings yet): ja / ko fall back to the English UI, zh-TW to the Simplified Chinese UI (no fallback; numbers in 萬 / 億); the owner's decision of 2026-10-07; UI translations welcome from the community; golden: unchanged (client text only)
- The packs: _meta = type lang, the code, its own and English name (日本語 / Japanese, 한국어 / Korean, 繁體中文 / Traditional Chinese), version 0.2.0, app >=0.2.0, authors, credits (the official client data, © Hypergryph (/ Yostar for JP and KR), via ArknightsAssets/ArknightsGamedata), fallback ["en"] for ja / ko and [] for zh-TW; formatted as `node tools/i18n.mjs template` writes them, so a translator runs it on the shipped file and keeps the manifest. - Built offline from the tables W5AD downloaded and checked today (jp / kr 51.10.0, tw 51.2.0; copied into .cache/gamedata-<code>): coverage as W5AD measured; `build-i18n --lang <code> --check` up to date for en, ja, ko, zh-TW. `node tools/i18n.mjs check --all --strict`: 0 errors (en complete; ja / ko / zh-TW 0 of 1022 msgids, fallback). `node tools/packs.mjs list` lists the four packs with their game texts; `tools/package.mjs --dry-run --list` ships data/i18n and public/i18n of all four and the generated packs/index.json. - With five languages the menu is a list (ui/lang.js SEGMENTED_MAX 4, W5AD's rule): test/ui/i18n.e2e.test.js reads and picks either form, expects the shipped languages, and checks each new pack in the browser (its official game text, the fallback UI, <html lang>) — SP_E2E=1: 3 / 3; the title page of the five devices (devices.e2e "title page") still fits: 5 / 5. - Docs: docs/I18N.md (intro, §2, §5 "Shipped packs" with the call for UI translations), docs/PLAYING.md (语言), DESIGN §25.20 and the §25 intro's decisions of 2026-10-07. Gate: non-browser suites 5024 / 5024 (test/i18n*, test/package, client-static 358 / 358), npm run golden ok, test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok.
This commit is contained in:
1 parent
9738bb829a
commit
2e83a6b977
10 files changed
+125
-17
No files matched your search
@@ -0,0 +1 @@
|
||||
{"version":1,"lang":"ja","meta":{"generator":"tools/build-i18n.mjs","source":{"id":"assets","label":"ArknightsAssets/ArknightsGamedata (jp)","home":"https://github.com/ArknightsAssets/ArknightsGamedata","season":"act2autochess"},"fallback":"public/i18n/ja.json (the pack's UI strings)","coverage":{"bands":{"texts":186,"translated":186,"official":186,"pct":100},"bonds":{"texts":138,"translated":132,"official":132,"pct":95.7},"bosses":{"texts":66,"translated":66,"official":66,"pct":100},"choices":{"texts":679,"translated":678,"official":678,"pct":99.9},"config":{"texts":110,"translated":103,"official":103,"pct":93.6},"effects":{"texts":1083,"translated":1074,"official":1074,"pct":99.2},"emotes":{"texts":41,"translated":3,"official":3,"pct":7.3},"enemies":{"texts":1734,"translated":1727,"official":1727,"pct":99.6},"factions":{"texts":14,"translated":14,"official":14,"pct":100},"garrisons":{"texts":747,"translated":747,"official":747,"pct":100},"items":{"texts":582,"translated":564,"official":564,"pct":96.9},"modules":{"texts":2482,"translated":2416,"official":2416,"pct":97.3},"operators":{"texts":873,"translated":810,"official":810,"pct":92.8},"skills":{"texts":5646,"translated":5364,"official":5364,"pct":95},"stages":{"texts":1124,"translated":1111,"official":1111,"pct":98.8},"talents":{"texts":4297,"translated":4043,"official":4043,"pct":94.1},"tokens":{"texts":442,"translated":441,"official":441,"pct":99.8},"traits":{"texts":1870,"translated":1854,"official":1854,"pct":99.1}},"totals":{"texts":22114,"translated":21333,"official":21333,"pct":96.5}},"names":{"\"炎佑\"":"「炎祐」","Dor-1号失败品":"Dor-1号失敗作","Dor-α号失败品":"Dor-α号失敗作","Dor-β号复制体":"Dor-β号複製体","M3茧甲":"M3コクーンアーマー","W·悬赏":"懸賞・W","“变形者集群”":"「変形者」","“墓碑”":"「墓碑」","“墓碑”·悬赏":"懸賞・「墓碑」","“复仇者”":"Vendetta","“复仇者”·悬赏":"懸賞・「ヴェンデッタ」","“小自在”":"「小自在」","“巨大的丑东西”":"「ビッグアグリー」","“巨大的丑东西”·悬赏":"懸賞・「ビッグアグリー」","“帝国的甲胄”":"「帝国の甲冑」","“庞贝”":"「ポンペイ」","“庞贝”·悬赏":"懸賞・Pompeii","“斩胄之剑”":"「甲冑断つ剣」","“灵幛”":"「霊骸布」","“独轮车玩具”":"「一輪車のおもちゃ」","“破胄之锤”":"「甲冑割る槌」","“碎铳之簧”":"「機銃砕く輪」","“神秘顾客”":"「謎の客人」","“耀阳”":"「ブレイジング・サン」","“自在”":"「ジザイ」","“自在”·悬赏":"懸賞・「ジザイ」","“萨科塔之眼”":"「サンクタの眼」","“萨科塔之眼”·悬赏":"懸賞・「サンクタの眼」","“萨科塔之翼”":"「サンクタの翼」","“萨科塔昂首”":"「サンクタの仰け反り」","“萨米的意志”":"「サーミの意志」","“裂管之音”/“断弦之音”":"「断絃の音」/「裂管の音」","“越长尘”":"「長塵」","“遗弃者”":"「見棄てし者」","“遗弃者”·悬赏":"懸賞・「見棄てし者」","“邪魔的利刃”":"「悪魔の利刃」","“邪魔的利刃”·悬赏":"懸賞・「悪魔の利刃」","“鼠王”·悬赏":"懸賞・「鼠王」","不屈弹射器":"不屈の射出装置","不祥幻影":"不祥の幻影","不稳定要素":"不安定要素","业余竞演者":"一般参加の競演者","业务指标":"業務ノルマ","临时收音师":"臨時録音技師","主角阵营角色":"主役側の脇役","乌尔比安":"ウルピアヌス","乌顶巨角卢鲁":"黒き大角のルル","九流之缘":"人たらし","人事部文档":"人事部の書類","人才盲盒":"人材抽選","仇白":"チューバイ","从不混淆的方向":"混同せぬ方向","令":"リィン","以己之长":"持ち味を活かして","伊内丝":"イネス","伊利昂的木驮兽":"イリオスの木駄獣","伊奥莱塔·罗素":"イオレッタ・ラッセル","伊芙利特":"イフリータ","休眠子裔":"眠れる眷属","休谟斯":"ヒューマス","休露丝":"スキウース","众志合一":"一致団結","优等生":"優等生","传奇重犯":"伝説の凶悪犯","伪装服":"迷彩ヘルメット","伺夜":"ヴィジェル","余":"ユー","佩佩":"ペペ","保护目标(冻结状态)":"防衛ライン(凍結状態)","信仰搅拌机":"サンクタ・ミキサー","信标":"ビーコン","假想敌:再生":"仮想敵:再生","假想敌:弦":"仮想敵:絃","假想敌:淤困":"仮想敵:泥濘","假想敌:管":"仮想敵:管","假想敌:胄":"仮想敵:冑","假想敌:蚀裂":"仮想敵:蝕裂","假想敌:铳":"仮想敵:銃","假想敌:镜膜":"仮想敵:鏡膜","假想敌:骨刺":"仮想敵:骨棘","假想敌:黑云":"仮想敵:黒雲","假日威龙陈":"遊龍チェン","傀影":"ファントム","催泪瓦斯":"催Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"version":1,"lang":"ko","meta":{"generator":"tools/build-i18n.mjs","source":{"id":"assets","label":"ArknightsAssets/ArknightsGamedata (kr)","home":"https://github.com/ArknightsAssets/ArknightsGamedata","season":"act2autochess"},"fallback":"public/i18n/ko.json (the pack's UI strings)","coverage":{"bands":{"texts":186,"translated":186,"official":186,"pct":100},"bonds":{"texts":138,"translated":138,"official":138,"pct":100},"bosses":{"texts":66,"translated":66,"official":66,"pct":100},"choices":{"texts":679,"translated":679,"official":679,"pct":100},"config":{"texts":110,"translated":103,"official":103,"pct":93.6},"effects":{"texts":1083,"translated":1083,"official":1083,"pct":100},"emotes":{"texts":41,"translated":3,"official":3,"pct":7.3},"enemies":{"texts":1734,"translated":1734,"official":1734,"pct":100},"factions":{"texts":14,"translated":14,"official":14,"pct":100},"garrisons":{"texts":747,"translated":747,"official":747,"pct":100},"items":{"texts":582,"translated":570,"official":570,"pct":97.9},"modules":{"texts":2482,"translated":2419,"official":2419,"pct":97.5},"operators":{"texts":873,"translated":843,"official":843,"pct":96.6},"skills":{"texts":5646,"translated":5439,"official":5439,"pct":96.3},"stages":{"texts":1124,"translated":1111,"official":1111,"pct":98.8},"talents":{"texts":4297,"translated":4099,"official":4099,"pct":95.4},"tokens":{"texts":442,"translated":441,"official":441,"pct":99.8},"traits":{"texts":1870,"translated":1854,"official":1854,"pct":99.1}},"totals":{"texts":22114,"translated":21529,"official":21529,"pct":97.4}},"names":{"\"炎佑\"":"'염의 가호'","Dor-1号失败品":"Dor-1호 실패작","Dor-α号失败品":"Dor-α호 실패작","Dor-β号复制体":"Dor-β호 복제품","M3茧甲":"M3 고치 갑옷","W·悬赏":"W - 수배","“变形者集群”":"'다마즈티'","“墓碑”":"'툼스톤'","“墓碑”·悬赏":"'툼스톤' - 수배","“复仇者”":"'어벤져'","“复仇者”·悬赏":"'어벤져' - 수배","“小自在”":"'작은 즈자이'","“巨大的丑东西”":"'거대하고 못생긴 것'","“巨大的丑东西”·悬赏":"'거대하고 못생긴 것' - 수배","“帝国的甲胄”":"'제국의 갑옷'","“庞贝”":"'폼페이'","“庞贝”·悬赏":"'폼페이' - 수배","“斩胄之剑”":"'갑주를 베는 검'","“灵幛”":"'영장'","“独轮车玩具”":"'완구 외발자전거'","“破胄之锤”":"'갑주를 부수는 해머'","“碎铳之簧”":"'총을 부수는 리드'","“神秘顾客”":"'신비한 고객'","“耀阳”":"'눈 부신 태양'","“自在”":"'즈자이'","“自在”·悬赏":"'즈자이' - 수배","“萨科塔之眼”":"'산크타의 눈'","“萨科塔之眼”·悬赏":"'산크타의 눈' - 수배","“萨科塔之翼”":"'산크타의 날개'","“萨科塔昂首”":"'산크타의 긍지'","“萨米的意志”":"'사미의 의지'","“裂管之音”/“断弦之音”":"'단현지음'/'열관지음'","“越长尘”":"'월장진'","“遗弃者”":"'버림받은 자'","“遗弃者”·悬赏":"'버림받은 자' - 수배","“邪魔的利刃”":"'데몬의 칼날'","“邪魔的利刃”·悬赏":"'데몬의 칼날' - 수배","“鼠王”·悬赏":"'래트킹' - 수배","不屈":"불굴","不屈弹射器":"불굴의 사출기","不祥幻影":"불길한 환영","不稳定要素":"불안요소","业余竞演者":"아마추어 경연자","业务指标":"업무 지표","临时收音师":"임시 음향기사","主角阵营角色":"주인공 진영 캐릭터","乌尔比安":"울피아누스","乌顶巨角卢鲁":"검은 뿔의 루울","九流之缘":"구류의 인연","人事部文档":"인사부 파일","人才盲盒":"인재 랜덤박스","仇白":"치우바이","从不混淆的方向":"헷갈리지 않는 방향","令":"링","以己之长":"장점 활용","伊内丝":"이네스","伊利昂的木驮兽":"일리온의 나무 버든비스트","伊奥莱塔·罗素":"이올레타 로시","伊芙利特":"이프리트","休眠子裔":"잠자는 후예","休谟斯":"휴머스","休露丝":"시우루스","众志合一":"일치단결","优等生":"우등생","传奇重犯":"전설의 중범죄수","伪装服":"위장복","伺夜":"비질","余":"위","佩佩":"페페","俗心":"수신","保护目标(冻结状态)":"방어 포인트 (빙결 상태)","信仰搅拌机":"산크타 믹사파라토","信标":"비콘","假想敌:再生":"가상적: 재생","假想敌:弦":"가상적: 현","假想敌:淤困":"가상적: 정체","假想敌:管":"가상적: 관","假想敌:胄":"가상적: 갑주","假想敌:蚀裂":"가상적: 침식","假想敌:铳":"가상적: 총","假想敌:镜膜":"가상적: 경막","假想敌:骨刺":"가상적: 골극","假想敌:黑云":"가상적: 흑운","假日威龙陈":"첸 더 홀룽데이","傀影":"팬텀","催泪瓦斯":"최루가스","元核孽生者":"원핵의 악생자","元核孽生者·悬赏":"원핵의 악생자 - 수배","克莱Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"version":1,"lang":"zh-TW","meta":{"generator":"tools/build-i18n.mjs","source":{"id":"assets","label":"ArknightsAssets/ArknightsGamedata (tw)","home":"https://github.com/ArknightsAssets/ArknightsGamedata","season":"act2autochess"},"fallback":"public/i18n/zh-TW.json (the pack's UI strings)","coverage":{"bands":{"texts":186,"translated":170,"official":170,"pct":91.4},"bonds":{"texts":138,"translated":130,"official":130,"pct":94.2},"bosses":{"texts":66,"translated":66,"official":66,"pct":100},"choices":{"texts":679,"translated":674,"official":674,"pct":99.3},"config":{"texts":110,"translated":103,"official":103,"pct":93.6},"effects":{"texts":1083,"translated":1053,"official":1053,"pct":97.2},"emotes":{"texts":41,"translated":3,"official":3,"pct":7.3},"enemies":{"texts":1734,"translated":1659,"official":1659,"pct":95.7},"factions":{"texts":14,"translated":14,"official":14,"pct":100},"garrisons":{"texts":747,"translated":747,"official":747,"pct":100},"items":{"texts":582,"translated":544,"official":544,"pct":93.5},"modules":{"texts":2482,"translated":2302,"official":2302,"pct":92.7},"operators":{"texts":873,"translated":585,"official":585,"pct":67},"skills":{"texts":5646,"translated":5110,"official":5110,"pct":90.5},"stages":{"texts":1124,"translated":901,"official":901,"pct":80.2},"talents":{"texts":4297,"translated":3806,"official":3806,"pct":88.6},"tokens":{"texts":442,"translated":419,"official":419,"pct":94.8},"traits":{"texts":1870,"translated":1854,"official":1854,"pct":99.1}},"totals":{"texts":22114,"translated":20140,"official":20140,"pct":91.1}},"names":{"\"炎佑\"":"“炎佑”","Dor-1号失败品":"Dor-1號失敗品","Dor-α号失败品":"Dor-α號失敗品","Dor-β号复制体":"Dor-β號複製體","M3茧甲":"M3繭甲","W·悬赏":"W·懸賞","“变形者集群”":"“變形者集群”","“墓碑”·悬赏":"“墓碑”·懸賞","“复仇者”":"“復仇者”","“复仇者”·悬赏":"“復仇者”·懸賞","“巨大的丑东西”":"“巨大的醜東西”","“巨大的丑东西”·悬赏":"“巨大的醜東西”·懸賞","“帝国的甲胄”":"“帝國的甲胄”","“庞贝”":"“龐貝”","“庞贝”·悬赏":"“龐貝”·懸賞","“斩胄之剑”":"“斬胄之劍”","“灵幛”":"“靈幛”","“独轮车玩具”":"“獨輪車玩具”","“破胄之锤”":"“破胄之錘”","“碎铳之簧”":"“碎銃之簧”","“神秘顾客”":"“神秘顧客”","“耀阳”":"“耀陽”","“自在”·悬赏":"“自在”·懸賞","“萨科塔之眼”":"“薩科塔之眼”","“萨科塔之眼”·悬赏":"“薩科塔之眼”·懸賞","“萨科塔之翼”":"“薩科塔之翼”","“萨科塔昂首”":"“薩科塔昂首”","“萨米的意志”":"“薩米的意志”","“裂管之音”/“断弦之音”":"“裂管之音”/“斷弦之音”","“越长尘”":"“越長塵”","“遗弃者”":"“遺棄者”","“遗弃者”·悬赏":"“遺棄者”·懸賞","“邪魔的利刃”·悬赏":"“邪魔的利刃”·懸賞","“鼠王”·悬赏":"“鼠王”·懸賞","不屈弹射器":"不屈彈射器","不稳定要素":"不穩定要素","业余竞演者":"業餘競演者","业务指标":"業務指標","临时收音师":"臨時收音師","主角阵营角色":"主角陣營角色","乌尔比安":"烏爾比安","乌顶巨角卢鲁":"烏頂巨角盧魯","九流之缘":"九流之緣","人事部文档":"人事部文件","从不混淆的方向":"從不混淆的方向","以己之长":"以己之長","伊内丝":"伊內絲","伊利昂的木驮兽":"伊利昂的木馱獸","伊奥莱塔·罗素":"伊奧萊塔·羅素","休谟斯":"休謨斯","休露丝":"休露絲","众志合一":"眾志合一","优等生":"優等生","传奇重犯":"傳奇重犯","伪装服":"偽裝服","余":"餘","保护目标(冻结状态)":"保護目標(凍結狀態)","信仰搅拌机":"信仰攪拌機","信标":"信標","假想敌:再生":"假想敵:再生","假想敌:弦":"假想敵:弦","假想敌:淤困":"假想敵:淤困","假想敌:管":"假想敵:管","假想敌:胄":"假想敵:胄","假想敌:蚀裂":"假想敵:蝕裂","假想敌:铳":"假想敵:銃","假想敌:镜膜":"假想敵:鏡膜","假想敌:骨刺":"假想敵:骨刺","假想敌:黑云":"假想敵:黑雲","假日威龙陈":"假日威龍陳","催泪瓦斯":"催淚瓦斯","元核孽生者·悬赏":"元核孽生者·懸賞","克莱门莎":"克萊門莎","军用猎狗":"軍用獵狗","冰爆源石虫":"冰爆源石蟲","冲击式施术单元":"衝擊式施術單元","凋零骑士":"凋零騎士","凋零骑士·悬赏":"凋零騎士·懸賞","凛御银灰":"凜御銀灰","凯尔希":"凱爾希","凯瑟琳":"凱瑟琳","列装":"列裝","利滚利":"利滾利","刺胄之弹":"刺胄之彈","加急调派":"緊急調派","加练!":"加練!","加速维式重锤":"加速維式重鎚","医疗探机":"醫療探機","升华":"昇華","华法琳":"華法琳","Line truncated
|
||||
+15
-4
@@ -4,8 +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. The
|
||||
choice is kept in `localStorage` (`sp.pref.lang`); `?lang=<code>` 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 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=<code>` 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).
|
||||
@@ -173,8 +174,10 @@ rebuilt.
|
||||
22 114 texts — the TW data is older, 51.2, and a Traditional text equal to the Simplified one counts as untranslated
|
||||
although it shows the same). The remake's own strings (stage-name composites, the fallback files) are English only;
|
||||
for another language the pack's UI file (`public/i18n/<code>.json`) is the fallback dictionary, and what is left falls
|
||||
back per text at run time (the pack's chain, then Chinese). Only English is generated in the repository: a game-text
|
||||
overlay ships with the UI pack of its language (a pack is listed by its UI file).
|
||||
back per text at run time (the pack's chain, then Chinese). A game-text overlay ships with the UI pack of its language
|
||||
(a pack is listed by its UI file): the repository generates English and, since the owner's decision of 2026-10-07,
|
||||
`data/i18n/ja.json`, `ko.json` and `zh-TW.json` (§5 "Shipped packs"). Rebuild them like English after a data change
|
||||
(`--lang <code> --check` says when one is out of date).
|
||||
- **A language without an official client** (French, German, Russian …) needs no game texts at all: they follow the
|
||||
pack's chain (`"fallback": ["en"]` shows the English ones). To translate some, write a community dictionary — a flat
|
||||
JSON `{ "<Chinese data text>": "<translation>" }`; the `names` of `data/i18n/en.json` (every game-data name with its
|
||||
@@ -237,6 +240,14 @@ 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 <code>` keeps its `_meta` and
|
||||
adds every msgid with an empty value — then follow the steps below and send a pull request.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
本文数值来自官方数据表(`data/config.json` 等)与 [research](research/00-INDEX.md);游戏内左下角 📖「玩法说明」有官方教程图(随素材一起下载;没下载到时显示文字要点)。操作方式见 [README · 操作](../README.md#操作),键盘快捷键和修改方法见 [§11](#11-快捷键)。
|
||||
|
||||
界面语言:默认中文;标题画面右上角或「设置」里可切换 **中文 / English**(会记住选择,不用刷新)。英文的干员、技能、盟约、装备、敌人等游戏文本取自官方英文服数据,界面文字全部有英文;切到英文后仍显示中文的只有玩家名字(人机的名字也是)、多轮悬赏改写过的「接下来两场作战」说明,以及官方英文服还没有的游戏文本(部分自选干员和较新的模组);「玩法说明」的教程图是官方的中文图片,标题有英文(docs/I18N.md)。
|
||||
界面语言:默认中文;标题画面右上角或「设置」里可切换 **中文 / English / 日本語 / 한국어 / 繁體中文**(会记住选择,不用刷新)。日语、韩语、繁体中文的游戏文本取自官方日服 / 韩服 / 台服数据,界面文字暂未翻译——日语和韩语显示英文界面,繁体中文显示简体中文界面,欢迎社区补全(docs/I18N.md「Adding a language」)。英文的干员、技能、盟约、装备、敌人等游戏文本取自官方英文服数据,界面文字全部有英文;切到英文后仍显示中文的只有玩家名字(人机的名字也是)、多轮悬赏改写过的「接下来两场作战」说明,以及官方英文服还没有的游戏文本(部分自选干员和较新的模组);「玩法说明」的教程图是官方的中文图片,标题有英文(docs/I18N.md)。
|
||||
|
||||
## 1. 一局的流程
|
||||
|
||||
|
||||
+11
-3
@@ -58,7 +58,9 @@ lock (§25.13.2); a newly merged elite 拉普兰德 counts as a new 拉普兰德
|
||||
alive at the fight's start, adopting PR #209 and replacing 「保持固定血量」 (§25.13.4); flying units at 1.3 tiles and the two
|
||||
model quirks of PR #211 (§25.13.5); the bounty pays whenever its enemy dies (relayed with the community
|
||||
reports, §25.14.3); 2026-10-07 — the in-match shortcuts rebindable in 设置 (§25.15); a language is a file in the
|
||||
language folder, and the language pack is the first type of a general content-pack mechanism (§25.20); 乌尔比安 knocked
|
||||
language folder, and the language pack is the first type of a general content-pack mechanism; Japanese, Korean and
|
||||
Traditional Chinese ship with the official game texts and fallback UIs — the Traditional one's in Simplified Chinese
|
||||
(§25.20); 乌尔比安 knocked
|
||||
out during his S3 comes back on his deployment tile, a deliberate deviation from PRTS's "where it fell" (§25.17.3);
|
||||
烛煌's 绝处重燃 before 坚固维式重锤's lock and the official tier cap of 寻呼模块 / 简易通讯机 kept as they are (§25.21.1,
|
||||
§25.21.10).
|
||||
@@ -1707,8 +1709,14 @@ one commit with its tests; two open community PRs were reused without merging th
|
||||
`node tools/build-i18n.mjs --lang ja | ko | zh-TW` builds a pack's game texts from the official JP / KR / TW client
|
||||
(ArknightsAssets/ArknightsGamedata `jp/`, `kr/`, `tw/`, each with both 盟约 seasons, checked 2026-10-07: ja 96.5 %,
|
||||
ko 97.4 %, zh-TW 91.1 % of 22 114 texts); a language without a client uses `--dict` (a community dictionary) or no
|
||||
game texts at all (they follow the chain). No language but English ships: there is no Japanese, Korean or
|
||||
Traditional Chinese UI pack yet, and a game-text file without its UI pack is not listed.
|
||||
game texts at all (they follow the chain). The stream shipped no language but English (a game-text file without
|
||||
its UI pack is not listed).
|
||||
- **Shipped: 日本語, 한국어, 繁體中文** (the owner's decision of 2026-10-07, at the merge): `data/i18n/ja.json`,
|
||||
`ko.json`, `zh-TW.json` built with `build-i18n --lang` (ja 96.5 %, ko 97.4 %, zh-TW 91.1 %) and minimal UI packs
|
||||
`public/i18n/ja.json`, `ko.json`, `zh-TW.json` — a `_meta` manifest (its own and English name, credits: the official
|
||||
client data via ArknightsAssets) and no UI strings yet: `ja` / `ko` fall back to English, `zh-TW` to the Simplified
|
||||
Chinese UI (no fallback — the owner's choice; [ASSUMED] numbers in 萬 / 億). The menu shows the five languages as
|
||||
buttons. UI translations are welcome from the community (docs/I18N.md §5 "Shipped packs").
|
||||
- **The output in Chinese and English did not change**: `test/i18n-packs.test.js` renders every msgid of
|
||||
`public/i18n/en.json` in both languages with sample params and compares it with the renderer of 0.2.0 before packs;
|
||||
`node tools/build-i18n.mjs --check` still finds `data/i18n/en.json` up to date. One English entry was added, the tab
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"_meta": {
|
||||
"type": "lang",
|
||||
"lang": "ja",
|
||||
"name": "日本語",
|
||||
"englishName": "Japanese",
|
||||
"version": "0.2.0",
|
||||
"app": ">=0.2.0",
|
||||
"authors": [
|
||||
"the Stronghold Protocol contributors"
|
||||
],
|
||||
"credits": "Game texts (data/i18n/ja.json): the official JP client data (© Hypergryph / Yostar) via ArknightsAssets/ArknightsGamedata. The UI strings are not translated yet — they show in English until a community translation fills them in (docs/I18N.md §5).",
|
||||
"fallback": [
|
||||
"en"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"_meta": {
|
||||
"type": "lang",
|
||||
"lang": "ko",
|
||||
"name": "한국어",
|
||||
"englishName": "Korean",
|
||||
"version": "0.2.0",
|
||||
"app": ">=0.2.0",
|
||||
"authors": [
|
||||
"the Stronghold Protocol contributors"
|
||||
],
|
||||
"credits": "Game texts (data/i18n/ko.json): the official KR client data (© Hypergryph / Yostar) via ArknightsAssets/ArknightsGamedata. The UI strings are not translated yet — they show in English until a community translation fills them in (docs/I18N.md §5).",
|
||||
"fallback": [
|
||||
"en"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"_meta": {
|
||||
"type": "lang",
|
||||
"lang": "zh-TW",
|
||||
"name": "繁體中文",
|
||||
"englishName": "Traditional Chinese",
|
||||
"version": "0.2.0",
|
||||
"app": ">=0.2.0",
|
||||
"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).",
|
||||
"fallback": [],
|
||||
"numberUnits": [
|
||||
"萬",
|
||||
"億"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
// 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. No console / page / request errors. docs/I18N.md.
|
||||
// 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).
|
||||
// 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.
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -38,6 +41,17 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E=
|
||||
after(async () => { removePacks(); await browser?.close(); await srv?.close(); });
|
||||
|
||||
const text = (page, sel) => page.$eval(sel, (el) => el.textContent.replace(/\s+/g, ' ').trim());
|
||||
/** The menu's languages, [code, label] — its buttons, or the options of its list. */
|
||||
const menu = (page) => page.$$eval('[data-testid="lang-toggle"] button, [data-testid="lang-toggle"] option',
|
||||
(els) => els.map((e) => [e.dataset.lang ?? e.value, e.textContent.trim()]));
|
||||
/** Pick a language: its button, or its option in the list. */
|
||||
const pick = async (page, code) => {
|
||||
if (await page.$('[data-testid="lang-toggle"] select')) await page.select('[data-testid="lang-toggle"] select', code);
|
||||
else await page.click(`[data-testid="lang-toggle"] button[data-lang="${code}"]`);
|
||||
};
|
||||
const SHIPPED = [['zh', '中文'], ['en', 'English'], ['ja', '日本語'], ['ko', '한국어'], ['zh-TW', '繁體中文']];
|
||||
/** A game text of a shipped pack's overlay (data/i18n/<code>.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; };
|
||||
|
||||
test('中文 by default; English in place, kept across a reload; ?lang=zh back to Chinese', async () => {
|
||||
const page = await browser.newPage();
|
||||
@@ -52,7 +66,8 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E=
|
||||
assert.equal(await text(page, '.title-login .btn--primary'), '开始');
|
||||
assert.equal(await page.evaluate(() => document.documentElement.lang), 'zh-CN');
|
||||
|
||||
await page.click('[data-testid="lang-toggle"] button[data-lang="en"]');
|
||||
assert.deepEqual(await menu(page), SHIPPED, 'the shipped languages');
|
||||
await pick(page, 'en');
|
||||
await page.waitForFunction(() => document.querySelector('.title-cn')?.textContent.includes('Stronghold Protocol'), { timeout: 8000 });
|
||||
assert.equal(await text(page, '.title-cn'), 'Stronghold Protocol: Alliance');
|
||||
assert.equal(await text(page, '.title-login .btn--primary'), 'Start');
|
||||
@@ -93,11 +108,10 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E=
|
||||
fs.writeFileSync(path.join(PACK_DIR, 'ui.json'), JSON.stringify({ 开始: 'Qab-Start' }));
|
||||
await sleep(1100); // the server's registry looks at the folders again after a second
|
||||
await page.goto(`${base}/?lang=zh`, { waitUntil: 'networkidle0' });
|
||||
await page.waitForSelector('.title-screen [data-testid="lang-toggle"] button[data-lang="qaa"]');
|
||||
assert.deepEqual(await page.$$eval('[data-testid="lang-toggle"] button', (bs) => bs.map((b) => [b.dataset.lang, b.textContent.trim()])),
|
||||
[['zh', '中文'], ['en', 'English'], ['qaa', 'Testisch'], ['qab', 'Qabisch']]);
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.title-screen [data-testid="lang-toggle"] :is(button, option)')].some((e) => (e.dataset.lang ?? e.value) === 'qaa'), { timeout: 8000 });
|
||||
assert.deepEqual(await menu(page), [...SHIPPED.slice(0, 4), ['qaa', 'Testisch'], ['qab', 'Qabisch'], SHIPPED[4]]);
|
||||
|
||||
await page.click('[data-testid="lang-toggle"] button[data-lang="qaa"]');
|
||||
await pick(page, 'qaa');
|
||||
await page.waitForFunction(() => document.querySelector('.title-login .btn--primary')?.textContent.includes('Los'), { timeout: 8000 });
|
||||
assert.equal(await text(page, '.title-cn'), 'Testprotokoll: Bund', 'an alphabetic title: the display face, an ASCII colon');
|
||||
assert.match(await text(page, '.title-tag'), /^Allocate Funds and Operators/, 'untranslated: English, the pack\'s fallback');
|
||||
@@ -107,22 +121,41 @@ describe('language switch on the title screen', { skip: !ENABLED && 'set SP_E2E=
|
||||
await page.waitForFunction(() => globalThis.__SP__?.data?.locale() === 'qaa', { timeout: 8000 });
|
||||
assert.equal(await page.evaluate(() => globalThis.__SP__.data.get('config').modes.mode_multi_abyss.name), 'Ultimate Simulation');
|
||||
|
||||
await page.click('[data-testid="lang-toggle"] button[data-lang="qab"]');
|
||||
await pick(page, 'qab');
|
||||
await page.waitForFunction(() => document.querySelector('.title-login .btn--primary')?.textContent.includes('Qab-Start'), { timeout: 8000 });
|
||||
assert.equal(await text(page, '.title-cn'), '卫戍协议:盟约', 'no fallback declared: the Chinese msgid');
|
||||
|
||||
await page.click('[data-testid="lang-toggle"] button[data-lang="qaa"]');
|
||||
await pick(page, 'qaa');
|
||||
await page.waitForFunction(() => document.querySelector('.title-login .btn--primary')?.textContent.includes('Los'), { timeout: 8000 });
|
||||
removePacks();
|
||||
await sleep(1100);
|
||||
await page.reload({ waitUntil: 'networkidle0' });
|
||||
await page.waitForSelector('.title-screen .title-cn');
|
||||
assert.equal(await text(page, '.title-cn'), '卫戍协议:盟约', 'the stored pack is gone: Chinese');
|
||||
assert.deepEqual(await page.$$eval('[data-testid="lang-toggle"] button', (bs) => bs.map((b) => b.dataset.lang)), ['zh', 'en']);
|
||||
assert.deepEqual(await menu(page), SHIPPED);
|
||||
assert.deepEqual(problems, []);
|
||||
} finally {
|
||||
removePacks();
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
test('the shipped 日本語 / 한국어 / 繁體中文 packs: the official game texts, the UI of their fallback (English; Simplified Chinese for zh-TW)', async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1600, height: 900 });
|
||||
const problems = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||
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']]) {
|
||||
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.deepEqual(problems, []);
|
||||
await page.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user