From eeaec53202425be20761198da837f5fb9b264c51 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 2 Sep 2026 04:18:57 +0000 Subject: [PATCH] Preact minor updates batch 47 - 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 --- build-tools/update | 14 +++++++ play.pokemonshowdown.com/src/battle-dex.ts | 42 +++++++++++-------- .../src/battle-text-parser.ts | 40 ++++++++++++++---- play.pokemonshowdown.com/src/client-main.ts | 1 + .../src/oldclient/client.js | 1 + .../src/panel-mainmenu.tsx | 4 +- play.pokemonshowdown.com/src/panel-popups.tsx | 2 +- play.pokemonshowdown.com/src/panel-rooms.tsx | 25 ++++++++++- play.pokemonshowdown.com/src/panel-topbar.tsx | 2 +- play.pokemonshowdown.com/src/panels.tsx | 7 +++- play.pokemonshowdown.com/style/client2.css | 7 ++++ translations/en-template.ts | 1 + 12 files changed, 111 insertions(+), 35 deletions(-) diff --git a/build-tools/update b/build-tools/update index aeb83e464..06686a967 100755 --- a/build-tools/update +++ b/build-tools/update @@ -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}', diff --git a/play.pokemonshowdown.com/src/battle-dex.ts b/play.pokemonshowdown.com/src/battle-dex.ts index c0546c1d3..1f2203dde 100644 --- a/play.pokemonshowdown.com/src/battle-dex.ts +++ b/play.pokemonshowdown.com/src/battle-dex.ts @@ -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 { 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; diff --git a/play.pokemonshowdown.com/src/battle-text-parser.ts b/play.pokemonshowdown.com/src/battle-text-parser.ts index a0ab7e22a..6088cbec7 100644 --- a/play.pokemonshowdown.com/src/battle-text-parser.ts +++ b/play.pokemonshowdown.com/src/battle-text-parser.ts @@ -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 + return line1 + message.replace(/\|\|([%%])/, '$1||'); } if (kwArgs.from.startsWith('item:')) { template = this.template(kwArgs.of ? 'damageFromPokemon' : 'damageFromItem'); diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index 1a7a8fd4e..47c07edd7 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -55,6 +55,7 @@ export interface PSConfig { teams: string, }; customcolors: Record; + translationCachebuster?: string; whitelist?: string[]; testclient?: boolean; } diff --git a/play.pokemonshowdown.com/src/oldclient/client.js b/play.pokemonshowdown.com/src/oldclient/client.js index 6f5c99a58..ecfba4535 100644 --- a/play.pokemonshowdown.com/src/oldclient/client.js +++ b/play.pokemonshowdown.com/src/oldclient/client.js @@ -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 }); diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 263791310..c96a4aaa1 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -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=/;"; } diff --git a/play.pokemonshowdown.com/src/panel-popups.tsx b/play.pokemonshowdown.com/src/panel-popups.tsx index 1382a43ba..c46956325 100644 --- a/play.pokemonshowdown.com/src/panel-popups.tsx +++ b/play.pokemonshowdown.com/src/panel-popups.tsx @@ -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
+ return

= { + 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], diff --git a/play.pokemonshowdown.com/src/panel-topbar.tsx b/play.pokemonshowdown.com/src/panel-topbar.tsx index ad8acc6f2..06f18bdb5 100644 --- a/play.pokemonshowdown.com/src/panel-topbar.tsx +++ b/play.pokemonshowdown.com/src/panel-topbar.tsx @@ -366,10 +366,10 @@ export class PSMiniHeader extends preact.Component { ); return

{menuButton} - {icon} {title} + {icon} {title}
; } } diff --git a/play.pokemonshowdown.com/src/panels.tsx b/play.pokemonshowdown.com/src/panels.tsx index 98c790fac..e94107210 100644 --- a/play.pokemonshowdown.com/src/panels.tsx +++ b/play.pokemonshowdown.com/src/panels.tsx @@ -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; diff --git a/play.pokemonshowdown.com/style/client2.css b/play.pokemonshowdown.com/style/client2.css index efd3bad26..af0e66c05 100644 --- a/play.pokemonshowdown.com/style/client2.css +++ b/play.pokemonshowdown.com/style/client2.css @@ -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; } diff --git a/translations/en-template.ts b/translations/en-template.ts index 301eb5034..39d63cc3f 100644 --- a/translations/en-template.ts +++ b/translations/en-template.ts @@ -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,