mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-09-12 13:59:09 -05:00
Preact minor updates batch 47
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled
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:
@@ -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}',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface PSConfig {
|
||||
teams: string,
|
||||
};
|
||||
customcolors: Record<string, string>;
|
||||
translationCachebuster?: string;
|
||||
whitelist?: string[];
|
||||
testclient?: boolean;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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=/;";
|
||||
}
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user