Files
sendou.ink/scripts/create-misc-json.ts
Kalle 92627e73aa
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled
i18n translate page, prune & allow different language for site text and in-game text (#3456)
2026-10-14 21:12:38 +03:00

333 lines
8.5 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { abilitiesShort } from "~/modules/in-game-lists/abilities";
import { brandIds } from "~/modules/in-game-lists/brand-ids";
import { modesShortWithSpecial } from "~/modules/in-game-lists/modes";
import { invariant } from "~/utils/invariant";
import {
LANG_JSONS_TO_CREATE,
loadLangDicts,
translationJsonFolderName,
} from "./utils";
type LangDicts = Awaited<ReturnType<typeof loadLangDicts>>;
type LangDict = LangDicts[number][1];
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const BADGE_DIR = path.join(__dirname, "badge");
const OUTPUT_DIR = path.join(__dirname, "output");
const BADGE_TEMPLATE_RULES = [
{
prefix: "CoopGrade_Normal_",
templateKey: "CoopGrade_Normal_",
lookupDict: "CommonMsg/Coop/CoopStageName",
},
{
prefix: "CoopGrade_Pair_",
templateKey: "CoopGrade_Normal_",
lookupDict: "CommonMsg/Coop/CoopStageName",
},
{
prefix: "CoopGrade_Underground_",
templateKey: "CoopGrade_Normal_",
lookupDict: "CommonMsg/Coop/CoopStageName",
},
{
prefix: "CoopBossKillNum_",
templateKey: "CoopBossKillNum_",
lookupDict: "CommonMsg/Coop/CoopEnemy",
},
{
prefix: "CoopRareEnemyKillNum_",
templateKey: "CoopRareEnemyKillNum_",
lookupDict: "CommonMsg/Coop/CoopEnemy",
},
{
prefix: "GearTotalRarity_",
templateKey: "GearTotalRarity_",
lookupDict: "CommonMsg/Gear/GearBrandName",
},
{
prefix: "WinCount_WeaponSp_",
templateKey: "WinCount_WeaponSp_",
lookupDict: "CommonMsg/Weapon/WeaponName_Special",
},
{
prefix: "WeaponLevel_",
templateKey: "WeaponLevel_",
lookupDict: "CommonMsg/Weapon/WeaponName_Main",
},
];
// ⚠️ keep same order as https://github.com/IPLSplatoon/IPLMapGen2/blob/main/data.js
const stages = [
"Scorch Gorge",
"Eeltail Alley",
"Hagglefish Market",
"Undertow Spillway",
"Mincemeat Metalworks",
"Hammerhead Bridge",
"Museum d'Alfonsino",
"Mahi-Mahi Resort",
"Inkblot Art Academy",
"Sturgeon Shipyard",
"MakoMart",
"Wahoo World",
"Flounder Heights",
"Brinewater Springs",
"Manta Maria",
"Um'ami Ruins",
"Humpback Pump Track",
"Barnacle & Dime",
"Crableg Capital",
"Shipshape Cargo Co.",
"Bluefin Depot",
"Robo ROM-en",
"Marlin Airport",
"Lemuria Hub",
"Urchin Underpass",
] as const;
const modeShortToLangDictEntry: Record<
(typeof modesShortWithSpecial)[number],
[dict: string, key: string]
> = {
TW: ["CommonMsg/VS/VSRuleName", "Pnt"],
SZ: ["CommonMsg/VS/VSRuleName", "Var"],
TC: ["CommonMsg/VS/VSRuleName", "Vlf"],
RM: ["CommonMsg/VS/VSRuleName", "Vgl"],
CB: ["CommonMsg/VS/VSRuleName", "Vcl"],
TB: ["CommonMsg/MatchMode", "MiniGame"],
SR: ["CommonMsg/MatchMode", "Coop"],
};
/** Replaces generated values where the game's text doesn't work as a standalone name */
const TRANSLATION_OVERRIDES: Record<string, Record<string, string>> = {
// game shows the English logo text "SALMON RUN", the names below are from its titles
JPja: { MODE_LONG_SR: "サーモンラン" },
KRko: { MODE_LONG_SR: "새먼 런" },
CNzh: { MODE_LONG_SR: "鲑鱼跑" },
};
const abilityShortToInternalName = new Map([
["ISM", "MainInk_Save"],
["ISS", "SubInk_Save"],
["IRU", "InkRecovery_Up"],
["RSU", "HumanMove_Up"],
["SSU", "SquidMove_Up"],
["SCU", "SpecialIncrease_Up"],
["SS", "RespawnSpecialGauge_Save"],
["SPU", "SpecialSpec_Up"],
["QR", "RespawnTime_Save"],
["QSJ", "JumpTime_Save"],
["BRU", "SubSpec_Up"],
["RES", "OpInkEffect_Reduction"],
["SRU", "SubEffect_Reduction"],
["IA", "Action_Up"],
["OG", "StartAllUp"],
["LDE", "EndAllUp"],
["T", "MinorityUp"],
["CB", "ComeBack"],
["NS", "SquidMoveSpatter_Reduction"],
["H", "DeathMarking"],
["TI", "ThermalInk"],
["RP", "Exorcist"],
["AD", "ExSkillDouble"],
["SJ", "SuperJumpSign_Hide"],
["OS", "ObjectEffect_Up"],
["DR", "SomersaultLanding"],
]);
async function main() {
const langDicts = await loadLangDicts();
const englishLangDict = langDicts.find(
([langCode]) => langCode === "EUen",
)?.[1];
invariant(englishLangDict);
const codeNames = stages.map((stage) => {
const codeName = Object.entries(
englishLangDict["CommonMsg/VS/VSStageName"],
).find(([_key, value]) => value === stage)?.[0];
invariant(codeName, `Could not find code name for stage ${stage}`);
return codeName;
});
for (const langCode of LANG_JSONS_TO_CREATE) {
const langDict = langDicts.find(([code]) => code === langCode)?.[1];
invariant(langDict, `Missing translations for ${langCode}`);
const translationsMap = Object.fromEntries(
stages.map((_, i) => {
const codeName = codeNames[
i
] as keyof (typeof langDict)["CommonMsg/VS/VSStageName"];
invariant(codeName);
return [`STAGE_${i}`, langDict["CommonMsg/VS/VSStageName"][codeName]];
}),
);
for (const ability of abilitiesShort) {
const internalName = abilityShortToInternalName.get(ability);
invariant(internalName, `Missing internal name for ${ability}`);
const translation = decodeURIComponent(
(langDict["CommonMsg/Gear/GearPowerName"] as Record<string, string>)[
internalName
],
);
translationsMap[`ABILITY_${ability}`] = translation;
}
for (const brandCode of brandIds) {
const translation = decodeURIComponent(
langDict["CommonMsg/Gear/GearBrandName"][brandCode],
);
translationsMap[`BRAND_${brandCode}`] = translation;
}
for (const mode of modesShortWithSpecial) {
const [dict, key] = modeShortToLangDictEntry[mode];
const translation = langDict[dict]?.[key];
invariant(translation, `Missing translation for mode ${mode}`);
translationsMap[`MODE_LONG_${mode}`] = translation;
}
for (const [key, translation] of Object.entries(
TRANSLATION_OVERRIDES[langCode] ?? {},
)) {
invariant(
translationsMap[key],
`Override for ${langCode} ${key} doesn't match a generated key`,
);
translationsMap[key] = translation;
}
fs.writeFileSync(
path.join(
__dirname,
"..",
"locales",
translationJsonFolderName(langCode),
"game-misc.json",
),
`${JSON.stringify(translationsMap, null, 2)}\n`,
);
}
generateBadgeData(langDicts);
}
function generateBadgeData(langDicts: LangDicts) {
const badgeFiles = fs
.readdirSync(BADGE_DIR)
.filter((f) => f.endsWith(".png"));
const badgeIds = badgeFiles
.map((f) => f.replace("Badge_", "").replace(".png", ""))
.sort();
for (const langCode of LANG_JSONS_TO_CREATE) {
const langDict = langDicts.find(([code]) => code === langCode)?.[1];
invariant(langDict, `Missing translations for ${langCode}`);
const translationsMap = buildBadgeTranslations(badgeIds, langDict);
const folder = translationJsonFolderName(langCode);
writeBadgeJson(folder, translationsMap);
}
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
const tsContent = [
"export const GAME_BADGE_IDS = [",
...badgeIds.map((id) => `\t"${id}",`),
"] as const;",
"",
"export type GameBadgeId = (typeof GAME_BADGE_IDS)[number];",
"",
].join("\n");
fs.writeFileSync(path.join(OUTPUT_DIR, "game-badge-ids.ts"), tsContent);
}
function buildBadgeTranslations(badgeIds: string[], langDict: LangDict) {
const badgeMsg = langDict["CommonMsg/Badge/BadgeMsg"] as Record<
string,
string
>;
const translationsMap: Record<string, string> = {};
for (const id of badgeIds) {
if (badgeMsg[id] && !badgeMsg[id].includes("[group=")) {
translationsMap[id] = badgeMsg[id];
continue;
}
const lvMatch = id.match(/_Lv(\d+)$/);
if (!lvMatch) {
translationsMap[id] = badgeMsg[id] ?? id;
continue;
}
const lvSuffix = `Lv${lvMatch[1]}`;
const rule = BADGE_TEMPLATE_RULES.find((r) => id.startsWith(r.prefix));
if (!rule) {
translationsMap[id] = badgeMsg[id] ?? id;
continue;
}
const variantName = id
.slice(rule.prefix.length)
.replace(`_${lvSuffix}`, "");
const templateKey = `${rule.templateKey}${lvSuffix}`;
const template = badgeMsg[templateKey];
if (!template) {
translationsMap[id] = id;
continue;
}
const lookupValue = (
langDict as unknown as Record<string, Record<string, string> | undefined>
)[rule.lookupDict]?.[variantName];
if (!lookupValue) {
translationsMap[id] = id;
continue;
}
translationsMap[id] = template.replace(
/\[group=\d+ type=\w+ params=[\w ]+\]/,
lookupValue,
);
}
return translationsMap;
}
function writeBadgeJson(
folder: string,
translationsMap: Record<string, string>,
) {
fs.writeFileSync(
path.join(__dirname, "..", "locales", folder, "game-badges.json"),
`${JSON.stringify(translationsMap, null, 2)}\n`,
);
}
void main();