Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1219,5 +1219,17 @@
"fullscreen": {
"enter": "Enter fullscreen",
"exit": "Exit fullscreen"
},
"worker_error": {
"account_banned": "Account Banned",
"cannot_join_game": "Cannot join game",
"connection_refused": "Connection refused",
"forbidden": "Forbidden",
"game_not_found": "Game not found",
"lobby_full": "Lobby full",
"turnstile_invalid": "Unauthorized: invalid token",
"turnstile_fix_tips": "Try this to fix: \n 1. Do a hard refresh (Mac: Cmd+Shift+R, Windows: Ctrl+F5) \n 2. Or resync your device clock \n 3. Or close all browser windows and restart your browser \n 4. Or try another browser, preferably without extensions \n 5. Or clear site data for this site",
"unauthorized": "Unauthorized",
"user_me_fetch_failed": "Unauthorized: user fetch failed"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
27 changes: 26 additions & 1 deletion src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { replacer } from "../core/Util";
import { getPlayToken } from "./Auth";
import { LobbyConfig } from "./ClientGameRunner";
import { LocalServer } from "./LocalServer";
import { getEnglishText, translateText } from "./Utils";

export class PauseGameIntentEvent implements GameEvent {
constructor(public readonly paused: boolean) {}
Expand Down Expand Up @@ -378,8 +379,32 @@ export class Transport {
`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
);
if (event.code === 1002) {
const connRefusedKey = `worker_error.connection_refused`;
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit messy. I'm thinking we do something like create a zod schema for websocket error and it's just something like:

const ErrorSchema = z.object({
translationKey: z.string()
args: z.record<z.string(),z.string()>
})

there's a 123 byte limit, so maybe we should send the error and then close:

import { z } from "zod";
import type { WebSocket } from "ws";

export const WSErrorSchema = z.object({
translationKey: z.string(),
args: z.record(z.string(), z.string()).optional(),
});

export type WSError = z.infer;

export function sendErrorAndClose(ws: WebSocket, error: WSError, code = 4000) {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify(error));
}
ws.close(code);
}

Copy link
Copy Markdown
Contributor Author

@VariableVince VariableVince May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it. Don't think it is much less 'messy'. Because we still need to test stuff, since we don't know if A) we recieved an actual translation key and not just a raw error string like "ClientJoinMessageSchema", "WS_ERR_UNEXPECTED_RSV_1" and in cosmetics.reason and B) if we did recieve a translation key, the translation may be missing from both the user's labguage JSON and en.json and translateText will send back the translation key as a result.

To clarify it a bit more, i could rename "translationKey" in WSErrorSchema and the const in Transport.ts to something like "translationKeyOrError" because it can contain both.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah let's just not support args and then it's really simple like:

ws.close(1002, worker_error.game_not_found)

and then here we just do:

alert(translateText(ws.reason))

const errorKey = `worker_error.${event.reason}`;

let alertMsg = `${translateText(connRefusedKey)}: `;
const translatedError = translateText(errorKey);

if (translatedError === errorKey) {
// No translation key in error.reason or no translation or default English found
alertMsg += `${event.reason}`;
} else {
alertMsg += translatedError;

// Add tips if token invalid
if (event.reason === "turnstile_invalid") {
alertMsg += `\n${translateText("worker_error.turnstile_fix_tips")}`;
}

// Append English translation if it differs
const englishMsg = getEnglishText(errorKey);
if (englishMsg !== errorKey && !alertMsg.includes(englishMsg)) {
alertMsg += `\n\n--- English ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate the English-section label too.

"--- English ---" is user-visible client text, so it should come from translateText() with a matching locale entry instead of being hard-coded here.

🌐 Minimal fix
-            alertMsg += `\n\n--- English ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`;
+            alertMsg += `\n\n--- ${translateText("worker_error.english_label")} ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`;
   "worker_error": {
     "account_banned": "Account Banned",
     "cannot_join_game": "Cannot join game",
     "connection_refused": "Connection refused",
+    "english_label": "English",
     "forbidden": "Forbidden",

As per coding guidelines "All user-visible text must go through translateText() function with corresponding entries in resources/lang/en.json".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const englishMsg = getEnglishText(errorKey);
if (englishMsg !== errorKey && !alertMsg.includes(englishMsg)) {
alertMsg += `\n\n--- English ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`;
const englishMsg = getEnglishText(errorKey);
if (englishMsg !== errorKey && !alertMsg.includes(englishMsg)) {
alertMsg += `\n\n--- ${translateText("worker_error.english_label")} ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/Transport.ts` around lines 400 - 402, The hard-coded user-visible
label "--- English ---" in Transport.ts should be replaced with a translated
string: call translateText('english_section_label') (or similar key) instead of
the literal, and use that value when building alertMsg alongside
getEnglishText(connRefusedKey). Also add the corresponding English locale entry
for 'english_section_label' in the app's i18n resources so the label is
available for translateText().

}
}

// TODO: make this a modal
alert(`connection refused: ${event.reason}`);
alert(alertMsg);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else if (event.code !== 1000) {
console.log(`received error code ${event.code}, reconnecting`);
this.reconnect();
Expand Down
10 changes: 9 additions & 1 deletion src/client/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,17 @@ function getCachedLangSelector(): LangSelector | null {
return found;
}

export function getEnglishText(
key: string,
params?: Record<string, string | number>,
): string {
return translateText(key, params, true);
}

export const translateText = (
key: string,
params?: Record<string, string | number>,
getOnlyEnglish: boolean = false,
): string => {
const self = translateText as any;
self.formatterCache ??= new Map();
Expand All @@ -436,7 +444,7 @@ export const translateText = (
self.lastLang = langSelector.currentLang;
}

let message = translations?.[key];
let message = getOnlyEnglish ? undefined : translations?.[key];
const hasPrimaryTranslation = message !== undefined;

message ??= defaultTranslations?.[key];
Expand Down
20 changes: 10 additions & 10 deletions src/server/Worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,13 @@ export async function startWorker() {
log.warn(`Invalid token: ${result.message}`, {
gameID: clientMsg.gameID,
});
ws.close(1002, `Unauthorized: invalid token`);
ws.close(1002, "turnstile_invalid");
return;
}
const { persistentId, claims } = result;

if (claims?.role === "banned") {
ws.close(1002, "Account Banned");
ws.close(1002, "account_banned");
return;
}

Expand All @@ -355,7 +355,7 @@ export async function startWorker() {
log.warn(
`game ${clientMsg.gameID} not found on worker ${workerId}`,
);
ws.close(1002, "Game not found");
ws.close(1002, "game_not_found");
}
return;
}
Expand Down Expand Up @@ -385,7 +385,7 @@ export async function startWorker() {
if (claims === null) {
if (allowedFlares !== undefined) {
log.warn("Unauthorized: Anonymous user attempted to join game");
ws.close(1002, "Unauthorized");
ws.close(1002, "unauthorized");
return;
}
} else {
Expand All @@ -396,7 +396,7 @@ export async function startWorker() {
persistentID: persistentId,
gameID: clientMsg.gameID,
});
ws.close(1002, "Unauthorized: user me fetch failed");
ws.close(1002, "user_me_fetch_failed");
return;
}
flares = result.response.player.flares;
Expand All @@ -409,7 +409,7 @@ export async function startWorker() {
log.warn(
"Forbidden: player without an allowed flare attempted to join game",
);
ws.close(1002, "Forbidden");
ws.close(1002, "forbidden");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return;
}
}
Expand Down Expand Up @@ -443,7 +443,7 @@ export async function startWorker() {
gameID: clientMsg.gameID,
reason: turnstileResult.reason,
});
ws.close(1002, "Unauthorized: Turnstile token rejected");
ws.close(1002, "turnstile_invalid");
return;
case "error":
// Fail open, allow the client to join.
Expand Down Expand Up @@ -473,19 +473,19 @@ export async function startWorker() {

if (joinResult === "not_found") {
log.info(`game ${clientMsg.gameID} not found on worker ${workerId}`);
ws.close(1002, "Game not found");
ws.close(1002, "game_not_found");
} else if (joinResult === "kicked") {
log.warn(`kicked client tried to join game ${clientMsg.gameID}`, {
gameID: clientMsg.gameID,
workerId,
});
ws.close(1002, "Cannot join game");
ws.close(1002, "cannot_join_game");
} else if (joinResult === "rejected") {
log.info(`client rejected from game ${clientMsg.gameID}`, {
gameID: clientMsg.gameID,
workerId,
});
ws.close(1002, "Lobby full");
ws.close(1002, "lobby_full");
}

// Handle other message types
Expand Down
Loading