Change name tables to key by name instead of ID
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled

- Also properly load modded text descriptions
- Also remove dex.text.whateverName in favor of TL.whatever

Client-side counterpart to
92f01f8f71
This commit is contained in:
Guangcong Luo
2026-09-13 11:54:41 -07:00
parent e47b8be410
commit fccc266fef
9 changed files with 79 additions and 91 deletions

View File

@@ -27,6 +27,7 @@ child_process.execSync('npm run build', { cwd: 'caches/pokemon-showdown' });
console.log("DONE");
const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex;
const { CLIENT_MODS } = require('./build-translations');
const CompoundWordNames = require('../caches/pokemon-showdown/dist/data/aliases').CompoundWordNames;
const toID = Dex.toID;
process.stdout.write("Loading gen 6 data... ");
@@ -1213,11 +1214,12 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
const overrideSpeciesKeys = [
'abilities', 'baseStats', 'cosmeticFormes', 'isNonstandard', 'requiredItems', 'types', 'unreleasedHidden',
];
// Descs omitted (they're all compiled into data/text/*)
const overrideMoveKeys = [
'accuracy', 'basePower', 'category', 'desc', 'flags', 'isNonstandard', 'pp', 'priority', 'shortDesc', 'target', 'type',
'accuracy', 'basePower', 'category', 'flags', 'isNonstandard', 'pp', 'priority', 'target', 'type',
];
const overrideAbilityKeys = ['desc', 'flags', 'isNonstandard', 'rating', 'shortDesc'];
const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'naturalGift', 'shortDesc'];
const overrideAbilityKeys = ['flags', 'isNonstandard', 'rating'];
const overrideItemKeys = ['fling', 'isNonstandard', 'naturalGift'];
//
// Past gen table
@@ -1306,9 +1308,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
// Mods
//
for (const mod of [
'gen3rs', 'gen3frlg', 'gen5bw1', 'gen7letsgo', 'gen8bdsp', 'gen9ssb', 'champions',
]) {
for (const mod of CLIENT_MODS) {
const modDex = Dex.mod(mod);
const modData = modDex.data;
const parentDex = Dex.forGen(modDex.gen);
@@ -1409,9 +1409,6 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
const Moves = requireNoCache('../caches/pokemon-showdown/dist/data/moves.js').Moves;
for (const id in Moves) {
const move = Dex.moves.get(Moves[id].name);
const text = Dex.text.get(move);
if (text.desc) Moves[id].desc = text.desc;
if (text.shortDesc) Moves[id].shortDesc = text.shortDesc;
if (move.basePowerCallback) Moves[id].basePowerCallback = true;
}
const buf = 'exports.BattleMovedex = ' + es3stringify(Moves) + ';';
@@ -1425,12 +1422,6 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
{
const Items = requireNoCache('../caches/pokemon-showdown/dist/data/items.js').Items;
for (const id in Items) {
const item = Dex.items.get(Items[id].name);
const text = Dex.text.get(item);
if (text.desc) Items[id].desc = text.desc;
if (text.shortDesc) Items[id].shortDesc = text.shortDesc;
}
const buf = 'exports.BattleItems = ' + es3stringify(Items) + ';';
fs.writeFileSync('play.pokemonshowdown.com/data/items.js', buf);
}
@@ -1441,12 +1432,6 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
{
const Abilities = requireNoCache('../caches/pokemon-showdown/dist/data/abilities.js').Abilities;
for (const id in Abilities) {
const ability = Dex.abilities.get(Abilities[id].name);
const text = Dex.text.get(ability);
if (text.desc) Abilities[id].desc = text.desc;
if (text.shortDesc) Abilities[id].shortDesc = text.shortDesc;
}
const buf = 'exports.BattleAbilities = ' + es3stringify(Abilities) + ';';
fs.writeFileSync('play.pokemonshowdown.com/data/abilities.js', buf);
}

View File

@@ -12,6 +12,9 @@ const TEXT_DATA_PATH = path.resolve(ROOT_PATH, 'caches/pokemon-showdown/data/tex
const UI_TRANSLATIONS_PATH = path.resolve(ROOT_PATH, 'translations');
const OUTPUT_PATH = path.resolve(ROOT_PATH, 'play.pokemonshowdown.com/data/text');
/** Mods the client ships data for (see build-indexes) */
const CLIENT_MODS = ['gen3rs', 'gen3frlg', 'gen5bw1', 'gen7letsgo', 'gen8bdsp', 'gen9ssb', 'champions'];
function es3stringify(obj) {
const buf = JSON.stringify(obj);
return buf.replace(/"([A-Za-z][A-Za-z0-9]*)":/g, (fullMatch, key) => (
@@ -32,12 +35,30 @@ function getLanguages() {
return [...languages];
}
function addModText(Dex, text) {
for (const mod of CLIENT_MODS) {
const modData = Dex.mod(mod).data;
for (const table of ['Moves', 'Abilities', 'Items']) {
for (const id in modData[table]) {
const { desc, shortDesc } = modData[table][id];
if (!desc && !shortDesc) continue;
text[table][id] = {
...text[table][id],
[mod]: { desc: desc || shortDesc, shortDesc: shortDesc || desc },
};
}
}
}
return text;
}
function buildTextFiles(Dex, selectedLanguages = null, options = {}) {
const uiCalls = updateTranslationFiles(options);
const languages = selectedLanguages || getLanguages();
fs.mkdirSync(OUTPUT_PATH, { recursive: true });
for (const lang of languages) {
const battleTranslations = Dex.loadTextData(lang);
let battleTranslations = Dex.loadTextData(lang);
if (lang === 'en') battleTranslations = addModText(Dex, JSON.parse(JSON.stringify(battleTranslations)));
const uiText = compileBattleUIText(loadTranslations(lang), uiCalls);
let output = 'exports.BattleText = exports.BattleText || {};\n' +
`exports.BattleText[${JSON.stringify(lang)}] = ${es3stringify(battleTranslations)};`;
@@ -50,6 +71,7 @@ function buildTextFiles(Dex, selectedLanguages = null, options = {}) {
}
exports.buildTextFiles = buildTextFiles;
exports.CLIENT_MODS = CLIENT_MODS;
if (require.main === module) {
const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex;

View File

@@ -577,7 +577,7 @@ export class DexSearch {
switch (fType) {
case 'type':
let type = fId.charAt(0).toUpperCase() + fId.slice(1) as Dex.TypeName;
const typeName = TL.type[fId] || type;
const typeName = TL.type[type] || type;
buf.push(['header', TL`${typeName}-type Pokémon`]);
for (let id in BattlePokedex) {
if (!BattlePokedex[id].types) continue;
@@ -602,7 +602,7 @@ export class DexSearch {
switch (fType) {
case 'type':
let type = fId.charAt(0).toUpperCase() + fId.slice(1);
const typeName = TL.type[fId] || type;
const typeName = TL.type[type] || type;
buf.push(['header', TL`${typeName}-type moves`]);
for (let id in BattleMovedex) {
if (BattleMovedex[id].type === type) {

View File

@@ -137,12 +137,6 @@ interface ClientDexText {
languages(): Language[];
findLanguage(lang: string): Language | null;
get(effect: TranslatableEffect, lang?: string): ClientEffectTextEntry;
typeName(name: string, lang?: string): string;
natureName(name: string, lang?: string): string;
categoryName(name: string, lang?: string): string;
genderName(name: string, lang?: string): string;
eggGroupName(name: string, lang?: string): string;
colorName(name: string, lang?: string): string;
}
function translate(strings: TemplateStringsArray, ...values: unknown[]): string;
@@ -270,17 +264,13 @@ type OtherNameTable =
'StatNames' | 'StatMediumNames' | 'StatShortNames';
function getOtherName(table: OtherNameTable, name: string, lang: string): string {
let id: string = toID(name);
if (table === 'GenderNames') {
id = ({ m: 'male', f: 'female', n: 'genderless' } as Record<string, string>)[id] || id;
}
return BattleText[lang]?.[table]?.[id] || BattleText.en?.[table]?.[id] || name;
return BattleText[lang]?.[table]?.[name] || BattleText.en?.[table]?.[name] || name;
}
/**
* Does actually match ClientEffectTextEntry exactly.
*/
function getTextEntry(effect: TranslatableEffect, gen: number, lang: string): ClientEffectTextEntry {
function getTextEntry(effect: TranslatableEffect, modid: string, gen: number, lang: string): ClientEffectTextEntry {
if (effect.effectType === 'Species') {
const entry = BattleText[lang]?.Pokedex?.[effect.id] || BattleText.en?.Pokedex?.[effect.id];
return {
@@ -301,7 +291,7 @@ function getTextEntry(effect: TranslatableEffect, gen: number, lang: string): Cl
const entry = {} as ClientEffectTextEntry;
assignTextFields(entry, english);
assignTextFields(entry, localized);
for (let i = 1; i <= 8; i++) {
for (let i = 1; i < Dex.gen; i++) {
const genName = `gen${i}`;
const englishGen = english[genName];
const localizedGen = localized[genName];
@@ -312,22 +302,22 @@ function getTextEntry(effect: TranslatableEffect, gen: number, lang: string): Cl
entry[genName] = genEntry;
}
}
for (let i = 8; i >= gen; i--) {
for (let i = Dex.gen - 1; i >= gen; i--) {
const genName = `gen${i}`;
const englishGen = english[genName];
const localizedGen = localized[genName];
if (englishGen && typeof englishGen === 'object') assignTextFields(entry, englishGen);
if (localizedGen && typeof localizedGen === 'object') assignTextFields(entry, localizedGen);
}
const fallback = effect as unknown as { desc?: string, shortDesc?: string };
if (!/^gen\d+$/.test(modid)) {
const englishMod = english[modid];
const localizedMod = localized[modid];
if (englishMod && typeof englishMod === 'object') assignTextFields(entry, englishMod);
if (localizedMod && typeof localizedMod === 'object') assignTextFields(entry, localizedMod);
}
if (typeof entry.name !== 'string') entry.name = effect.name;
if (typeof entry.desc !== 'string') {
entry.desc = fallback.desc || fallback.shortDesc ||
(typeof entry.shortDesc === 'string' ? entry.shortDesc : '');
}
if (typeof entry.shortDesc !== 'string') {
entry.shortDesc = fallback.shortDesc || fallback.desc || entry.desc;
}
if (typeof entry.desc !== 'string') entry.desc = typeof entry.shortDesc === 'string' ? entry.shortDesc : '';
if (typeof entry.shortDesc !== 'string') entry.shortDesc = entry.desc;
return entry;
}
@@ -619,15 +609,8 @@ export const Dex = new class implements ModdedDex {
return TEXT_LANGUAGE_TABLE[lang.toLowerCase()] || TEXT_LANGUAGE_TABLE[toID(lang)] || null;
},
get: (effect: TranslatableEffect, lang = Dex.text.getLanguage()) => {
return getTextEntry(effect, 9, lang);
return getTextEntry(effect, 'gen9', 9, lang);
},
typeName: (name, lang = Dex.text.getLanguage()) => getOtherName('TypeNames', name, lang),
natureName: (name, lang = Dex.text.getLanguage()) => getOtherName('NatureNames', name, lang),
categoryName: (name, lang = Dex.text.getLanguage()) =>
BattleText[lang]?.Tags?.[toID(name)]?.name || BattleText.en?.Tags?.[toID(name)]?.name || name,
genderName: (name, lang = Dex.text.getLanguage()) => getOtherName('GenderNames', name, lang),
eggGroupName: (name, lang = Dex.text.getLanguage()) => getOtherName('EggGroupNames', name, lang),
colorName: (name, lang = Dex.text.getLanguage()) => getOtherName('ColorNames', name, lang),
};
getShortName(name: string) {
@@ -1252,7 +1235,7 @@ export const Dex = new class implements ModdedDex {
type = this.types.get(type).name;
if (!type) type = '???';
let sanitizedType = type.replace(/\?/g, '%3f');
const alt = BattleLog.escapeHTML(TL.type[toID(type)] || type);
const alt = BattleLog.escapeHTML(TL.type[type] || type);
return `<img src="${Dex.resourcePrefix}sprites/types/${sanitizedType}.png" alt="${alt}" height="14" width="32" class="pixelated${b ? ' b' : ''}" />`;
}
@@ -1309,15 +1292,8 @@ export class ModdedDex {
languages: () => Dex.text.languages(),
findLanguage: lang => Dex.text.findLanguage(lang),
get: (effect: TranslatableEffect, lang = Dex.text.getLanguage()) => {
return getTextEntry(effect, this.gen, lang);
return getTextEntry(effect, this.modid, this.gen, lang);
},
typeName: (name, lang = Dex.text.getLanguage()) => getOtherName('TypeNames', name, lang),
natureName: (name, lang = Dex.text.getLanguage()) => getOtherName('NatureNames', name, lang),
categoryName: (name, lang = Dex.text.getLanguage()) =>
BattleText[lang]?.Tags?.[toID(name)]?.name || BattleText.en?.Tags?.[toID(name)]?.name || name,
genderName: (name, lang = Dex.text.getLanguage()) => getOtherName('GenderNames', name, lang),
eggGroupName: (name, lang = Dex.text.getLanguage()) => getOtherName('EggGroupNames', name, lang),
colorName: (name, lang = Dex.text.getLanguage()) => getOtherName('ColorNames', name, lang),
};
moves = {
get: (name: string): Move => {

View File

@@ -33,9 +33,9 @@ function getSearchDisplayName(search: DexSearch, type: string, name: string): st
case 'move': return search.dex.text.get(search.dex.moves.get(name)).name;
case 'item': return search.dex.text.get(search.dex.items.get(name)).name;
case 'ability': return search.dex.text.get(search.dex.abilities.get(name)).name;
case 'type': return search.dex.text.typeName(name);
case 'category': return search.dex.text.categoryName(name);
case 'egggroup': return search.dex.text.eggGroupName(name);
case 'type': return TL.type[name] || name;
case 'category': return TL.tag[toID(name)] || name;
case 'egggroup': return TL.egggroup[name] || name;
default: return name;
}
}
@@ -127,7 +127,7 @@ export class PSSearchResults extends preact.Component<{
if (errorMessage) return `${buf}${errorMessage}</a></li>`;
buf += `<span class="col typecol">${pokemon.types.map(type =>
`<img src="${Dex.resourcePrefix}sprites/types/${type}.png" alt="${escapeHTML(search.dex.text.typeName(type))}" height="14" width="32" class="pixelated" />`
`<img src="${Dex.resourcePrefix}sprites/types/${type}.png" alt="${escapeHTML(TL.type[type] || type)}" height="14" width="32" class="pixelated" />`
).join('')}</span>`;
if (search.numAbilityCols) {
@@ -269,9 +269,9 @@ export class PSSearchResults extends preact.Component<{
}
buf += `<span class="col typecol">` +
`<img src="${Dex.resourcePrefix}sprites/types/${encodeURIComponent(move.type)}.png" ` +
`alt="${escapeHTML(search.dex.text.typeName(move.type))}" height="14" width="32" class="pixelated" />` +
`alt="${escapeHTML(TL.type[move.type] || move.type)}" height="14" width="32" class="pixelated" />` +
`<img src="${Dex.resourcePrefix}sprites/categories/${escapeHTML(move.category)}.png" ` +
`alt="${escapeHTML(search.dex.text.categoryName(move.category))}" height="14" width="32" class="pixelated" />` +
`alt="${escapeHTML(TL.tag[toID(move.category)] || move.category)}" height="14" width="32" class="pixelated" />` +
`</span>` +
`<span class="col labelcol">${move.category !== 'Status' ? `<em>Power</em><br />${move.basePower || '&mdash;'}` : ''}</span>` +
`<span class="col widelabelcol"><em>Accuracy</em><br />` +
@@ -283,7 +283,7 @@ export class PSSearchResults extends preact.Component<{
renderTypeRowHTML(index: number, id: ID, matchStart: number, matchEnd: number, errorMessage?: string) {
const name = id.charAt(0).toUpperCase() + id.slice(1);
const displayName = this.props.search.dex.text.typeName(name);
const displayName = TL.type[name] || name;
[matchStart, matchEnd] = getLocalizedMatch(displayName, name, matchStart, matchEnd);
return `<li class="result" value="${index}"><a href="${this.URL_ROOT}types/${id}" ` +
@@ -297,7 +297,7 @@ export class PSSearchResults extends preact.Component<{
renderCategoryRowHTML(index: number, id: ID, matchStart: number, matchEnd: number, errorMessage?: string) {
const name = id.charAt(0).toUpperCase() + id.slice(1);
const displayName = this.props.search.dex.text.categoryName(name);
const displayName = TL.tag[id] || name;
[matchStart, matchEnd] = getLocalizedMatch(displayName, name, matchStart, matchEnd);
return `<li class="result" value="${index}"><a href="${this.URL_ROOT}categories/${id}" ` +
@@ -333,7 +333,7 @@ export class PSSearchResults extends preact.Component<{
} else {
name = id.charAt(0).toUpperCase() + id.slice(1);
}
const displayName = this.props.search.dex.text.eggGroupName(name);
const displayName = TL.egggroup[name] || name;
[matchStart, matchEnd] = getLocalizedMatch(displayName, name, matchStart, matchEnd);
return `<li class="result" value="${index}"><a href="${this.URL_ROOT}egggroups/${id}" ` +

View File

@@ -1063,7 +1063,7 @@ export class TeamEditor extends preact.Component<{
const good = [], medium = [], bad = [];
const renderTypeDefensive = (counter: typeof counters[number]) => (
<tr>
<th>{editor.dex.text.typeName(counter.type)}</th>
<th>{TL.type[counter.type] || counter.type}</th>
<td>{counter.resists} <small class="gray">{TL`Resist`}</small></td>
<td>{counter.weaknesses} <small class="gray">{TL`Weak`}</small></td>
</tr>
@@ -1802,7 +1802,7 @@ class TeamTextbox extends preact.Component<{
const genderID = (set.gender || species.gender || 'N') as Dex.GenderName;
// em dash
const gender = genderID === 'N' ? '\u2014' : editor.dex.text.genderName(genderID);
const gender = genderID === 'N' ? '\u2014' : TL.gender[genderID] || genderID;
return <button class="textbox setdetails" name="details" value={i} onClick={this.clickDetails}>
<span class="detailcell">
@@ -3933,10 +3933,10 @@ class DetailsForm extends preact.Component<{
this.props.onChange();
};
renderGender(gender: Dex.GenderName) {
if (gender === 'N') return this.props.editor.dex.text.genderName(gender);
if (gender === 'N') return TL.gender[gender] || gender;
return <>
<PSIcon gender={gender} /> {}
{this.props.editor.dex.text.genderName(gender)}
{TL.gender[gender] || gender}
</>;
}
render() {
@@ -4023,7 +4023,7 @@ class DetailsForm extends preact.Component<{
>
{Dex.types.all().map(type => (
type.HPivs && <option value={type.name}>
{editor.dex.text.typeName(type.name)}
{TL.type[type.name] || type.name}
</option>
))}
</select></label>

View File

@@ -916,7 +916,7 @@ export class BattleTooltips {
let genderBuf = '';
const gender = pokemon.gender;
if (gender === 'M' || gender === 'F') {
const genderName = TL.gender[gender === 'M' ? 'male' : 'female'] || gender;
const genderName = TL.gender[gender] || gender;
genderBuf = ` <img src="${Dex.fxPrefix}gender-${gender.toLowerCase()}.png" alt="${BattleLog.escapeHTML(genderName)}" width="7" height="10" class="pixelated" /> `;
}
@@ -1053,7 +1053,7 @@ export class BattleTooltips {
// Only display when you don't also have stats
if (clientPokemon?.nature && !serverPokemon) {
const nature = TL.nature[toID(clientPokemon.nature)] || clientPokemon.nature;
const nature = TL.nature[clientPokemon.nature] || clientPokemon.nature;
let natureText = `<small>${TL.label(TL`Nature`)}</small>` + nature;
text += `<p>${natureText}</p>`;
}

View File

@@ -189,6 +189,11 @@
// These all have static versions
/** The format's dex (for gen-specific text), or the current-gen dex if there's no engine yet */
Search.prototype.dex = function () {
return this.engine ? this.engine.dex : Dex;
};
Search.prototype.renderRow = function (id, type, matchStart, matchLength, errorMessage, attrs) {
// errorMessage = '<span class="col illegalcol"><em>' + errorMessage + '</em></span>';
switch (type) {
@@ -496,7 +501,7 @@
}
// desc
buf += '<span class="col itemdesccol">' + BattleLog.escapeHTML(item.shortDesc) + '</span> ';
buf += '<span class="col itemdesccol">' + BattleLog.escapeHTML(this.dex().text.get(item).shortDesc) + '</span> ';
buf += '</a></li>';
@@ -522,7 +527,7 @@
return buf;
}
buf += '<span class="col abilitydesccol">' + BattleLog.escapeHTML(ability.shortDesc) + '</span> ';
buf += '<span class="col abilitydesccol">' + BattleLog.escapeHTML(this.dex().text.get(ability).shortDesc) + '</span> ';
buf += '</a></li>';
@@ -579,7 +584,7 @@
buf += '<span class="col pplabelcol"><em>PP</em><br />' + pp + '</span> ';
// desc
buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(move.shortDesc) + '</span> ';
buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(this.dex().text.get(move).shortDesc) + '</span> ';
buf += '</a></li>';
@@ -620,7 +625,7 @@
buf += '<span class="col pplabelcol"><em>PP</em><br />' + pp + '</span> ';
// desc
buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(move.shortDesc || move.desc) + '</span> ';
buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(this.dex().text.get(move).shortDesc) + '</span> ';
buf += '</a>';
@@ -663,7 +668,7 @@
buf += '<span class="col pplabelcol"><em>PP</em><br />' + pp + '</span> ';
// desc
buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(move.shortDesc || move.desc) + '</span> ';
buf += '<span class="col movedesccol">' + BattleLog.escapeHTML(this.dex().text.get(move).shortDesc) + '</span> ';
buf += '</a></li>';

View File

@@ -1731,33 +1731,33 @@ export function PSIcon(
}
const sanitizedType = typeName.replace(/\?/g, '%3f');
return <img
src={`${Dex.resourcePrefix}sprites/types/${sanitizedType}.png`} alt={Dex.text.typeName(typeName)}
src={`${Dex.resourcePrefix}sprites/types/${sanitizedType}.png`} alt={TL.type[typeName] || typeName}
height="14" width="32" class={`pixelated${props.b ? ' b' : ''}`} style="vertical-align:middle"
/>;
}
if ('category' in props) {
const categoryID = toID(props.category);
let sanitizedCategory = '';
let categoryName = '';
switch (categoryID) {
case 'physical':
case 'special':
case 'status':
sanitizedCategory = categoryID.charAt(0).toUpperCase() + categoryID.slice(1);
categoryName = categoryID.charAt(0).toUpperCase() + categoryID.slice(1);
break;
default:
sanitizedCategory = 'undefined';
categoryName = 'undefined';
break;
}
return <img
src={`${Dex.resourcePrefix}sprites/categories/${sanitizedCategory}.png`}
alt={Dex.text.categoryName(sanitizedCategory)}
src={`${Dex.resourcePrefix}sprites/categories/${categoryName}.png`}
alt={TL.tag[categoryID] || categoryName}
height="14" width="32" class="pixelated" style="vertical-align:middle"
/>;
}
if ('gender' in props) {
return <img
src={`${Dex.resourcePrefix}sprites/misc/gender-${props.gender.toLowerCase()}.png`}
width={18} height={18} alt={Dex.text.genderName(props.gender)} style="margin-top: -1px; filter: grayscale(30%)"
width={18} height={18} alt={TL.gender[props.gender] || props.gender} style="margin-top: -1px; filter: grayscale(30%)"
/>;
}
return null!;