Preact minor updates batch 47
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled

- Show a "Language room" section for the current language, above
  all other chatrooms.
- Cachebust translations
- Properly fall back to English when translations don't exist
- Fix an oldclient translation regression
- Fix Options popup width bouncing around because of the overlay's
  scrollbar
- Support better article contraction for European languages
- Fix topbar overflowing on mobile layout
- Bump cookie expiration
This commit is contained in:
Guangcong Luo
2026-09-02 04:18:57 +00:00
parent 122b015ff6
commit eeaec53202
12 changed files with 111 additions and 35 deletions

View File

@@ -52,9 +52,23 @@ try {
} catch {}
const routes = JSON.parse(fs.readFileSync('config/routes.json'));
const translationCachebuster = (() => {
try {
const textDir = 'play.pokemonshowdown.com/data/text/';
const hash = crypto.createHash('md5');
for (const textFile of fs.readdirSync(textDir).filter(file => file.endsWith('.js')).sort()) {
hash.update(textFile);
hash.update(fs.readFileSync(textDir + textFile));
}
return hash.digest('hex').slice(0, 8);
} catch {
return '';
}
})();
const autoconfigRegex = new RegExp(`${escapeRegex(AUTOCONFIG_START)}[^]+${escapeRegex(AUTOCONFIG_END)}`);
const autoconfig = `${AUTOCONFIG_START}
Config.version = ${JSON.stringify(version)};
Config.translationCachebuster = ${JSON.stringify(translationCachebuster)};
Config.routes = {
root: '${routes.root}',

View File

@@ -242,22 +242,23 @@ export const TL = Object.assign(translate, {
function updateTranslatedNames(lang: string) {
if (lang !== Dex.text.getLanguage()) return;
const text = BattleText[lang];
if (!text) return;
if (typeof BattleText === 'undefined') return;
const english = BattleText.en;
TL.term = text.TermNames || english.TermNames;
TL.type = text.TypeNames || english.TypeNames;
TL.nature = text.NatureNames || english.NatureNames;
TL.gender = text.GenderNames || english.GenderNames;
TL.egggroup = text.EggGroupNames || english.EggGroupNames;
TL.tag = tagField(text.Tags || english.Tags, 'name');
TL.tagHint = tagField(text.Tags || english.Tags, 'hint');
TL.color = text.ColorNames || english.ColorNames;
TL.status = text.StatusNames || english.StatusNames;
TL.target = text.TargetNames || english.TargetNames;
TL.stat = text.StatNames || english.StatNames;
TL.statShort = text.StatShortNames || english.StatShortNames;
TL.statMedium = text.StatMediumNames || english.StatMediumNames;
const text = BattleText[lang] || english;
if (!text) return;
TL.term = text.TermNames || english?.TermNames || {};
TL.type = text.TypeNames || english?.TypeNames || {};
TL.nature = text.NatureNames || english?.NatureNames || {};
TL.gender = text.GenderNames || english?.GenderNames || {};
TL.egggroup = text.EggGroupNames || english?.EggGroupNames || {};
TL.tag = tagField(text.Tags || english?.Tags, 'name');
TL.tagHint = tagField(text.Tags || english?.Tags, 'hint');
TL.color = text.ColorNames || english?.ColorNames || {};
TL.status = text.StatusNames || english?.StatusNames || {};
TL.target = text.TargetNames || english?.TargetNames || {};
TL.stat = text.StatNames || english?.StatNames || {};
TL.statShort = text.StatShortNames || english?.StatShortNames || {};
TL.statMedium = text.StatMediumNames || english?.StatMediumNames || {};
}
function assignTextFields(target: BattleTextEntry, source: BattleTextEntry) {
@@ -875,8 +876,8 @@ export const Dex = new class implements ModdedDex {
}
loadTextData(lang = this.text.getLanguage()): Promise<void> {
const text = typeof BattleText === 'undefined' ? undefined : BattleText;
updateTranslatedNames(lang);
if (text?.[lang]) {
updateTranslatedNames(lang);
return Promise.resolve();
}
// in case the initial English failed to load
@@ -892,12 +893,17 @@ export const Dex = new class implements ModdedDex {
el.onerror = () => reject(new Error(`Failed to load text data from ${src}`));
document.getElementsByTagName('body')[0].appendChild(el);
});
let loading = loadScript(Config.testclient ? `data/text/${lang}.js` : `${this.resourcePrefix}data/text/${lang}.js`);
const cachebuster = Config.translationCachebuster ? `?${Config.translationCachebuster}` : '';
const textURL = Config.testclient ? `data/text/${lang}.js` : `${this.resourcePrefix}data/text/${lang}.js`;
let loading = loadScript(textURL + cachebuster);
if (Config.testclient) {
loading = loading.catch(() => loadScript(`https://play.pokemonshowdown.com/data/text/${lang}.js`));
loading = loading.catch(() =>
loadScript(`https://play.pokemonshowdown.com/data/text/${lang}.js${cachebuster}`)
);
}
loading = loading.then(() => updateTranslatedNames(lang)).catch(() => {
delete this.loadedTextData[lang];
updateTranslatedNames(lang);
});
this.loadedTextData[lang] = loading;
return loading;

View File

@@ -426,7 +426,13 @@ export class BattleTextParser {
if (language === 'fr') {
let article = '';
if (has('definite')) {
const lead = (has('a') || has('de')) && !has('definite') && !has('indefinite') &&
/^(\*\*)?(le |la |les |l)/i.exec(value);
if (lead) {
// detect article for "de les" -> "des" etc
article = lead[2].toLowerCase();
value = (lead[1] || '') + value.slice(lead[0].length);
} else if (has('definite')) {
article = plural ? 'les ' : vowel ? 'l' : feminine ? 'la ' : 'le ';
} else if (has('indefinite')) {
article = uncountable ? '' : plural ? 'des ' : feminine ? 'une ' : 'un ';
@@ -446,7 +452,13 @@ export class BattleTextParser {
} else if (language === 'es') {
const articleFeminine = feminine && articleRule !== 'stressed-a';
let article = '';
if (has('definite')) article = plural ? (feminine ? 'las ' : 'los ') : (articleFeminine ? 'la ' : 'el ');
const lead = (has('a') || has('de')) && !has('definite') && !has('indefinite') &&
/^(\*\*)?(el |la |los |las )/i.exec(value);
if (lead) {
// detect article for "de el" -> "del" etc
article = lead[2].toLowerCase();
value = (lead[1] || '') + value.slice(lead[0].length);
} else if (has('definite')) article = plural ? (feminine ? 'las ' : 'los ') : (articleFeminine ? 'la ' : 'el ');
else if (has('indefinite')) {
article = uncountable ? '' : plural ? (feminine ? 'unas ' : 'unos ') : (articleFeminine ? 'una ' : 'un ');
}
@@ -457,7 +469,15 @@ export class BattleTextParser {
} else if (language === 'it') {
const special = /^(?:s[^aeiouàèéìòù]|z|gn|ps|pn|x|y)/i.test(initial);
let article = '';
if (has('definite')) {
let hasDefinite = has('definite');
const lead = (has('a') || has('di') || has('su')) && !has('definite') && !has('indefinite') &&
/^(\*\*)?(il |lo |la |i |gli |le |l)/i.exec(value);
if (lead) {
// detect article for "su la" -> "sulla" etc
article = lead[2].toLowerCase();
value = (lead[1] || '') + value.slice(lead[0].length);
hasDefinite = true;
} else if (has('definite')) {
if (plural) article = feminine ? 'le ' : (vowel || special ? 'gli ' : 'i ');
else if (vowel) article = 'l';
else article = feminine ? 'la ' : (special ? 'lo ' : 'il ');
@@ -466,11 +486,11 @@ export class BattleTextParser {
else if (feminine) article = vowel ? 'un' : 'una ';
else article = special ? 'uno ' : 'un ';
}
if (has('a') && has('definite')) {
if (has('a') && hasDefinite) {
prefix = this.italianContraction(article, ['al ', 'allo ', 'all', 'alla ', 'ai ', 'agli ', 'alle ']);
} else if (has('di') && has('definite')) {
} else if (has('di') && hasDefinite) {
prefix = this.italianContraction(article, ['del ', 'dello ', 'dell', 'della ', 'dei ', 'degli ', 'delle ']);
} else if (has('su') && has('definite')) {
} else if (has('su') && hasDefinite) {
prefix = this.italianContraction(article, ['sul ', 'sullo ', 'sull', 'sulla ', 'sui ', 'sugli ', 'sulle ']);
} else if (has('a')) {
prefix = vowel ? 'ad ' : 'a ';
@@ -526,7 +546,7 @@ export class BattleTextParser {
if (!text) return 0;
const code = text.charCodeAt(text.length - 1);
if (code >= 0xAC00 && code <= 0xD7A3) return (code - 0xAC00) % 28;
if (code >= 0x30 && code <= 0x39) return [1, 0, 0, 8, 0, 0, 1, 8, 8, 0][code - 0x30];
if (code >= 0x30 && code <= 0x39) return [1, 8, 0, 1, 0, 0, 1, 8, 8, 0][code - 0x30];
return /[lmnr]$/i.test(text) ? 8 : 0;
}
@@ -1374,10 +1394,12 @@ export class BattleTextParser {
if (!kwArgs.from) {
template = this.template(percentage ? 'damagePercentage' : 'damage');
percentage = percentage ? percentage.replace(/%$/, '') : '';
return line1 + this.render(template, {
percentage = percentage ? percentage.replace(/%(\|\|)?$/, '$1') : '';
const message = this.render(template, {
POKEMON: this.pokemon(pokemon), PERCENTAGE: percentage,
});
// move % sign inside <abbr>
return line1 + message.replace(/\|\|([%])/, '$1||');
}
if (kwArgs.from.startsWith('item:')) {
template = this.template(kwArgs.of ? 'damageFromPokemon' : 'damageFromItem');

View File

@@ -55,6 +55,7 @@ export interface PSConfig {
teams: string,
};
customcolors: Record<string, string>;
translationCachebuster?: string;
whitelist?: string[];
testclient?: boolean;
}

View File

@@ -428,6 +428,7 @@ function toId() {
this.addRoom('lobby', null, true);
}
Storage.whenPrefsLoaded(function () {
Dex.loadTextData();
if (!Config.server.registered) {
app.send('/autojoin');
Backbone.history.start({ pushState: !Config.testclient });

View File

@@ -523,9 +523,9 @@ class NewsPanel extends PSRoomPanel {
};
setClient(setting: '0' | '1' | 'leave') {
if (setting === '1') {
document.cookie = "preactalpha=1; expires=Thu, 1 Sep 2026 12:00:00 UTC; path=/";
document.cookie = "preactalpha=1; expires=Thu, 1 Dec 2026 12:00:00 UTC; path=/";
} else if (setting === '0') {
document.cookie = "preactalpha=0; expires=Thu, 1 Sep 2026 12:00:00 UTC; path=/";
document.cookie = "preactalpha=0; expires=Thu, 1 Dec 2026 12:00:00 UTC; path=/";
} else {
document.cookie = "preactalpha=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}

View File

@@ -711,7 +711,7 @@ class OptionsPanel extends PSRoomPanel {
const singlePanel = TL`Single panel`;
const verticalTabs = TL`Vertical tabs`;
const automaticLanguage = Dex.text.findLanguage(Dex.text.getBrowserLanguage())?.name;
return <PSPanelWrapper room={room} width={340}><div class="pad">
return <PSPanelWrapper room={room} width={380}><div class="pad">
<p style="padding-left:50px">
<img
class="trainersprite yours" width="40" height="40" style={{ float: 'left', marginLeft: '-50px' }}

View File

@@ -10,6 +10,20 @@ import { PSPanelWrapper, PSRoomPanel } from "./panels";
import type { RoomInfo } from "./panel-mainmenu";
import { Dex, TL, toID } from "./battle-dex";
const LANGUAGE_ROOM_IDS: Record<string, readonly string[]> = {
it: ['italiano'],
es: ['espanol', 'espaol'],
'zh-cn': ['chinese'],
'zh-tw': ['chinese'],
hi: ['hindi'],
fr: ['franais', 'francais'],
pt: ['portugus', 'portugues'],
ja: ['japanese'],
nl: ['nederlands'],
de: ['deutsche'],
ko: ['korean'],
};
export class RoomsRoom extends PSRoom {
override readonly classType: string = 'rooms';
constructor(options: RoomOptions) {
@@ -127,10 +141,16 @@ class RoomsPanel extends PSRoomPanel {
if (!searchid) {
const roomsCache = PS.mainmenu.roomsCache;
let spotLightLabel = '';
const officialRooms = [], chatRooms = [], hiddenRooms = [], spotLightRooms = [];
const languageRooms = [], officialRooms = [], chatRooms = [], hiddenRooms = [], spotLightRooms = [];
const languageRoomIDs = LANGUAGE_ROOM_IDS[Dex.text.getLanguage()] || [];
const languageRoom = languageRoomIDs.map(roomid =>
roomsCache.chat?.find(room => room.id === roomid || toID(room.title) === roomid)
).find(room => !!room && room.privacy !== 'hidden');
for (const room of roomsCache.chat || []) {
if (room.section !== this.section && this.section !== '') continue;
if (room.privacy === 'hidden') {
if (room === languageRoom) {
languageRooms.push(room);
} else if (room.privacy === 'hidden') {
hiddenRooms.push(room);
} else if (room.spotlight) {
spotLightLabel = room.spotlight;
@@ -142,6 +162,7 @@ class RoomsPanel extends PSRoomPanel {
}
}
return [
[TL`Language room`, languageRooms],
[TL`Official chat rooms`, officialRooms],
[spotLightLabel, spotLightRooms],
[TL`Chat rooms`, chatRooms],

View File

@@ -366,10 +366,10 @@ export class PSMiniHeader extends preact.Component {
);
return <div class="mini-header" style={`left:${PSView.verticalHeaderWidth + (PSView.narrowMode ? 0 : -1)}px;`}>
{menuButton}
{icon} {title}
<button data-href="options" class="mini-header-right" aria-label={TL`[Options]`}>
{PS.user.named ? <strong style={userColor}>{PS.user.name}</strong> : <i class="fa fa-cog" aria-hidden></i>}
</button>
{icon} {title}
</div>;
}
}

View File

@@ -1585,8 +1585,11 @@ export class PSView extends preact.Component {
const isFixed = room.location !== 'popup';
const offsetLeft = isFixed || this.useScrollFrame() ? 0 : window.scrollX;
const offsetTop = isFixed ? 0 : window.scrollY;
const availableWidth = document.documentElement.clientWidth + offsetLeft;
const availableHeight = document.documentElement.clientHeight;
// overlay might have a scrollbar, which changes the available space
const overlay = isFixed ? document.getElementById(`room-${room.id}`)?.parentElement : null;
const availableWidth = (overlay?.clientWidth || document.documentElement.clientWidth) + offsetLeft;
const availableHeight = overlay?.clientHeight || document.documentElement.clientHeight;
const sourceWidth = source.width;
const sourceHeight = source.height;

View File

@@ -225,6 +225,7 @@ li::marker {
padding: 0;
height: 29px;
line-height: 29px;
white-space: nowrap;
border-left: 0;
border-right: 0;
border-top: 0;
@@ -264,6 +265,12 @@ li::marker {
border-width: 0 1px 0 0;
margin-right: 5px;
}
.mini-header-right {
position: absolute;
top: 0;
right: 0;
background: inherit;
}
.mini-header-left.notifying {
color: #AA6600;
}

View File

@@ -287,6 +287,7 @@ export const translations: UIText = {
"Meloetta is PS's mascot! The Aria forme is about using its voice, and represents our chatrooms.": null,
"Meloetta is PS's mascot! The Pirouette forme is Fighting-type, and represents our battles.": null,
"Language room": null,
"Official chat rooms": null,
"Hidden rooms": null,