minesweeper panel keyed (578 -> 573)
First panel, so it also proves the panel.* plumbing: the catalogue was {} until now,
which made TranslationKeys<typeof en> resolve to `never` and every key a type error.
rfb.ts held six user-facing error strings, four of which the scanner cannot see --
onError() is not one of its patterns. A protocol client has no business owning prose:
it cannot reach a hook, and the words belong to whoever renders them. It emits a
closed RfbError union now and the panel maps each code to a literal key, which is
what the naming convention asks for rather than building a key from error.code.
The install hint had <code>wine-minesweeper</code> mid-sentence. Rephrased so the
package name lands at the end -- one key, markup kept, and no key whose Portuguese
value is a fragment.
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Bomb, Loader2, Play, RefreshCw } from "lucide-react";
|
import { Bomb, Loader2, Play, RefreshCw } from 'lucide-react';
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from 'hooks/useClient';
|
||||||
import { RfbClient } from "./rfb";
|
import type { RfbError } from './rfb';
|
||||||
|
import { RfbClient } from './rfb';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
|
type PanelT = ReturnType<typeof useTranslation>['t'];
|
||||||
|
|
||||||
type SessionStatus = {
|
type SessionStatus = {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
@@ -16,7 +20,26 @@ const VISIBLE = { width: 164, height: 226 };
|
|||||||
|
|
||||||
const BUTTON_MASK: Record<number, number> = { 0: 1, 1: 2, 2: 4 };
|
const BUTTON_MASK: Record<number, number> = { 0: 1, 1: 2, 2: 4 };
|
||||||
|
|
||||||
|
/** Explicit map from RfbError to literal keys — never a key assembled from `error.code`. */
|
||||||
|
const describeRfbError = (error: RfbError, t: PanelT): string => {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'connectionFailed':
|
||||||
|
return t('minesweeper.error.connectionFailed');
|
||||||
|
case 'refused':
|
||||||
|
return t('minesweeper.error.refused');
|
||||||
|
case 'authUnsupported':
|
||||||
|
return t('minesweeper.error.authUnsupported');
|
||||||
|
case 'handshakeRejected':
|
||||||
|
return t('minesweeper.error.handshakeRejected');
|
||||||
|
case 'unknownMessage':
|
||||||
|
return t('minesweeper.error.unknownMessage', { type: error.type });
|
||||||
|
case 'unknownEncoding':
|
||||||
|
return t('minesweeper.error.unknownEncoding', { encoding: error.encoding });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const MinesweeperPanel = () => {
|
export const MinesweeperPanel = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
// useClient() rebuilds its verbs on EVERY render, so putting one in a dependency list gives that
|
// useClient() rebuilds its verbs on EVERY render, so putting one in a dependency list gives that
|
||||||
// list a new identity every render. Held in a ref instead — the same fix the Jellyfin player needed
|
// list a new identity every render. Held in a ref instead — the same fix the Jellyfin player needed
|
||||||
// after its playback reports silently stopped for days. Getting this wrong here meant `refresh` was
|
// after its playback reports silently stopped for days. Getting this wrong here meant `refresh` was
|
||||||
@@ -45,9 +68,9 @@ export const MinesweeperPanel = () => {
|
|||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
adopt(await clientRef.current.get<SessionStatus>("/minesweeper/status"));
|
adopt(await clientRef.current.get<SessionStatus>('/minesweeper/status'));
|
||||||
} catch {
|
} catch {
|
||||||
setError("Could not reach the server.");
|
setError('Could not reach the server.');
|
||||||
}
|
}
|
||||||
}, [adopt]);
|
}, [adopt]);
|
||||||
|
|
||||||
@@ -59,9 +82,9 @@ export const MinesweeperPanel = () => {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
adopt(await clientRef.current.post<SessionStatus>("/minesweeper/start", {}));
|
adopt(await clientRef.current.post<SessionStatus>('/minesweeper/start', {}));
|
||||||
} catch {
|
} catch {
|
||||||
setError("The session would not start. Is wine-minesweeper installed on the server?");
|
setError('The session would not start. Is wine-minesweeper installed on the server?');
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -73,7 +96,7 @@ export const MinesweeperPanel = () => {
|
|||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const offscreen = offscreenRef.current;
|
const offscreen = offscreenRef.current;
|
||||||
if (!canvas || !offscreen) return;
|
if (!canvas || !offscreen) return;
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext('2d');
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
const { width, height } = geometryRef.current;
|
const { width, height } = geometryRef.current;
|
||||||
if (canvas.width !== width || canvas.height !== height) {
|
if (canvas.width !== width || canvas.height !== height) {
|
||||||
@@ -86,15 +109,15 @@ export const MinesweeperPanel = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!status?.running) return;
|
if (!status?.running) return;
|
||||||
|
|
||||||
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
// 'BEARER_TOKEN' is the key the rest of the app uses (Terminal.tsx, useAudioRecording). Reading
|
// 'BEARER_TOKEN' is the key the rest of the app uses (Terminal.tsx, useAudioRecording). Reading
|
||||||
// 'token' here sent an empty string, and upgradeWs answered 401 before any RFB byte was exchanged.
|
// 'token' here sent an empty string, and upgradeWs answered 401 before any RFB byte was exchanged.
|
||||||
const token = localStorage.getItem("BEARER_TOKEN") ?? sessionStorage.getItem("BEARER_TOKEN") ?? "";
|
const token = localStorage.getItem('BEARER_TOKEN') ?? sessionStorage.getItem('BEARER_TOKEN') ?? '';
|
||||||
const url = `${scheme}://${window.location.host}/api/minesweeper/ws?token=${encodeURIComponent(token)}`;
|
const url = `${scheme}://${window.location.host}/api/minesweeper/ws?token=${encodeURIComponent(token)}`;
|
||||||
|
|
||||||
const rfb = new RfbClient(url, {
|
const rfb = new RfbClient(url, {
|
||||||
onReady: (width, height) => {
|
onReady: (width, height) => {
|
||||||
const offscreen = document.createElement("canvas");
|
const offscreen = document.createElement('canvas');
|
||||||
offscreen.width = width;
|
offscreen.width = width;
|
||||||
offscreen.height = height;
|
offscreen.height = height;
|
||||||
offscreenRef.current = offscreen;
|
offscreenRef.current = offscreen;
|
||||||
@@ -102,12 +125,12 @@ export const MinesweeperPanel = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
},
|
},
|
||||||
onFrame: (image, x, y) => {
|
onFrame: (image, x, y) => {
|
||||||
offscreenRef.current?.getContext("2d")?.putImageData(image, x, y);
|
offscreenRef.current?.getContext('2d')?.putImageData(image, x, y);
|
||||||
present();
|
present();
|
||||||
},
|
},
|
||||||
onCopyRect: (from, to) => {
|
onCopyRect: (from, to) => {
|
||||||
const offscreen = offscreenRef.current;
|
const offscreen = offscreenRef.current;
|
||||||
const context = offscreen?.getContext("2d");
|
const context = offscreen?.getContext('2d');
|
||||||
if (!offscreen || !context) return;
|
if (!offscreen || !context) return;
|
||||||
context.drawImage(offscreen, from.x, from.y, to.w, to.h, to.x, to.y, to.w, to.h);
|
context.drawImage(offscreen, from.x, from.y, to.w, to.h, to.x, to.y, to.w, to.h);
|
||||||
present();
|
present();
|
||||||
@@ -120,7 +143,7 @@ export const MinesweeperPanel = () => {
|
|||||||
}
|
}
|
||||||
rfb.requestUpdate(false);
|
rfb.requestUpdate(false);
|
||||||
},
|
},
|
||||||
onError: (message) => setError(message),
|
onError: (rfbError) => setError(describeRfbError(rfbError, t)),
|
||||||
onClose: () => setConnected(false),
|
onClose: () => setConnected(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +161,7 @@ export const MinesweeperPanel = () => {
|
|||||||
if (!connected) return;
|
if (!connected) return;
|
||||||
const timer = setInterval(async () => {
|
const timer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const next = await clientRef.current.get<SessionStatus>("/minesweeper/status");
|
const next = await clientRef.current.get<SessionStatus>('/minesweeper/status');
|
||||||
if (!next.window) return;
|
if (!next.window) return;
|
||||||
const { width, height } = next.window;
|
const { width, height } = next.window;
|
||||||
if (width !== geometryRef.current.width || height !== geometryRef.current.height) {
|
if (width !== geometryRef.current.width || height !== geometryRef.current.height) {
|
||||||
@@ -159,14 +182,14 @@ export const MinesweeperPanel = () => {
|
|||||||
return { x: (ev.clientX - rect.left) * scaleX, y: (ev.clientY - rect.top) * scaleY };
|
return { x: (ev.clientX - rect.left) * scaleX, y: (ev.clientY - rect.top) * scaleY };
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointer = (ev: React.MouseEvent<HTMLCanvasElement>, kind: "down" | "up" | "move") => {
|
const onPointer = (ev: React.MouseEvent<HTMLCanvasElement>, kind: 'down' | 'up' | 'move') => {
|
||||||
const rfb = rfbRef.current;
|
const rfb = rfbRef.current;
|
||||||
if (!rfb) return;
|
if (!rfb) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (kind !== "move") {
|
if (kind !== 'move') {
|
||||||
const bit = BUTTON_MASK[ev.button] ?? 0;
|
const bit = BUTTON_MASK[ev.button] ?? 0;
|
||||||
buttonsRef.current = kind === "down" ? buttonsRef.current | bit : buttonsRef.current & ~bit;
|
buttonsRef.current = kind === 'down' ? buttonsRef.current | bit : buttonsRef.current & ~bit;
|
||||||
}
|
}
|
||||||
const { x, y } = toFramebuffer(ev);
|
const { x, y } = toFramebuffer(ev);
|
||||||
rfb.pointer(x, y, buttonsRef.current);
|
rfb.pointer(x, y, buttonsRef.current);
|
||||||
@@ -176,9 +199,11 @@ export const MinesweeperPanel = () => {
|
|||||||
return (
|
return (
|
||||||
<Centred>
|
<Centred>
|
||||||
<Bomb className="size-8 text-muted-foreground" />
|
<Bomb className="size-8 text-muted-foreground" />
|
||||||
<p className="text-sm text-muted-foreground">Minesweeper is not installed on this server.</p>
|
<p className="text-sm text-muted-foreground">{t('minesweeper.notInstalled')}</p>
|
||||||
|
{/* The package name sits at the end so the sentence is one key rather than two around an
|
||||||
|
inline element — see docs/localization.md on the fragmentation that produces. */}
|
||||||
<p className="max-w-sm text-center text-xs text-muted-foreground">
|
<p className="max-w-sm text-center text-xs text-muted-foreground">
|
||||||
It needs <code>wine-minesweeper</code> beside the platform, with wine and a 32-bit prefix.
|
{t('minesweeper.needs')} <code>wine-minesweeper</code>
|
||||||
</p>
|
</p>
|
||||||
</Centred>
|
</Centred>
|
||||||
);
|
);
|
||||||
@@ -188,7 +213,7 @@ export const MinesweeperPanel = () => {
|
|||||||
return (
|
return (
|
||||||
<Centred>
|
<Centred>
|
||||||
<Bomb className="size-8 text-muted-foreground" />
|
<Bomb className="size-8 text-muted-foreground" />
|
||||||
<p className="text-sm text-muted-foreground">The Minesweeper session is not running.</p>
|
<p className="text-sm text-muted-foreground">{t('minesweeper.notRunning')}</p>
|
||||||
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -197,7 +222,7 @@ export const MinesweeperPanel = () => {
|
|||||||
className="flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-60"
|
className="flex items-center gap-2 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{busy ? <Loader2 className="size-4 animate-spin" /> : <Play className="size-4" />}
|
{busy ? <Loader2 className="size-4 animate-spin" /> : <Play className="size-4" />}
|
||||||
{busy ? "Starting" : "Start it"}
|
{busy ? 'Starting' : 'Start it'}
|
||||||
</button>
|
</button>
|
||||||
</Centred>
|
</Centred>
|
||||||
);
|
);
|
||||||
@@ -209,17 +234,17 @@ export const MinesweeperPanel = () => {
|
|||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
width={VISIBLE.width}
|
width={VISIBLE.width}
|
||||||
height={VISIBLE.height}
|
height={VISIBLE.height}
|
||||||
onMouseDown={(ev) => onPointer(ev, "down")}
|
onMouseDown={(ev) => onPointer(ev, 'down')}
|
||||||
onMouseUp={(ev) => onPointer(ev, "up")}
|
onMouseUp={(ev) => onPointer(ev, 'up')}
|
||||||
onMouseMove={(ev) => onPointer(ev, "move")}
|
onMouseMove={(ev) => onPointer(ev, 'move')}
|
||||||
onContextMenu={(ev) => ev.preventDefault()}
|
onContextMenu={(ev) => ev.preventDefault()}
|
||||||
// Nearest-neighbour: this is a 16px-per-cell bitmap from 1995 and smoothing it looks like a
|
// Nearest-neighbour: this is a 16px-per-cell bitmap from 1995 and smoothing it looks like a
|
||||||
// photograph of a screen rather than the screen.
|
// photograph of a screen rather than the screen.
|
||||||
className="rounded-sm shadow-lg [image-rendering:pixelated]"
|
className="rounded-sm shadow-lg [image-rendering:pixelated]"
|
||||||
style={{ cursor: connected ? "default" : "progress" }}
|
style={{ cursor: connected ? 'default' : 'progress' }}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
<span>{connected ? "connected" : "connecting…"}</span>
|
<span>{connected ? 'connected' : 'connecting…'}</span>
|
||||||
<button type="button" onClick={() => void refresh()} className="flex items-center gap-1 hover:text-foreground">
|
<button type="button" onClick={() => void refresh()} className="flex items-center gap-1 hover:text-foreground">
|
||||||
<RefreshCw className="size-3" /> refresh
|
<RefreshCw className="size-3" /> refresh
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -26,10 +26,26 @@ export type RfbEvents = {
|
|||||||
onFrame: (image: ImageData, x: number, y: number) => void;
|
onFrame: (image: ImageData, x: number, y: number) => void;
|
||||||
onCopyRect: (from: { x: number; y: number }, to: { x: number; y: number; w: number; h: number }) => void;
|
onCopyRect: (from: { x: number; y: number }, to: { x: number; y: number; w: number; h: number }) => void;
|
||||||
onResize: (width: number, height: number) => void;
|
onResize: (width: number, height: number) => void;
|
||||||
onError: (message: string) => void;
|
onError: (error: RfbError) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What went wrong, as a code rather than a sentence.
|
||||||
|
*
|
||||||
|
* A protocol client has no business holding user-facing prose: it cannot reach a React hook, and the
|
||||||
|
* words belong to whoever is rendering them. The panel maps each code to a catalog key — an explicit
|
||||||
|
* map from a closed union to literal keys, which is what the naming convention asks for instead of
|
||||||
|
* building a key at runtime.
|
||||||
|
*/
|
||||||
|
export type RfbError =
|
||||||
|
| { code: 'connectionFailed' }
|
||||||
|
| { code: 'refused' }
|
||||||
|
| { code: 'authUnsupported' }
|
||||||
|
| { code: 'handshakeRejected' }
|
||||||
|
| { code: 'unknownMessage'; type: number }
|
||||||
|
| { code: 'unknownEncoding'; encoding: number };
|
||||||
|
|
||||||
const ENCODING_RAW = 0;
|
const ENCODING_RAW = 0;
|
||||||
const ENCODING_COPY_RECT = 1;
|
const ENCODING_COPY_RECT = 1;
|
||||||
const PSEUDO_ENCODING_DESKTOP_SIZE = -223;
|
const PSEUDO_ENCODING_DESKTOP_SIZE = -223;
|
||||||
@@ -54,7 +70,7 @@ export class RfbClient {
|
|||||||
this.closed = true;
|
this.closed = true;
|
||||||
this.events.onClose();
|
this.events.onClose();
|
||||||
};
|
};
|
||||||
this.socket.onerror = () => this.events.onError('The connection to the Minesweeper session failed.');
|
this.socket.onerror = () => this.events.onError({ code: 'connectionFailed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
@@ -125,7 +141,7 @@ export class RfbClient {
|
|||||||
// A zero count means the server is refusing us, and the reason follows as a string.
|
// A zero count means the server is refusing us, and the reason follows as a string.
|
||||||
const body = this.take(1 + 4);
|
const body = this.take(1 + 4);
|
||||||
if (!body) return false;
|
if (!body) return false;
|
||||||
this.events.onError('The VNC server refused the connection.');
|
this.events.onError({ code: 'refused' });
|
||||||
this.close();
|
this.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -134,7 +150,7 @@ export class RfbClient {
|
|||||||
|
|
||||||
const types = Array.from(head.subarray(1));
|
const types = Array.from(head.subarray(1));
|
||||||
if (!types.includes(1)) {
|
if (!types.includes(1)) {
|
||||||
this.events.onError('The VNC server wants authentication, which this client does not implement.');
|
this.events.onError({ code: 'authUnsupported' });
|
||||||
this.close();
|
this.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -147,7 +163,7 @@ export class RfbClient {
|
|||||||
const head = this.take(4);
|
const head = this.take(4);
|
||||||
if (!head) return false;
|
if (!head) return false;
|
||||||
if (new DataView(head.buffer, head.byteOffset, 4).getUint32(0) !== 0) {
|
if (new DataView(head.buffer, head.byteOffset, 4).getUint32(0) !== 0) {
|
||||||
this.events.onError('The VNC server rejected the handshake.');
|
this.events.onError({ code: 'handshakeRejected' });
|
||||||
this.close();
|
this.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -231,7 +247,7 @@ export class RfbClient {
|
|||||||
return this.take(8 + length) !== null;
|
return this.take(8 + length) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.events.onError(`The VNC server sent an unknown message type (${type}).`);
|
this.events.onError({ code: 'unknownMessage', type });
|
||||||
this.close();
|
this.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -251,7 +267,14 @@ export class RfbClient {
|
|||||||
for (let i = 0; i < rectangles; i++) {
|
for (let i = 0; i < rectangles; i++) {
|
||||||
if (this.buffer.length < offset + 12) return false;
|
if (this.buffer.length < offset + 12) return false;
|
||||||
const view = new DataView(this.buffer.buffer, this.buffer.byteOffset + offset, 12);
|
const view = new DataView(this.buffer.buffer, this.buffer.byteOffset + offset, 12);
|
||||||
const rect = { x: view.getUint16(0), y: view.getUint16(2), w: view.getUint16(4), h: view.getUint16(6), encoding: view.getInt32(8), at: offset + 12 };
|
const rect = {
|
||||||
|
x: view.getUint16(0),
|
||||||
|
y: view.getUint16(2),
|
||||||
|
w: view.getUint16(4),
|
||||||
|
h: view.getUint16(6),
|
||||||
|
encoding: view.getInt32(8),
|
||||||
|
at: offset + 12,
|
||||||
|
};
|
||||||
offset += 12;
|
offset += 12;
|
||||||
|
|
||||||
if (rect.encoding === ENCODING_RAW) offset += rect.w * rect.h * 4;
|
if (rect.encoding === ENCODING_RAW) offset += rect.w * rect.h * 4;
|
||||||
@@ -259,7 +282,7 @@ export class RfbClient {
|
|||||||
else if (rect.encoding === PSEUDO_ENCODING_DESKTOP_SIZE) {
|
else if (rect.encoding === PSEUDO_ENCODING_DESKTOP_SIZE) {
|
||||||
/* no payload — the rect's w/h ARE the new size */
|
/* no payload — the rect's w/h ARE the new size */
|
||||||
} else {
|
} else {
|
||||||
this.events.onError(`The VNC server used encoding ${rect.encoding}, which this client did not ask for.`);
|
this.events.onError({ code: 'unknownEncoding', encoding: rect.encoding });
|
||||||
this.close();
|
this.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
{}
|
{
|
||||||
|
"minesweeper": {
|
||||||
|
"notInstalled": "Minesweeper is not installed on this server.",
|
||||||
|
"needs": "It needs wine, a 32-bit prefix, and this package beside the platform:",
|
||||||
|
"notRunning": "The Minesweeper session is not running.",
|
||||||
|
"error": {
|
||||||
|
"connectionFailed": "The connection to the Minesweeper session failed.",
|
||||||
|
"refused": "The VNC server refused the connection.",
|
||||||
|
"authUnsupported": "The VNC server wants authentication, which this client does not implement.",
|
||||||
|
"handshakeRejected": "The VNC server rejected the handshake.",
|
||||||
|
"unknownMessage": "The VNC server sent an unknown message type ({{type}}).",
|
||||||
|
"unknownEncoding": "The VNC server used encoding {{encoding}}, which this client did not ask for."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
{}
|
{
|
||||||
|
"minesweeper": {
|
||||||
|
"notInstalled": "O Minesweeper não está instalado neste servidor.",
|
||||||
|
"needs": "Precisa do wine, de um prefixo de 32 bits e deste pacote junto à plataforma:",
|
||||||
|
"notRunning": "A sessão do Minesweeper não está em execução.",
|
||||||
|
"error": {
|
||||||
|
"connectionFailed": "A ligação à sessão do Minesweeper falhou.",
|
||||||
|
"refused": "O servidor VNC recusou a ligação.",
|
||||||
|
"authUnsupported": "O servidor VNC pede autenticação, que este cliente não implementa.",
|
||||||
|
"handshakeRejected": "O servidor VNC rejeitou o handshake.",
|
||||||
|
"unknownMessage": "O servidor VNC enviou um tipo de mensagem desconhecido ({{type}}).",
|
||||||
|
"unknownEncoding": "O servidor VNC usou a codificação {{encoding}}, que este cliente não pediu."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user