mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
console page: Home
Running game with Close, one storage card per drive, every job with its owner (this console / a PC) and Cancel, quick actions Browse USB (hidden until Files opens at a path), Rest mode and Restart. Every destructive action goes through the shared ConfirmDialog (nav modal, focus on the safe choice, ○ closes). useJobs polls /api/jobs every 1 s while a job runs, every 5 s otherwise, stops on unmount and on 401. Sections get a context (status, refreshStatus, openSection with params). Strings in 21 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
1 parent
8f7233827f
commit
f91e0f9b67
31 files changed
+3592
-33
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { call, isOffline, onTokenExpired } from "./api";
|
||||
import { focusElement, onNavAction, type InputMap } from "./nav";
|
||||
import { SECTIONS } from "./sections";
|
||||
import { SECTIONS, type SectionContext, type SectionParams } from "./sections";
|
||||
import { HintBar } from "./shell/HintBar";
|
||||
import { Rail } from "./shell/Rail";
|
||||
import { ReloadBanner } from "./shell/ReloadBanner";
|
||||
@@ -21,6 +21,16 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
const [expired, setExpired] = useState(false);
|
||||
const [status, setStatus] = useState<ConsoleStatus | null>(null);
|
||||
const [offline, setOffline] = useState(false);
|
||||
/** Bumped to read /api/status again at once. */
|
||||
const [statusTick, setStatusTick] = useState(0);
|
||||
/** What the active section was opened with (Home's Browse USB → Files at a path). */
|
||||
const [params, setParams] = useState<SectionParams | null>(null);
|
||||
|
||||
/** Switch section; params go only to a section opened through openSection. */
|
||||
const show = useCallback((id: string, p: SectionParams | null = null) => {
|
||||
setActive(id);
|
||||
setParams(p);
|
||||
}, []);
|
||||
|
||||
useEffect(() => onTokenExpired(() => setExpired(true)), []);
|
||||
|
||||
@@ -46,7 +56,7 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
live = false;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [expired]);
|
||||
}, [expired, statusTick]);
|
||||
|
||||
// Start with focus on the rail, so the D-pad works at once.
|
||||
useEffect(() => {
|
||||
@@ -62,7 +72,7 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
if (action === "prevSection" || action === "nextSection") {
|
||||
const step = action === "nextSection" ? 1 : SECTIONS.length - 1;
|
||||
const next = SECTIONS[(i + step) % SECTIONS.length].id;
|
||||
setActive(next);
|
||||
show(next);
|
||||
const el = railButton(next);
|
||||
if (el) focusElement(el);
|
||||
return true;
|
||||
@@ -73,7 +83,7 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
const el = railButton(active);
|
||||
if (el) focusElement(el);
|
||||
} else if (active !== SECTIONS[0].id) {
|
||||
setActive(SECTIONS[0].id);
|
||||
show(SECTIONS[0].id);
|
||||
const el = railButton(SECTIONS[0].id);
|
||||
if (el) focusElement(el);
|
||||
}
|
||||
@@ -81,7 +91,23 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
[active],
|
||||
[active, show],
|
||||
);
|
||||
|
||||
const ctx = useMemo<SectionContext>(
|
||||
() => ({
|
||||
status,
|
||||
refreshStatus: () => setStatusTick((n) => n + 1),
|
||||
openSection: (id, p) => {
|
||||
if (!SECTIONS.some((s) => s.id === id)) return;
|
||||
show(id, p ?? null);
|
||||
const el = railButton(id);
|
||||
if (el) focusElement(el);
|
||||
},
|
||||
canOpen: (id) => SECTIONS.some((s) => s.id === id && s.acceptsParams),
|
||||
params,
|
||||
}),
|
||||
[status, params, show],
|
||||
);
|
||||
|
||||
const section = SECTIONS.find((s) => s.id === active) ?? SECTIONS[0];
|
||||
@@ -91,8 +117,8 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
<div className="cw-app">
|
||||
<TopBar status={status} chipFallback={chipFallback} />
|
||||
<div className="cw-body" aria-hidden={expired ? true : undefined}>
|
||||
<Rail sections={SECTIONS} active={active} onSelect={setActive} />
|
||||
<main className="cw-content">{section.render()}</main>
|
||||
<Rail sections={SECTIONS} active={active} onSelect={(id) => show(id)} />
|
||||
<main className="cw-content">{section.render(ctx)}</main>
|
||||
</div>
|
||||
<HintBar map={inputMap} />
|
||||
{expired ? <ReloadBanner /> : null}
|
||||
|
||||
@@ -243,3 +243,236 @@ button {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
.cw-button.is-danger {
|
||||
background: var(--color-bad);
|
||||
color: #fff;
|
||||
}
|
||||
.cw-button.is-danger:focus {
|
||||
background: var(--color-bad);
|
||||
box-shadow: 0 0 0 4px var(--color-bad-soft);
|
||||
}
|
||||
|
||||
/* ---- shared text ---- */
|
||||
.cw-h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cw-dim {
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.cw-mono {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.cw-tag {
|
||||
display: inline-block;
|
||||
padding: 0.05rem 0.5rem;
|
||||
margin-left: 0.6rem;
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-pill);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cw-tag.is-pc {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ---- progress bar ---- */
|
||||
.cw-bar {
|
||||
height: 0.4rem;
|
||||
margin: 0.35rem 0;
|
||||
border-radius: 0.2rem;
|
||||
background: var(--color-surface-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cw-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 0.2rem;
|
||||
background: var(--accent-fill);
|
||||
}
|
||||
.cw-bar-fill.is-warn {
|
||||
background: var(--color-warn);
|
||||
}
|
||||
.cw-bar-fill.is-unknown {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ---- confirm dialog ---- */
|
||||
.cw-scrim {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--overlay-scrim, rgba(0, 0, 0, 0.6));
|
||||
z-index: 20;
|
||||
}
|
||||
.cw-dialog {
|
||||
width: 32rem;
|
||||
max-width: calc(100vw - 2rem);
|
||||
padding: 1.2rem 1.4rem;
|
||||
border-radius: var(--radius-panel);
|
||||
background: var(--color-float);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
.cw-dialog h2 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.cw-dialog p {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.cw-dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.cw-dialog-buttons .cw-button {
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
|
||||
/* ---- Home ---- */
|
||||
.cw-msg {
|
||||
margin: 0 0 0.8rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: var(--radius-field);
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
.cw-msg.is-error {
|
||||
background: var(--color-bad-soft);
|
||||
color: var(--color-bad);
|
||||
}
|
||||
.cw-msg.is-info {
|
||||
background: var(--color-good-soft);
|
||||
color: var(--color-good);
|
||||
}
|
||||
.cw-quick {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.cw-quick .cw-button {
|
||||
margin: 0 0.7rem 0.5rem 0;
|
||||
}
|
||||
.cw-card {
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.cw-game {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem 1rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.cw-game-icon {
|
||||
flex: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-right: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.cw-game-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cw-game-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cw-home-section {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.cw-home-section > p {
|
||||
margin: 0;
|
||||
}
|
||||
.cw-tasks,
|
||||
.cw-volumes {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.cw-task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-right: 0.8rem;
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.cw-task-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.6rem 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
.cw-task-icon {
|
||||
flex: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.cw-task-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cw-task-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.cw-task-sub {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.cw-task.is-failed .cw-task-sub {
|
||||
color: var(--color-bad);
|
||||
}
|
||||
.cw-task.is-done .cw-task-sub {
|
||||
color: var(--color-good);
|
||||
}
|
||||
.cw-task > .cw-button {
|
||||
flex: none;
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
.cw-volumes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
grid-gap: 0.8rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.cw-volume {
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.cw-volume-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cw-volume-head svg {
|
||||
flex: none;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
margin-right: 0.5rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.cw-volume-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.cw-volume-free {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../api", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("../api")>();
|
||||
return { ...real, call: vi.fn() };
|
||||
});
|
||||
|
||||
import { ApiError, call } from "../api";
|
||||
import { t } from "../strings";
|
||||
import { ConfirmDialog, dialogNavHandler } from "../ui/ConfirmDialog";
|
||||
import { errorText, firstUsbVolume, HomeView, runAsk, type Ask, type HomeViewProps, type Volume } from "./Home";
|
||||
import { TaskRow } from "./TaskRow";
|
||||
import { cancelJob, type Job } from "./useJobs";
|
||||
|
||||
const mockCall = vi.mocked(call);
|
||||
|
||||
const job = (over: Partial<Job> = {}): Job => ({
|
||||
job: "0123456789abcdef0123456789abcdef",
|
||||
kind: "copy",
|
||||
state: "running",
|
||||
files_done: 1,
|
||||
files_total: 4,
|
||||
bytes_done: 100,
|
||||
bytes_total: 400,
|
||||
mine: true,
|
||||
...over,
|
||||
});
|
||||
|
||||
const vol = (path: string, mount_from: string, over: Partial<Volume> = {}): Volume => ({
|
||||
path,
|
||||
mount_from,
|
||||
fs_type: "ufs",
|
||||
total_bytes: 1000,
|
||||
free_bytes: 250,
|
||||
writable: true,
|
||||
...over,
|
||||
});
|
||||
|
||||
/** Every element in `node`'s tree (function components are not expanded). */
|
||||
function elements(node: ReactNode): ReactElement<Record<string, unknown>>[] {
|
||||
const out: ReactElement<Record<string, unknown>>[] = [];
|
||||
const walk = (n: ReactNode) => {
|
||||
if (Array.isArray(n)) return n.forEach(walk);
|
||||
if (!isValidElement(n)) return;
|
||||
const el = n as ReactElement<Record<string, unknown>>;
|
||||
out.push(el);
|
||||
walk(el.props.children as ReactNode);
|
||||
};
|
||||
walk(node);
|
||||
return out;
|
||||
}
|
||||
|
||||
const byAction = (node: ReactNode, action: string) => {
|
||||
const el = elements(node).find((e) => e.props["data-action"] === action);
|
||||
if (!el) throw new Error(`no element with data-action=${action}`);
|
||||
return el;
|
||||
};
|
||||
|
||||
function viewProps(over: Partial<HomeViewProps> = {}): HomeViewProps {
|
||||
return {
|
||||
status: null,
|
||||
volumes: [],
|
||||
volumesError: false,
|
||||
jobs: [],
|
||||
jobsLoaded: true,
|
||||
jobsError: false,
|
||||
ask: null,
|
||||
busy: false,
|
||||
message: null,
|
||||
onAsk: vi.fn(),
|
||||
onConfirm: vi.fn(),
|
||||
onDismiss: vi.fn(),
|
||||
onBrowseUsb: null,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCall.mockReset();
|
||||
mockCall.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("Home", () => {
|
||||
it("shows_pc_job_as_from_pc", () => {
|
||||
expect(t("cw_task_from_pc")).not.toBe("cw_task_from_pc");
|
||||
const pc = renderToStaticMarkup(<TaskRow job={job({ mine: false })} onCancel={() => {}} />);
|
||||
expect(pc).toContain(t("cw_task_from_pc"));
|
||||
expect(pc).not.toContain(t("cw_task_from_console"));
|
||||
const mine = renderToStaticMarkup(<TaskRow job={job({ mine: true })} onCancel={() => {}} />);
|
||||
expect(mine).toContain(t("cw_task_from_console"));
|
||||
});
|
||||
|
||||
it("cancel_asks_confirm_then_calls_api", async () => {
|
||||
const running = job({ mine: false });
|
||||
// Cancel on the row only asks.
|
||||
const onCancel = vi.fn();
|
||||
(byAction(TaskRow({ job: running, onCancel }), "cancel").props.onClick as () => void)();
|
||||
expect(onCancel).toHaveBeenCalledWith(running);
|
||||
expect(mockCall).not.toHaveBeenCalled();
|
||||
|
||||
// The ask opens the confirm dialog; focus starts on the safe choice.
|
||||
const ask: Ask = { kind: "cancel", job: running };
|
||||
const props = viewProps({ ask, jobs: [running] });
|
||||
const html = renderToStaticMarkup(<HomeView {...props} />);
|
||||
expect(html).toContain("data-nav-modal");
|
||||
expect(html).toContain(t("cw_home_confirm_cancel_title"));
|
||||
const dialog = elements(HomeView(props)).find((e) => e.type === ConfirmDialog);
|
||||
expect(dialog).toBeDefined();
|
||||
expect(mockCall).not.toHaveBeenCalled();
|
||||
|
||||
// "Keep going" (or ○) closes it with no call.
|
||||
(dialog!.props.onCancel as () => void)();
|
||||
expect(props.onDismiss).toHaveBeenCalled();
|
||||
expect(mockCall).not.toHaveBeenCalled();
|
||||
|
||||
// Confirm runs the ask: POST /api/jobs/cancel {job}.
|
||||
(dialog!.props.onConfirm as () => void)();
|
||||
expect(props.onConfirm).toHaveBeenCalled();
|
||||
await runAsk(ask, { cancel: cancelJob });
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/jobs/cancel", {
|
||||
job: running.job,
|
||||
});
|
||||
});
|
||||
|
||||
it("a finished task has no Cancel", () => {
|
||||
expect(() => byAction(TaskRow({ job: job({ state: "done" }), onCancel: () => {} }), "cancel")).toThrow();
|
||||
});
|
||||
|
||||
it("close game asks, then posts the app id", async () => {
|
||||
const props = viewProps({
|
||||
status: {
|
||||
version: null,
|
||||
firmware: null,
|
||||
model: null,
|
||||
temp_c: null,
|
||||
running: { title_id: "PPSA01234", app_id: 77, name: null },
|
||||
},
|
||||
});
|
||||
(byAction(HomeView(props), "close-game").props.onClick as () => void)();
|
||||
const ask: Ask = { kind: "close", appId: 77, titleId: "PPSA01234" };
|
||||
expect(props.onAsk).toHaveBeenCalledWith(ask);
|
||||
expect(mockCall).not.toHaveBeenCalled();
|
||||
await runAsk(ask, { cancel: cancelJob });
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/app/close", { app_id: 77 });
|
||||
});
|
||||
|
||||
it("rest mode and restart ask, then post the power action", async () => {
|
||||
const props = viewProps();
|
||||
(byAction(HomeView(props), "rest").props.onClick as () => void)();
|
||||
(byAction(HomeView(props), "restart").props.onClick as () => void)();
|
||||
expect(props.onAsk).toHaveBeenNthCalledWith(1, {
|
||||
kind: "power",
|
||||
action: "rest",
|
||||
});
|
||||
expect(props.onAsk).toHaveBeenNthCalledWith(2, {
|
||||
kind: "power",
|
||||
action: "reboot",
|
||||
});
|
||||
expect(mockCall).not.toHaveBeenCalled();
|
||||
await runAsk({ kind: "power", action: "rest" }, { cancel: cancelJob });
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/power", { action: "rest" });
|
||||
await runAsk({ kind: "power", action: "reboot" }, { cancel: cancelJob });
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/power", { action: "reboot" });
|
||||
});
|
||||
|
||||
it("Browse USB shows only when there is somewhere to go", () => {
|
||||
expect(() => byAction(HomeView(viewProps()), "browse-usb")).toThrow();
|
||||
const go = vi.fn();
|
||||
(byAction(HomeView(viewProps({ onBrowseUsb: go })), "browse-usb").props.onClick as () => void)();
|
||||
expect(go).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("first USB volume: a /mnt/usbN stick or USB extended storage, never internal or M.2", () => {
|
||||
expect(firstUsbVolume([vol("/data", "/dev/ssd0.user"), vol("/mnt/ext0", "/dev/nvme0p1")])).toBeNull();
|
||||
expect(
|
||||
firstUsbVolume([vol("/data", "x"), vol("/mnt/ext1", "/dev/da0"), vol("/mnt/usb0", "/dev/da1s1")])?.path,
|
||||
).toBe("/mnt/ext1");
|
||||
expect(firstUsbVolume([vol("/mnt/usb0", "/dev/da0s1")])?.path).toBe("/mnt/usb0");
|
||||
});
|
||||
|
||||
it("one storage card per drive with free of total", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<HomeView
|
||||
{...viewProps({
|
||||
volumes: [vol("/data", "x"), vol("/mnt/usb0", "/dev/da0s1")],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain(t("cw_home_vol_internal"));
|
||||
expect(html).toContain(t("cw_home_vol_usb"));
|
||||
expect(html.split("data-volume=").length - 1).toBe(2);
|
||||
});
|
||||
|
||||
it("errors become short translated messages", () => {
|
||||
expect(errorText(new ApiError(0, "offline"))).toBe(t("cw_home_err_offline"));
|
||||
expect(errorText(new ApiError(409, "busy"))).toBe(t("cw_home_err_busy"));
|
||||
expect(errorText(new ApiError(404, "unknown_job"))).toBe(t("cw_home_err_gone"));
|
||||
expect(errorText(new ApiError(500, "io_error"))).toBe(t("cw_home_err_failed", { code: "io_error" }));
|
||||
expect(errorText(new ApiError(401, "token"))).toBeNull(); // the reload banner says it
|
||||
expect(errorText(new Error("x"))).toBe(t("cw_home_err_failed", { code: "x" }));
|
||||
});
|
||||
|
||||
it("the shown message renders as an alert", () => {
|
||||
const html = renderToStaticMarkup(<HomeView {...viewProps({ message: { tone: "error", text: "Nope" } })} />);
|
||||
expect(html).toContain('role="alert"');
|
||||
expect(html).toContain("Nope");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConfirmDialog", () => {
|
||||
it("is a nav modal whose safe choice is the default focus", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ConfirmDialog title="T" confirmLabel="Do it" onConfirm={() => {}} onCancel={() => {}} />,
|
||||
);
|
||||
expect(html).toContain("data-nav-modal");
|
||||
expect(html).toContain('role="alertdialog"');
|
||||
// The cancel button comes first and carries data-nav-default.
|
||||
const safe = html.indexOf("data-nav-default");
|
||||
expect(safe).toBeGreaterThan(-1);
|
||||
expect(safe).toBeLessThan(html.indexOf("Do it"));
|
||||
expect(html).toContain(t("cw_dialog_cancel"));
|
||||
});
|
||||
|
||||
it("○ cancels; other actions are swallowed while it is open", () => {
|
||||
const onCancel = vi.fn();
|
||||
const handle = dialogNavHandler(onCancel);
|
||||
expect(handle("back")).toBe(true);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
expect(handle("nextSection")).toBe(true);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,399 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FolderOpen, Gamepad2, HardDrive, Moon, RotateCw, Usb, X } from "lucide-react";
|
||||
import { formatBytes } from "../../lib/format";
|
||||
import { ApiError, call, isOffline } from "../api";
|
||||
import type { SectionContext } from "../sections";
|
||||
import type { ConsoleStatus } from "../shell/TopBar";
|
||||
import { t } from "../strings";
|
||||
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
||||
import { TaskRow } from "./TaskRow";
|
||||
import { useJobs, type Job } from "./useJobs";
|
||||
|
||||
/** Free space moves with every write: re-read this often while Home is open. */
|
||||
const VOLUMES_POLL_MS = 15_000;
|
||||
/** An action's result line clears itself after this long. */
|
||||
const MESSAGE_MS = 8_000;
|
||||
|
||||
/** One /api/volumes entry (payload/src/runtime.c handle_fs_list_volumes). */
|
||||
export interface Volume {
|
||||
path: string;
|
||||
mount_from: string;
|
||||
fs_type: string;
|
||||
total_bytes: number;
|
||||
free_bytes: number;
|
||||
writable: boolean;
|
||||
source_image?: string;
|
||||
}
|
||||
|
||||
/** Something Home asks about before doing it. */
|
||||
export type Ask =
|
||||
| { kind: "cancel"; job: Job }
|
||||
| { kind: "close"; appId: number; titleId: string }
|
||||
| { kind: "power"; action: "rest" | "reboot" };
|
||||
|
||||
export interface Message {
|
||||
tone: "error" | "info";
|
||||
text: string;
|
||||
}
|
||||
|
||||
/* ---- volumes ---- */
|
||||
|
||||
const isUsbStick = (v: Volume) => /^\/mnt\/usb\d+$/.test(v.path);
|
||||
const isExt = (v: Volume) => /^\/mnt\/ext\d+$/.test(v.path);
|
||||
/** A USB drive formatted as PS5 extended storage (M.2 is /dev/nvme*). */
|
||||
const isUsbExt = (v: Volume) => isExt(v) && v.mount_from.startsWith("/dev/da");
|
||||
|
||||
/** The console's drives: internal, extended/M.2 and USB. Mounted disk images are left out. */
|
||||
export function isDrive(v: Volume): boolean {
|
||||
return v.path === "/data" || isExt(v) || isUsbStick(v);
|
||||
}
|
||||
|
||||
/** The first USB drive in the helper's order (a stick or USB extended storage), or null. */
|
||||
export function firstUsbVolume(volumes: Volume[]): Volume | null {
|
||||
return volumes.find((v) => isUsbStick(v) || isUsbExt(v)) ?? null;
|
||||
}
|
||||
|
||||
export function volumeLabel(v: Volume): string {
|
||||
if (v.path === "/data") return t("cw_home_vol_internal");
|
||||
if (isUsbStick(v)) return t("cw_home_vol_usb");
|
||||
if (isUsbExt(v)) return t("cw_home_vol_usb_ext");
|
||||
if (v.mount_from.startsWith("/dev/nvme")) return t("cw_home_vol_m2");
|
||||
return t("cw_home_vol_ext");
|
||||
}
|
||||
|
||||
/* ---- actions ---- */
|
||||
|
||||
/** A failed call as one short translated line; null for a 401 (the reload banner says it). */
|
||||
export function errorText(err: unknown): string | null {
|
||||
if (err instanceof ApiError) {
|
||||
if (err.status === 401) return null;
|
||||
if (isOffline(err)) return t("cw_home_err_offline");
|
||||
if (err.code === "busy") return t("cw_home_err_busy");
|
||||
if (err.code === "unknown_job") return t("cw_home_err_gone");
|
||||
return t("cw_home_err_failed", { code: err.code });
|
||||
}
|
||||
return t("cw_home_err_failed", {
|
||||
code: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
|
||||
/** Do what was confirmed. Resolves to a line to show (the power actions), throws the ApiError. */
|
||||
export async function runAsk(ask: Ask, deps: { cancel: (id: string) => Promise<void> }): Promise<string | null> {
|
||||
switch (ask.kind) {
|
||||
case "cancel":
|
||||
await deps.cancel(ask.job.job);
|
||||
return null;
|
||||
case "close":
|
||||
await call("/api/app/close", { app_id: ask.appId });
|
||||
return null;
|
||||
case "power":
|
||||
await call("/api/power", { action: ask.action });
|
||||
return t(ask.action === "rest" ? "cw_home_rest_sent" : "cw_home_restart_sent");
|
||||
}
|
||||
}
|
||||
|
||||
function askCopy(ask: Ask): {
|
||||
title: string;
|
||||
body: string;
|
||||
confirm: string;
|
||||
cancel?: string;
|
||||
danger: boolean;
|
||||
} {
|
||||
switch (ask.kind) {
|
||||
case "cancel":
|
||||
return {
|
||||
title: t("cw_home_confirm_cancel_title"),
|
||||
body: t(ask.job.mine ? "cw_home_confirm_cancel_body" : "cw_home_confirm_cancel_body_pc"),
|
||||
confirm: t("cw_home_cancel_task_confirm"),
|
||||
cancel: t("cw_home_keep_task"),
|
||||
danger: true,
|
||||
};
|
||||
case "close":
|
||||
return {
|
||||
title: t("cw_home_confirm_close_title", { name: ask.titleId }),
|
||||
body: t("cw_home_confirm_close_body"),
|
||||
confirm: t("cw_home_close_game"),
|
||||
danger: true,
|
||||
};
|
||||
case "power":
|
||||
return ask.action === "rest"
|
||||
? {
|
||||
title: t("cw_home_confirm_rest_title"),
|
||||
body: t("cw_home_confirm_rest_body"),
|
||||
confirm: t("cw_home_rest"),
|
||||
danger: false,
|
||||
}
|
||||
: {
|
||||
title: t("cw_home_confirm_restart_title"),
|
||||
body: t("cw_home_confirm_restart_body"),
|
||||
confirm: t("cw_home_restart"),
|
||||
danger: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- view ---- */
|
||||
|
||||
export interface HomeViewProps {
|
||||
status: ConsoleStatus | null;
|
||||
/** null until the first answer. */
|
||||
volumes: Volume[] | null;
|
||||
volumesError: boolean;
|
||||
jobs: Job[];
|
||||
jobsLoaded: boolean;
|
||||
jobsError: boolean;
|
||||
ask: Ask | null;
|
||||
busy: boolean;
|
||||
message: Message | null;
|
||||
onAsk: (ask: Ask) => void;
|
||||
onConfirm: () => void;
|
||||
onDismiss: () => void;
|
||||
/** Browse USB, or null to hide it (no USB drive, or Files cannot open a path yet). */
|
||||
onBrowseUsb: (() => void) | null;
|
||||
}
|
||||
|
||||
function VolumeCard({ v }: { v: Volume }) {
|
||||
const used = v.total_bytes > 0 ? Math.max(0, Math.min(100, 100 - (v.free_bytes / v.total_bytes) * 100)) : 0;
|
||||
const Icon = isUsbStick(v) || isUsbExt(v) ? Usb : HardDrive;
|
||||
return (
|
||||
<li className="cw-focusable cw-volume" tabIndex={0} data-volume={v.path}>
|
||||
<div className="cw-volume-head">
|
||||
<Icon aria-hidden />
|
||||
<span className="cw-volume-name">{volumeLabel(v)}</span>
|
||||
{!v.writable ? <span className="cw-tag">{t("cw_home_vol_readonly")}</span> : null}
|
||||
</div>
|
||||
<div className="cw-dim cw-mono">{v.path}</div>
|
||||
<div className="cw-bar" aria-hidden>
|
||||
<div className={`cw-bar-fill${used >= 90 ? " is-warn" : ""}`} style={{ width: `${used}%` }} />
|
||||
</div>
|
||||
<div className="cw-volume-free">
|
||||
{t("cw_home_free_of", {
|
||||
free: formatBytes(v.free_bytes),
|
||||
total: formatBytes(v.total_bytes),
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** Home without state: everything comes in through props (tests render it directly). */
|
||||
export function HomeView(p: HomeViewProps) {
|
||||
const running = p.status?.running ?? null;
|
||||
const tasks = p.jobs.filter((j) => j.state === "running").concat(p.jobs.filter((j) => j.state !== "running"));
|
||||
const drives = (p.volumes ?? []).filter(isDrive);
|
||||
const copy = p.ask ? askCopy(p.ask) : null;
|
||||
|
||||
let tasksNote: string | null = null;
|
||||
if (p.jobsError && !tasks.length) tasksNote = t("cw_home_err_load_tasks");
|
||||
else if (!p.jobsLoaded) tasksNote = t("cw_home_loading");
|
||||
else if (!tasks.length) tasksNote = t("cw_home_no_tasks");
|
||||
|
||||
let drivesNote: string | null = null;
|
||||
if (p.volumesError && !drives.length) drivesNote = t("cw_home_err_load_storage");
|
||||
else if (p.volumes == null) drivesNote = t("cw_home_loading");
|
||||
|
||||
return (
|
||||
<div className="cw-home">
|
||||
<h1 className="cw-h1">{t("cw_nav_home")}</h1>
|
||||
{p.message ? (
|
||||
<div className={`cw-msg is-${p.message.tone}`} role={p.message.tone === "error" ? "alert" : "status"}>
|
||||
{p.message.text}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="cw-quick" role="group" aria-label={t("cw_home_quick")}>
|
||||
{p.onBrowseUsb ? (
|
||||
<button type="button" className="cw-focusable cw-button" data-action="browse-usb" onClick={p.onBrowseUsb}>
|
||||
<FolderOpen aria-hidden />
|
||||
<span>{t("cw_home_browse_usb")}</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="rest"
|
||||
onClick={() => p.onAsk({ kind: "power", action: "rest" })}
|
||||
>
|
||||
<Moon aria-hidden />
|
||||
<span>{t("cw_home_rest")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="restart"
|
||||
onClick={() => p.onAsk({ kind: "power", action: "reboot" })}
|
||||
>
|
||||
<RotateCw aria-hidden />
|
||||
<span>{t("cw_home_restart")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="cw-card cw-game" aria-label={t("cw_home_now_running")}>
|
||||
<Gamepad2 className="cw-game-icon" aria-hidden />
|
||||
{running ? (
|
||||
<>
|
||||
<div className="cw-game-text">
|
||||
<div className="cw-dim">{t("cw_home_now_running")}</div>
|
||||
<div className="cw-game-title">{running.name ?? running.title_id}</div>
|
||||
{running.name ? <div className="cw-dim cw-mono">{running.title_id}</div> : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="close-game"
|
||||
onClick={() =>
|
||||
p.onAsk({
|
||||
kind: "close",
|
||||
appId: running.app_id,
|
||||
titleId: running.name ?? running.title_id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<X aria-hidden />
|
||||
<span>{t("cw_home_close_game")}</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="cw-game-text cw-dim">{p.status ? t("cw_home_no_game") : t("cw_home_loading")}</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="cw-home-section">
|
||||
<h2 className="cw-h2">{t("cw_home_tasks")}</h2>
|
||||
{p.jobsError && tasks.length ? <div className="cw-dim">{t("cw_home_err_load_tasks")}</div> : null}
|
||||
{tasksNote ? (
|
||||
<p className="cw-dim">{tasksNote}</p>
|
||||
) : (
|
||||
<ul className="cw-tasks">
|
||||
{tasks.map((j) => (
|
||||
<TaskRow key={j.job} job={j} onCancel={(job) => p.onAsk({ kind: "cancel", job })} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="cw-home-section">
|
||||
<h2 className="cw-h2">{t("cw_home_storage")}</h2>
|
||||
{drivesNote ? (
|
||||
<p className="cw-dim">{drivesNote}</p>
|
||||
) : (
|
||||
<ul className="cw-volumes">
|
||||
{drives.map((v) => (
|
||||
<VolumeCard key={v.path} v={v} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{p.ask && copy ? (
|
||||
<ConfirmDialog
|
||||
title={copy.title}
|
||||
body={copy.body}
|
||||
confirmLabel={copy.confirm}
|
||||
cancelLabel={copy.cancel}
|
||||
danger={copy.danger}
|
||||
busy={p.busy}
|
||||
onConfirm={p.onConfirm}
|
||||
onCancel={p.onDismiss}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- the section ---- */
|
||||
|
||||
function useVolumes(): { volumes: Volume[] | null; error: boolean } {
|
||||
const [volumes, setVolumes] = useState<Volume[] | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const load = () => {
|
||||
call<{ volumes?: unknown }>("/api/volumes").then(
|
||||
(r) => {
|
||||
if (!live) return;
|
||||
setVolumes(Array.isArray(r?.volumes) ? (r.volumes as Volume[]) : []);
|
||||
setError(false);
|
||||
timer = setTimeout(load, VOLUMES_POLL_MS);
|
||||
},
|
||||
(e: unknown) => {
|
||||
if (!live) return;
|
||||
setError(true);
|
||||
// The token is gone: every read would 401 until the page reloads.
|
||||
if (!(e instanceof ApiError && e.status === 401)) timer = setTimeout(load, VOLUMES_POLL_MS);
|
||||
},
|
||||
);
|
||||
};
|
||||
load();
|
||||
return () => {
|
||||
live = false;
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
return { volumes, error };
|
||||
}
|
||||
|
||||
/** The Home section: the running game, tasks, storage and quick actions. */
|
||||
export function Home({ ctx }: { ctx: SectionContext }) {
|
||||
const { jobs, loaded, error: jobsError, cancel } = useJobs();
|
||||
const { volumes, error: volumesError } = useVolumes();
|
||||
const [ask, setAsk] = useState<Ask | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [message, setMessage] = useState<Message | null>(null);
|
||||
const { refreshStatus, openSection, canOpen } = ctx;
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) return;
|
||||
const timer = setTimeout(() => setMessage(null), MESSAGE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [message]);
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
if (!ask || busy) return;
|
||||
setBusy(true);
|
||||
runAsk(ask, { cancel })
|
||||
.then(
|
||||
(notice) => {
|
||||
setMessage(notice ? { tone: "info", text: notice } : null);
|
||||
if (ask.kind === "close") refreshStatus();
|
||||
},
|
||||
(e: unknown) => {
|
||||
const text = errorText(e);
|
||||
setMessage(text ? { tone: "error", text } : null);
|
||||
},
|
||||
)
|
||||
.finally(() => {
|
||||
setBusy(false);
|
||||
setAsk(null);
|
||||
});
|
||||
}, [ask, busy, cancel, refreshStatus]);
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
if (!busy) setAsk(null);
|
||||
}, [busy]);
|
||||
|
||||
const usb = volumes ? firstUsbVolume(volumes) : null;
|
||||
const onBrowseUsb = usb && canOpen("files") ? () => openSection("files", { path: usb.path }) : null;
|
||||
|
||||
return (
|
||||
<HomeView
|
||||
status={ctx.status}
|
||||
volumes={volumes}
|
||||
volumesError={volumesError}
|
||||
jobs={jobs}
|
||||
jobsLoaded={loaded}
|
||||
jobsError={jobsError != null}
|
||||
ask={ask}
|
||||
busy={busy}
|
||||
message={message}
|
||||
onAsk={(a) => {
|
||||
setMessage(null);
|
||||
setAsk(a);
|
||||
}}
|
||||
onConfirm={onConfirm}
|
||||
onDismiss={onDismiss}
|
||||
onBrowseUsb={onBrowseUsb}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CircleX, Copy, Download, FolderCog, ListTodo, Upload, type LucideIcon } from "lucide-react";
|
||||
import { formatBytes } from "../../lib/format";
|
||||
import { t } from "../strings";
|
||||
import type { Job } from "./useJobs";
|
||||
|
||||
const KIND_ICON: Record<Job["kind"], LucideIcon> = {
|
||||
upload: Upload,
|
||||
download: Download,
|
||||
copy: Copy,
|
||||
op: FolderCog,
|
||||
other: ListTodo,
|
||||
};
|
||||
|
||||
export function kindLabel(kind: Job["kind"]): string {
|
||||
return t(`cw_home_kind_${KIND_ICON[kind] ? kind : "other"}`);
|
||||
}
|
||||
|
||||
/** 0–100 from bytes, else from files; null when the job reports neither. */
|
||||
export function jobPercent(j: Job): number | null {
|
||||
const [done, total] = j.bytes_total > 0 ? [j.bytes_done, j.bytes_total] : [j.files_done, j.files_total];
|
||||
if (!(total > 0)) return null;
|
||||
return Math.max(0, Math.min(100, Math.floor((done / total) * 100)));
|
||||
}
|
||||
|
||||
function progressText(j: Job): string {
|
||||
const parts: string[] = [];
|
||||
if (j.bytes_total > 0)
|
||||
parts.push(
|
||||
t("cw_home_progress_bytes", {
|
||||
done: formatBytes(j.bytes_done),
|
||||
total: formatBytes(j.bytes_total),
|
||||
}),
|
||||
);
|
||||
if (j.files_total > 0) parts.push(t("cw_home_progress_files", { done: j.files_done, total: j.files_total }));
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/**
|
||||
* One task on Home: what it is, who started it, how far it is. The row itself
|
||||
* is focusable so the D-pad reaches (and scrolls to) every task; a running one
|
||||
* has a Cancel button, which only asks (Home shows the confirm).
|
||||
*/
|
||||
export function TaskRow({ job, onCancel }: { job: Job; onCancel: (job: Job) => void }) {
|
||||
const Icon = KIND_ICON[job.kind] ?? ListTodo;
|
||||
const pct = jobPercent(job);
|
||||
const running = job.state === "running";
|
||||
const progress = progressText(job);
|
||||
return (
|
||||
<li className={`cw-task is-${job.state}`}>
|
||||
<div className="cw-focusable cw-task-main" tabIndex={0}>
|
||||
<Icon className="cw-task-icon" aria-hidden />
|
||||
<div className="cw-task-text">
|
||||
<div className="cw-task-title">
|
||||
<span>{kindLabel(job.kind)}</span>
|
||||
<span className={`cw-tag${job.mine ? "" : " is-pc"}`}>
|
||||
{t(job.mine ? "cw_task_from_console" : "cw_task_from_pc")}
|
||||
</span>
|
||||
</div>
|
||||
{running ? (
|
||||
<div
|
||||
className="cw-bar"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={pct ?? undefined}
|
||||
>
|
||||
<div className={`cw-bar-fill${pct == null ? " is-unknown" : ""}`} style={{ width: `${pct ?? 100}%` }} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="cw-task-sub">
|
||||
{running
|
||||
? pct != null
|
||||
? t("cw_home_percent", { n: pct })
|
||||
: t("cw_home_state_running")
|
||||
: t(job.state === "done" ? "cw_home_state_done" : "cw_home_state_failed")}
|
||||
{progress ? <span className="cw-dim">{` · ${progress}`}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{running ? (
|
||||
<button type="button" className="cw-focusable cw-button" data-action="cancel" onClick={() => onCancel(job)}>
|
||||
<CircleX aria-hidden />
|
||||
<span>{t("cw_home_cancel_task")}</span>
|
||||
</button>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../api", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("../api")>();
|
||||
return { ...real, call: vi.fn() };
|
||||
});
|
||||
|
||||
import { ApiError, call } from "../api";
|
||||
import { cancelJob, createJobsPoller, JOBS_FAST_MS, JOBS_SLOW_MS, nextJobsDelay, type Job } from "./useJobs";
|
||||
|
||||
const mockCall = vi.mocked(call);
|
||||
|
||||
const job = (state: Job["state"], over: Partial<Job> = {}): Job => ({
|
||||
job: "0123456789abcdef0123456789abcdef",
|
||||
kind: "copy",
|
||||
state,
|
||||
files_done: 1,
|
||||
files_total: 4,
|
||||
bytes_done: 100,
|
||||
bytes_total: 400,
|
||||
mine: true,
|
||||
...over,
|
||||
});
|
||||
|
||||
const jobsCalls = () => mockCall.mock.calls.filter((c) => c[0] === "/api/jobs").length;
|
||||
|
||||
describe("useJobs poller", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockCall.mockReset();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("polls_fast_while_running_then_slow", async () => {
|
||||
const replies = [[job("running")], [job("done")], [job("done")]];
|
||||
mockCall.mockImplementation(async () => ({ jobs: replies.shift() ?? [] }));
|
||||
const seen: Job[][] = [];
|
||||
const p = createJobsPoller({
|
||||
onJobs: (j) => seen.push(j),
|
||||
onError: () => {},
|
||||
});
|
||||
p.start();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(jobsCalls()).toBe(1);
|
||||
expect(seen[0][0].state).toBe("running");
|
||||
|
||||
// something runs: the next read comes after JOBS_FAST_MS
|
||||
await vi.advanceTimersByTimeAsync(JOBS_FAST_MS - 1);
|
||||
expect(jobsCalls()).toBe(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(jobsCalls()).toBe(2);
|
||||
|
||||
// nothing runs any more: the poller backs off to JOBS_SLOW_MS
|
||||
await vi.advanceTimersByTimeAsync(JOBS_FAST_MS);
|
||||
expect(jobsCalls()).toBe(2);
|
||||
await vi.advanceTimersByTimeAsync(JOBS_SLOW_MS - JOBS_FAST_MS);
|
||||
expect(jobsCalls()).toBe(3);
|
||||
p.stop();
|
||||
});
|
||||
|
||||
it("stops polling once stopped (the section unmounted)", async () => {
|
||||
mockCall.mockResolvedValue({ jobs: [job("running")] });
|
||||
const p = createJobsPoller({ onJobs: () => {}, onError: () => {} });
|
||||
p.start();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
p.stop();
|
||||
await vi.advanceTimersByTimeAsync(JOBS_SLOW_MS * 3);
|
||||
expect(jobsCalls()).toBe(1);
|
||||
});
|
||||
|
||||
it("reports errors and backs off; stops for good on 401", async () => {
|
||||
mockCall.mockRejectedValueOnce(new ApiError(0, "offline")).mockRejectedValueOnce(new ApiError(401, "token"));
|
||||
const errors: unknown[] = [];
|
||||
const p = createJobsPoller({
|
||||
onJobs: () => {},
|
||||
onError: (e) => errors.push(e),
|
||||
});
|
||||
p.start();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(errors).toHaveLength(1);
|
||||
await vi.advanceTimersByTimeAsync(JOBS_FAST_MS);
|
||||
expect(jobsCalls()).toBe(1); // an error backs off, it does not hammer
|
||||
await vi.advanceTimersByTimeAsync(JOBS_SLOW_MS);
|
||||
expect(jobsCalls()).toBe(2);
|
||||
expect(errors).toHaveLength(2);
|
||||
await vi.advanceTimersByTimeAsync(JOBS_SLOW_MS * 3);
|
||||
expect(jobsCalls()).toBe(2); // the token is gone: every read would 401
|
||||
});
|
||||
|
||||
it("refresh reads at once without overlapping a read in flight", async () => {
|
||||
let release: (v: unknown) => void = () => {};
|
||||
mockCall.mockImplementationOnce(() => new Promise((r) => (release = r)));
|
||||
mockCall.mockResolvedValue({ jobs: [] });
|
||||
const p = createJobsPoller({ onJobs: () => {}, onError: () => {} });
|
||||
p.start();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
p.refresh();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(jobsCalls()).toBe(1); // still waiting for the first answer
|
||||
release({ jobs: [] });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(jobsCalls()).toBe(2); // the refresh ran right after it
|
||||
p.stop();
|
||||
});
|
||||
|
||||
it("delay: fast only while some job is running", () => {
|
||||
expect(nextJobsDelay([])).toBe(JOBS_SLOW_MS);
|
||||
expect(nextJobsDelay([job("done"), job("failed")])).toBe(JOBS_SLOW_MS);
|
||||
expect(nextJobsDelay([job("done"), job("running")])).toBe(JOBS_FAST_MS);
|
||||
expect(JOBS_FAST_MS).toBe(1000);
|
||||
expect(JOBS_SLOW_MS).toBe(5000);
|
||||
});
|
||||
|
||||
it("cancelJob posts the job id", async () => {
|
||||
mockCall.mockResolvedValue({});
|
||||
await cancelJob("abc");
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/jobs/cancel", { job: "abc" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* The helper's jobs, every owner's (the PC app's uploads and copies as well as
|
||||
* the page's own), read from /api/jobs: about once a second while one runs,
|
||||
* every five seconds otherwise. The poller stops when the section unmounts and
|
||||
* for good on a 401 (the token is gone; the reload banner takes over).
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ApiError, call } from "../api";
|
||||
|
||||
/** One /api/jobs entry (payload/src/webui_codec.c webui_jobs_to_json). */
|
||||
export interface Job {
|
||||
/** 32 lowercase hex chars. */
|
||||
job: string;
|
||||
kind: "upload" | "download" | "copy" | "op" | "other";
|
||||
state: "running" | "done" | "failed";
|
||||
files_done: number;
|
||||
files_total: number;
|
||||
bytes_done: number;
|
||||
bytes_total: number;
|
||||
/** Started from this page (false: a paired PC's job). */
|
||||
mine: boolean;
|
||||
}
|
||||
|
||||
export const JOBS_FAST_MS = 1000;
|
||||
export const JOBS_SLOW_MS = 5000;
|
||||
|
||||
/** How long until the next read after `jobs`. */
|
||||
export function nextJobsDelay(jobs: Job[]): number {
|
||||
return jobs.some((j) => j.state === "running") ? JOBS_FAST_MS : JOBS_SLOW_MS;
|
||||
}
|
||||
|
||||
async function fetchJobs(): Promise<Job[]> {
|
||||
const r = await call<{ jobs?: unknown }>("/api/jobs");
|
||||
return Array.isArray(r?.jobs) ? (r.jobs as Job[]) : [];
|
||||
}
|
||||
|
||||
/** Ask the helper to stop job `id` (any owner's). */
|
||||
export async function cancelJob(id: string): Promise<void> {
|
||||
await call("/api/jobs/cancel", { job: id });
|
||||
}
|
||||
|
||||
export interface JobsPoller {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
/** Read now (after a cancel), never two reads at once. */
|
||||
refresh(): void;
|
||||
}
|
||||
|
||||
/** The polling loop behind useJobs, with no React in it. */
|
||||
export function createJobsPoller(opts: { onJobs: (jobs: Job[]) => void; onError: (err: unknown) => void }): JobsPoller {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let stopped = false;
|
||||
let inFlight = false;
|
||||
let again = false;
|
||||
|
||||
const schedule = (ms: number) => {
|
||||
if (stopped) return;
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
timer = setTimeout(tick, ms);
|
||||
};
|
||||
|
||||
async function tick(): Promise<void> {
|
||||
timer = undefined;
|
||||
if (stopped) return;
|
||||
if (inFlight) {
|
||||
again = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
let delay = JOBS_SLOW_MS;
|
||||
try {
|
||||
const jobs = await fetchJobs();
|
||||
if (stopped) return;
|
||||
opts.onJobs(jobs);
|
||||
delay = nextJobsDelay(jobs);
|
||||
} catch (e) {
|
||||
if (stopped) return;
|
||||
opts.onError(e);
|
||||
if (e instanceof ApiError && e.status === 401) {
|
||||
stopped = true;
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
if (again) {
|
||||
again = false;
|
||||
delay = 0;
|
||||
}
|
||||
schedule(delay);
|
||||
}
|
||||
|
||||
return {
|
||||
start() {
|
||||
schedule(0);
|
||||
},
|
||||
stop() {
|
||||
stopped = true;
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
timer = undefined;
|
||||
},
|
||||
refresh() {
|
||||
if (stopped) return;
|
||||
if (inFlight) again = true;
|
||||
else schedule(0);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface JobsState {
|
||||
jobs: Job[];
|
||||
/** The first answer has come in. */
|
||||
loaded: boolean;
|
||||
/** The last read failed (null after a good one). */
|
||||
error: unknown;
|
||||
/** Stop job `id`; throws the ApiError when the helper refuses. Re-reads either way. */
|
||||
cancel(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
export function useJobs(): JobsState {
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState<unknown>(null);
|
||||
const poller = useRef<JobsPoller | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const p = createJobsPoller({
|
||||
onJobs: (j) => {
|
||||
setJobs(j);
|
||||
setLoaded(true);
|
||||
setError(null);
|
||||
},
|
||||
onError: setError,
|
||||
});
|
||||
poller.current = p;
|
||||
p.start();
|
||||
return () => {
|
||||
p.stop();
|
||||
if (poller.current === p) poller.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const cancel = useCallback(async (id: string) => {
|
||||
try {
|
||||
await cancelJob(id);
|
||||
} finally {
|
||||
poller.current?.refresh();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { jobs, loaded, error, cancel };
|
||||
}
|
||||
@@ -1,13 +1,34 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Folder, House, Settings, type LucideIcon } from "lucide-react";
|
||||
import { Home } from "./home/Home";
|
||||
import type { ConsoleStatus } from "./shell/TopBar";
|
||||
import { t } from "./strings";
|
||||
|
||||
/** What a section is opened with, e.g. Files at {path: "/mnt/usb0"}. */
|
||||
export type SectionParams = Record<string, string>;
|
||||
|
||||
/** What App hands every section. */
|
||||
export interface SectionContext {
|
||||
/** The last /api/status answer (App polls it for the console chip). */
|
||||
status: ConsoleStatus | null;
|
||||
/** Read /api/status again now (after closing a game). */
|
||||
refreshStatus: () => void;
|
||||
/** Switch to section `id`, opened with `params`. */
|
||||
openSection: (id: string, params?: SectionParams) => void;
|
||||
/** Whether section `id` exists and reads params (e.g. Files at a path). */
|
||||
canOpen: (id: string) => boolean;
|
||||
/** The params this section was opened with, or null. */
|
||||
params: SectionParams | null;
|
||||
}
|
||||
|
||||
/** One entry of the Rail. Only implemented sections are listed. */
|
||||
export interface Section {
|
||||
id: string;
|
||||
icon: LucideIcon;
|
||||
labelKey: string;
|
||||
render: () => ReactNode;
|
||||
render: (ctx: SectionContext) => ReactNode;
|
||||
/** It reads ctx.params, so other sections may open it at a place (Home's Browse USB → Files). */
|
||||
acceptsParams?: boolean;
|
||||
}
|
||||
|
||||
/** A section whose screen is not built yet: its heading only. */
|
||||
@@ -15,10 +36,22 @@ function Placeholder({ labelKey }: { labelKey: string }) {
|
||||
return <h1 className="cw-h1">{t(labelKey)}</h1>;
|
||||
}
|
||||
|
||||
// Tasks 8–10 replace these renders with the Home, Files and Settings screens.
|
||||
// Tasks 9–10 replace these renders with the Files and Settings screens. Files
|
||||
// sets acceptsParams once it opens at ctx.params.path: until then Home hides
|
||||
// Browse USB.
|
||||
export const SECTIONS: Section[] = [
|
||||
{ id: "home", icon: House, labelKey: "cw_nav_home", render: () => <Placeholder labelKey="cw_nav_home" /> },
|
||||
{ id: "files", icon: Folder, labelKey: "cw_nav_files", render: () => <Placeholder labelKey="cw_nav_files" /> },
|
||||
{
|
||||
id: "home",
|
||||
icon: House,
|
||||
labelKey: "cw_nav_home",
|
||||
render: (ctx) => <Home ctx={ctx} />,
|
||||
},
|
||||
{
|
||||
id: "files",
|
||||
icon: Folder,
|
||||
labelKey: "cw_nav_files",
|
||||
render: () => <Placeholder labelKey="cw_nav_files" />,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
icon: Settings,
|
||||
|
||||
+1113
-21
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { focusElement, onNavAction, type NavAction } from "../nav";
|
||||
import { t } from "../strings";
|
||||
|
||||
/**
|
||||
* The dialog's controller handling: ○ is "no". Every other action is swallowed
|
||||
* while it is open (the page behind never sees them anyway: nav.ts scopes
|
||||
* handlers to the open modal).
|
||||
*/
|
||||
export function dialogNavHandler(onCancel: () => void): (action: NavAction) => boolean {
|
||||
return (action) => {
|
||||
if (action === "back") onCancel();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "Are you sure?" for anything that cannot be undone: close a game, cancel a
|
||||
* task, rest mode, restart, delete. A nav modal: the D-pad stays inside it,
|
||||
* focus starts on the safe choice (listed first), ○ closes it, and focus goes
|
||||
* back where it was when it closes.
|
||||
*/
|
||||
export function ConfirmDialog({
|
||||
title,
|
||||
body,
|
||||
confirmLabel,
|
||||
cancelLabel,
|
||||
danger,
|
||||
busy,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
title: string;
|
||||
body?: string;
|
||||
confirmLabel: string;
|
||||
/** Default: "Cancel". */
|
||||
cancelLabel?: string;
|
||||
/** The confirm button in the warning colour. */
|
||||
danger?: boolean;
|
||||
/** The action is running: a second ✕ does nothing. */
|
||||
busy?: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const root = useRef<HTMLDivElement>(null);
|
||||
const safe = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const before = document.activeElement as HTMLElement | null;
|
||||
if (safe.current) focusElement(safe.current);
|
||||
return () => {
|
||||
if (before && before !== document.body && before.isConnected) focusElement(before);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => onNavAction(dialogNavHandler(onCancel), () => root.current), [onCancel]);
|
||||
|
||||
return (
|
||||
<div className="cw-scrim">
|
||||
<div
|
||||
ref={root}
|
||||
className="cw-dialog"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cw-dialog-title"
|
||||
aria-describedby={body ? "cw-dialog-body" : undefined}
|
||||
aria-busy={busy ? true : undefined}
|
||||
data-nav-modal
|
||||
>
|
||||
<h2 id="cw-dialog-title">{title}</h2>
|
||||
{body ? <p id="cw-dialog-body">{body}</p> : null}
|
||||
<div className="cw-dialog-buttons">
|
||||
<button ref={safe} type="button" className="cw-focusable cw-button" data-nav-default onClick={onCancel}>
|
||||
<span>{cancelLabel ?? t("cw_dialog_cancel")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`cw-focusable cw-button ${danger ? "is-danger" : "is-primary"}`}
|
||||
onClick={() => {
|
||||
if (!busy) onConfirm();
|
||||
}}
|
||||
>
|
||||
<span>{confirmLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4046,6 +4046,58 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
|
||||
"cw_status_connecting": "جارٍ الاتصال…",
|
||||
"cw_status_offline": "غير متصل",
|
||||
"cw_hints_label": "أزرار وحدة التحكم",
|
||||
"cw_task_from_console": "من هذا الجهاز",
|
||||
"cw_task_from_pc": "من حاسوب",
|
||||
"cw_dialog_cancel": "إلغاء",
|
||||
"cw_home_kind_upload": "رفع",
|
||||
"cw_home_kind_download": "تنزيل",
|
||||
"cw_home_kind_copy": "نسخ",
|
||||
"cw_home_kind_op": "عملية ملفات",
|
||||
"cw_home_kind_other": "مهمة",
|
||||
"cw_home_progress_bytes": "{done} من {total}",
|
||||
"cw_home_progress_files": "{done} من {total} ملفات",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "قيد التشغيل",
|
||||
"cw_home_state_done": "اكتملت",
|
||||
"cw_home_state_failed": "فشلت",
|
||||
"cw_home_cancel_task": "إلغاء",
|
||||
"cw_home_vol_internal": "التخزين الداخلي",
|
||||
"cw_home_vol_usb": "محرك USB",
|
||||
"cw_home_vol_usb_ext": "تخزين موسّع عبر USB",
|
||||
"cw_home_vol_m2": "تخزين M.2",
|
||||
"cw_home_vol_ext": "تخزين موسّع",
|
||||
"cw_home_vol_readonly": "للقراءة فقط",
|
||||
"cw_home_free_of": "{free} متاحة من {total}",
|
||||
"cw_home_err_offline": "لم يستجب الجهاز. حاول مرة أخرى.",
|
||||
"cw_home_err_busy": "الجهاز مشغول. حاول مرة أخرى بعد قليل.",
|
||||
"cw_home_err_gone": "انتهت هذه المهمة بالفعل.",
|
||||
"cw_home_err_failed": "لم ينجح ذلك ({code}).",
|
||||
"cw_home_err_load_tasks": "تعذّرت قراءة قائمة المهام.",
|
||||
"cw_home_err_load_storage": "تعذّرت قراءة المحركات.",
|
||||
"cw_home_rest_sent": "ينتقل الجهاز إلى وضع الراحة.",
|
||||
"cw_home_restart_sent": "يُعاد تشغيل الجهاز. أعد تحميل هذه الصفحة عند عودته.",
|
||||
"cw_home_confirm_cancel_title": "إلغاء هذه المهمة؟",
|
||||
"cw_home_confirm_cancel_body": "ستتوقف الآن. لا يمكن التراجع عن ذلك.",
|
||||
"cw_home_confirm_cancel_body_pc": "بدأ حاسوب هذه المهمة. ستتوقف على الحاسوب أيضًا.",
|
||||
"cw_home_cancel_task_confirm": "إلغاء المهمة",
|
||||
"cw_home_keep_task": "متابعة",
|
||||
"cw_home_confirm_close_title": "إغلاق {name}؟",
|
||||
"cw_home_confirm_close_body": "سيضيع التقدم غير المحفوظ في اللعبة.",
|
||||
"cw_home_close_game": "إغلاق اللعبة",
|
||||
"cw_home_confirm_rest_title": "وضع الجهاز في وضع الراحة؟",
|
||||
"cw_home_confirm_rest_body": "تنقطع هذه الصفحة حتى يستيقظ الجهاز.",
|
||||
"cw_home_rest": "وضع الراحة",
|
||||
"cw_home_confirm_restart_title": "إعادة تشغيل الجهاز؟",
|
||||
"cw_home_confirm_restart_body": "تتوقف الألعاب والمهام الجارية. تنقطع هذه الصفحة حتى يعود الجهاز.",
|
||||
"cw_home_restart": "إعادة التشغيل",
|
||||
"cw_home_loading": "جارٍ التحميل…",
|
||||
"cw_home_no_tasks": "لا توجد مهام حاليًا.",
|
||||
"cw_home_quick": "إجراءات سريعة",
|
||||
"cw_home_browse_usb": "تصفح USB",
|
||||
"cw_home_now_running": "قيد التشغيل الآن",
|
||||
"cw_home_no_game": "لا توجد لعبة قيد التشغيل.",
|
||||
"cw_home_tasks": "المهام",
|
||||
"cw_home_storage": "التخزين",
|
||||
};
|
||||
|
||||
export default ar;
|
||||
@@ -4046,6 +4046,58 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
|
||||
"cw_status_connecting": "সংযোগ হচ্ছে…",
|
||||
"cw_status_offline": "সংযুক্ত নয়",
|
||||
"cw_hints_label": "কন্ট্রোলারের বোতাম",
|
||||
"cw_task_from_console": "এই কনসোল থেকে",
|
||||
"cw_task_from_pc": "PC থেকে",
|
||||
"cw_dialog_cancel": "বাতিল",
|
||||
"cw_home_kind_upload": "আপলোড",
|
||||
"cw_home_kind_download": "ডাউনলোড",
|
||||
"cw_home_kind_copy": "কপি",
|
||||
"cw_home_kind_op": "ফাইল অপারেশন",
|
||||
"cw_home_kind_other": "কাজ",
|
||||
"cw_home_progress_bytes": "{total}-এর মধ্যে {done}",
|
||||
"cw_home_progress_files": "{total}টির মধ্যে {done}টি ফাইল",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "চলছে",
|
||||
"cw_home_state_done": "সম্পন্ন",
|
||||
"cw_home_state_failed": "ব্যর্থ",
|
||||
"cw_home_cancel_task": "বাতিল",
|
||||
"cw_home_vol_internal": "অভ্যন্তরীণ স্টোরেজ",
|
||||
"cw_home_vol_usb": "USB ড্রাইভ",
|
||||
"cw_home_vol_usb_ext": "USB এক্সটেন্ডেড স্টোরেজ",
|
||||
"cw_home_vol_m2": "M.2 স্টোরেজ",
|
||||
"cw_home_vol_ext": "এক্সটেন্ডেড স্টোরেজ",
|
||||
"cw_home_vol_readonly": "শুধু পড়া যায়",
|
||||
"cw_home_free_of": "{total}-এর মধ্যে {free} খালি",
|
||||
"cw_home_err_offline": "কনসোল সাড়া দেয়নি। আবার চেষ্টা করুন।",
|
||||
"cw_home_err_busy": "কনসোল ব্যস্ত। একটু পরে আবার চেষ্টা করুন।",
|
||||
"cw_home_err_gone": "সেই কাজটি আগেই শেষ হয়েছে।",
|
||||
"cw_home_err_failed": "এটি কাজ করেনি ({code})।",
|
||||
"cw_home_err_load_tasks": "কাজের তালিকা পড়া যায়নি।",
|
||||
"cw_home_err_load_storage": "ড্রাইভগুলো পড়া যায়নি।",
|
||||
"cw_home_rest_sent": "কনসোল রেস্ট মোডে যাচ্ছে।",
|
||||
"cw_home_restart_sent": "কনসোল রিস্টার্ট হচ্ছে। ফিরে এলে এই পেজটি আবার লোড করুন।",
|
||||
"cw_home_confirm_cancel_title": "এই কাজটি বাতিল করবেন?",
|
||||
"cw_home_confirm_cancel_body": "এটি এখনই থেমে যাবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
|
||||
"cw_home_confirm_cancel_body_pc": "একটি PC এই কাজটি শুরু করেছিল। এটি PC-তেও থেমে যাবে।",
|
||||
"cw_home_cancel_task_confirm": "কাজ বাতিল করুন",
|
||||
"cw_home_keep_task": "চলতে দিন",
|
||||
"cw_home_confirm_close_title": "{name} বন্ধ করবেন?",
|
||||
"cw_home_confirm_close_body": "গেমের সেভ না করা অগ্রগতি হারিয়ে যাবে।",
|
||||
"cw_home_close_game": "গেম বন্ধ করুন",
|
||||
"cw_home_confirm_rest_title": "কনসোলকে রেস্ট মোডে রাখবেন?",
|
||||
"cw_home_confirm_rest_body": "কনসোল জেগে না ওঠা পর্যন্ত এই পেজ বিচ্ছিন্ন থাকবে।",
|
||||
"cw_home_rest": "রেস্ট মোড",
|
||||
"cw_home_confirm_restart_title": "কনসোল রিস্টার্ট করবেন?",
|
||||
"cw_home_confirm_restart_body": "চলমান গেম ও কাজ থেমে যাবে। কনসোল ফিরে না আসা পর্যন্ত এই পেজ বিচ্ছিন্ন থাকবে।",
|
||||
"cw_home_restart": "রিস্টার্ট",
|
||||
"cw_home_loading": "লোড হচ্ছে…",
|
||||
"cw_home_no_tasks": "এখন কোনো কাজ নেই।",
|
||||
"cw_home_quick": "দ্রুত কাজ",
|
||||
"cw_home_browse_usb": "USB ব্রাউজ করুন",
|
||||
"cw_home_now_running": "এখন চলছে",
|
||||
"cw_home_no_game": "কোনো গেম চলছে না।",
|
||||
"cw_home_tasks": "কাজ",
|
||||
"cw_home_storage": "স্টোরেজ",
|
||||
};
|
||||
|
||||
export default bn;
|
||||
@@ -4054,6 +4054,58 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}",
|
||||
"cw_status_connecting": "Verbinde…",
|
||||
"cw_status_offline": "Nicht verbunden",
|
||||
"cw_hints_label": "Controller-Tasten",
|
||||
"cw_task_from_console": "Von dieser Konsole",
|
||||
"cw_task_from_pc": "Von einem PC",
|
||||
"cw_dialog_cancel": "Abbrechen",
|
||||
"cw_home_kind_upload": "Hochladen",
|
||||
"cw_home_kind_download": "Herunterladen",
|
||||
"cw_home_kind_copy": "Kopieren",
|
||||
"cw_home_kind_op": "Dateivorgang",
|
||||
"cw_home_kind_other": "Aufgabe",
|
||||
"cw_home_progress_bytes": "{done} von {total}",
|
||||
"cw_home_progress_files": "{done} von {total} Dateien",
|
||||
"cw_home_percent": "{n} %",
|
||||
"cw_home_state_running": "Läuft",
|
||||
"cw_home_state_done": "Fertig",
|
||||
"cw_home_state_failed": "Fehlgeschlagen",
|
||||
"cw_home_cancel_task": "Abbrechen",
|
||||
"cw_home_vol_internal": "Interner Speicher",
|
||||
"cw_home_vol_usb": "USB-Laufwerk",
|
||||
"cw_home_vol_usb_ext": "USB-Erweiterungsspeicher",
|
||||
"cw_home_vol_m2": "M.2-Speicher",
|
||||
"cw_home_vol_ext": "Erweiterungsspeicher",
|
||||
"cw_home_vol_readonly": "Schreibgeschützt",
|
||||
"cw_home_free_of": "{free} frei von {total}",
|
||||
"cw_home_err_offline": "Die Konsole hat nicht geantwortet. Versuche es erneut.",
|
||||
"cw_home_err_busy": "Die Konsole ist beschäftigt. Versuche es gleich noch einmal.",
|
||||
"cw_home_err_gone": "Diese Aufgabe ist bereits beendet.",
|
||||
"cw_home_err_failed": "Das hat nicht funktioniert ({code}).",
|
||||
"cw_home_err_load_tasks": "Die Aufgabenliste konnte nicht gelesen werden.",
|
||||
"cw_home_err_load_storage": "Die Laufwerke konnten nicht gelesen werden.",
|
||||
"cw_home_rest_sent": "Die Konsole wechselt in den Ruhemodus.",
|
||||
"cw_home_restart_sent": "Die Konsole startet neu. Lade diese Seite neu, sobald sie wieder da ist.",
|
||||
"cw_home_confirm_cancel_title": "Diese Aufgabe abbrechen?",
|
||||
"cw_home_confirm_cancel_body": "Sie stoppt sofort. Das lässt sich nicht rückgängig machen.",
|
||||
"cw_home_confirm_cancel_body_pc": "Ein PC hat diese Aufgabe gestartet. Sie stoppt auch auf dem PC.",
|
||||
"cw_home_cancel_task_confirm": "Aufgabe abbrechen",
|
||||
"cw_home_keep_task": "Weiterlaufen lassen",
|
||||
"cw_home_confirm_close_title": "{name} schließen?",
|
||||
"cw_home_confirm_close_body": "Nicht gespeicherter Spielfortschritt geht verloren.",
|
||||
"cw_home_close_game": "Spiel schließen",
|
||||
"cw_home_confirm_rest_title": "Konsole in den Ruhemodus versetzen?",
|
||||
"cw_home_confirm_rest_body": "Diese Seite ist getrennt, bis die Konsole aufwacht.",
|
||||
"cw_home_rest": "Ruhemodus",
|
||||
"cw_home_confirm_restart_title": "Konsole neu starten?",
|
||||
"cw_home_confirm_restart_body": "Laufende Spiele und Aufgaben werden beendet. Diese Seite ist getrennt, bis die Konsole wieder da ist.",
|
||||
"cw_home_restart": "Neu starten",
|
||||
"cw_home_loading": "Wird geladen…",
|
||||
"cw_home_no_tasks": "Gerade keine Aufgaben.",
|
||||
"cw_home_quick": "Schnellaktionen",
|
||||
"cw_home_browse_usb": "USB durchsuchen",
|
||||
"cw_home_now_running": "Läuft gerade",
|
||||
"cw_home_no_game": "Es läuft kein Spiel.",
|
||||
"cw_home_tasks": "Aufgaben",
|
||||
"cw_home_storage": "Speicher",
|
||||
};
|
||||
|
||||
export default de;
|
||||
@@ -4369,6 +4369,58 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}",
|
||||
"cw_status_connecting": "Connecting…",
|
||||
"cw_status_offline": "Not connected",
|
||||
"cw_hints_label": "Controller buttons",
|
||||
"cw_task_from_console": "From this console",
|
||||
"cw_task_from_pc": "From a PC",
|
||||
"cw_dialog_cancel": "Cancel",
|
||||
"cw_home_kind_upload": "Upload",
|
||||
"cw_home_kind_download": "Download",
|
||||
"cw_home_kind_copy": "Copy",
|
||||
"cw_home_kind_op": "File operation",
|
||||
"cw_home_kind_other": "Task",
|
||||
"cw_home_progress_bytes": "{done} of {total}",
|
||||
"cw_home_progress_files": "{done} of {total} files",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "Running",
|
||||
"cw_home_state_done": "Done",
|
||||
"cw_home_state_failed": "Failed",
|
||||
"cw_home_cancel_task": "Cancel",
|
||||
"cw_home_vol_internal": "Internal storage",
|
||||
"cw_home_vol_usb": "USB drive",
|
||||
"cw_home_vol_usb_ext": "USB extended storage",
|
||||
"cw_home_vol_m2": "M.2 storage",
|
||||
"cw_home_vol_ext": "Extended storage",
|
||||
"cw_home_vol_readonly": "Read-only",
|
||||
"cw_home_free_of": "{free} free of {total}",
|
||||
"cw_home_err_offline": "The console did not answer. Try again.",
|
||||
"cw_home_err_busy": "The console is busy. Try again in a moment.",
|
||||
"cw_home_err_gone": "That task has already ended.",
|
||||
"cw_home_err_failed": "That did not work ({code}).",
|
||||
"cw_home_err_load_tasks": "Could not read the task list.",
|
||||
"cw_home_err_load_storage": "Could not read the drives.",
|
||||
"cw_home_rest_sent": "The console is going into rest mode.",
|
||||
"cw_home_restart_sent": "The console is restarting. Reload this page when it is back.",
|
||||
"cw_home_confirm_cancel_title": "Cancel this task?",
|
||||
"cw_home_confirm_cancel_body": "It stops now. This cannot be undone.",
|
||||
"cw_home_confirm_cancel_body_pc": "A PC started this task. It stops on the PC too.",
|
||||
"cw_home_cancel_task_confirm": "Cancel task",
|
||||
"cw_home_keep_task": "Keep going",
|
||||
"cw_home_confirm_close_title": "Close {name}?",
|
||||
"cw_home_confirm_close_body": "Progress you have not saved in the game is lost.",
|
||||
"cw_home_close_game": "Close game",
|
||||
"cw_home_confirm_rest_title": "Put the console in rest mode?",
|
||||
"cw_home_confirm_rest_body": "This page disconnects until the console wakes up.",
|
||||
"cw_home_rest": "Rest mode",
|
||||
"cw_home_confirm_restart_title": "Restart the console?",
|
||||
"cw_home_confirm_restart_body": "Running games and tasks stop. This page disconnects until the console is back.",
|
||||
"cw_home_restart": "Restart",
|
||||
"cw_home_loading": "Loading…",
|
||||
"cw_home_no_tasks": "No tasks right now.",
|
||||
"cw_home_quick": "Quick actions",
|
||||
"cw_home_browse_usb": "Browse USB",
|
||||
"cw_home_now_running": "Now running",
|
||||
"cw_home_no_game": "No game is running.",
|
||||
"cw_home_tasks": "Tasks",
|
||||
"cw_home_storage": "Storage",
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -3996,6 +3996,58 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}",
|
||||
"cw_status_connecting": "Conectando…",
|
||||
"cw_status_offline": "Sin conexión",
|
||||
"cw_hints_label": "Botones del mando",
|
||||
"cw_task_from_console": "Desde esta consola",
|
||||
"cw_task_from_pc": "Desde un PC",
|
||||
"cw_dialog_cancel": "Cancelar",
|
||||
"cw_home_kind_upload": "Subida",
|
||||
"cw_home_kind_download": "Descarga",
|
||||
"cw_home_kind_copy": "Copia",
|
||||
"cw_home_kind_op": "Operación de archivos",
|
||||
"cw_home_kind_other": "Tarea",
|
||||
"cw_home_progress_bytes": "{done} de {total}",
|
||||
"cw_home_progress_files": "{done} de {total} archivos",
|
||||
"cw_home_percent": "{n} %",
|
||||
"cw_home_state_running": "En curso",
|
||||
"cw_home_state_done": "Hecho",
|
||||
"cw_home_state_failed": "Error",
|
||||
"cw_home_cancel_task": "Cancelar",
|
||||
"cw_home_vol_internal": "Almacenamiento interno",
|
||||
"cw_home_vol_usb": "Unidad USB",
|
||||
"cw_home_vol_usb_ext": "Almacenamiento ampliado USB",
|
||||
"cw_home_vol_m2": "Almacenamiento M.2",
|
||||
"cw_home_vol_ext": "Almacenamiento ampliado",
|
||||
"cw_home_vol_readonly": "Solo lectura",
|
||||
"cw_home_free_of": "{free} libres de {total}",
|
||||
"cw_home_err_offline": "La consola no respondió. Inténtalo de nuevo.",
|
||||
"cw_home_err_busy": "La consola está ocupada. Inténtalo de nuevo en un momento.",
|
||||
"cw_home_err_gone": "Esa tarea ya ha terminado.",
|
||||
"cw_home_err_failed": "No ha funcionado ({code}).",
|
||||
"cw_home_err_load_tasks": "No se pudo leer la lista de tareas.",
|
||||
"cw_home_err_load_storage": "No se pudieron leer las unidades.",
|
||||
"cw_home_rest_sent": "La consola está entrando en modo de reposo.",
|
||||
"cw_home_restart_sent": "La consola se está reiniciando. Recarga esta página cuando vuelva.",
|
||||
"cw_home_confirm_cancel_title": "¿Cancelar esta tarea?",
|
||||
"cw_home_confirm_cancel_body": "Se detiene ahora. No se puede deshacer.",
|
||||
"cw_home_confirm_cancel_body_pc": "Un PC inició esta tarea. También se detiene en el PC.",
|
||||
"cw_home_cancel_task_confirm": "Cancelar tarea",
|
||||
"cw_home_keep_task": "Continuar",
|
||||
"cw_home_confirm_close_title": "¿Cerrar {name}?",
|
||||
"cw_home_confirm_close_body": "Se perderá el progreso no guardado del juego.",
|
||||
"cw_home_close_game": "Cerrar juego",
|
||||
"cw_home_confirm_rest_title": "¿Poner la consola en modo de reposo?",
|
||||
"cw_home_confirm_rest_body": "Esta página se desconecta hasta que la consola se active.",
|
||||
"cw_home_rest": "Modo de reposo",
|
||||
"cw_home_confirm_restart_title": "¿Reiniciar la consola?",
|
||||
"cw_home_confirm_restart_body": "Los juegos y tareas en curso se detienen. Esta página se desconecta hasta que la consola vuelva.",
|
||||
"cw_home_restart": "Reiniciar",
|
||||
"cw_home_loading": "Cargando…",
|
||||
"cw_home_no_tasks": "No hay tareas ahora mismo.",
|
||||
"cw_home_quick": "Acciones rápidas",
|
||||
"cw_home_browse_usb": "Explorar USB",
|
||||
"cw_home_now_running": "En ejecución",
|
||||
"cw_home_no_game": "No hay ningún juego en ejecución.",
|
||||
"cw_home_tasks": "Tareas",
|
||||
"cw_home_storage": "Almacenamiento",
|
||||
};
|
||||
|
||||
export default es;
|
||||
@@ -4347,6 +4347,58 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
|
||||
"cw_status_connecting": "در حال اتصال…",
|
||||
"cw_status_offline": "متصل نیست",
|
||||
"cw_hints_label": "دکمههای دسته",
|
||||
"cw_task_from_console": "از این کنسول",
|
||||
"cw_task_from_pc": "از رایانه",
|
||||
"cw_dialog_cancel": "لغو",
|
||||
"cw_home_kind_upload": "بارگذاری",
|
||||
"cw_home_kind_download": "بارگیری",
|
||||
"cw_home_kind_copy": "کپی",
|
||||
"cw_home_kind_op": "عملیات فایل",
|
||||
"cw_home_kind_other": "کار",
|
||||
"cw_home_progress_bytes": "{done} از {total}",
|
||||
"cw_home_progress_files": "{done} از {total} فایل",
|
||||
"cw_home_percent": "{n}٪",
|
||||
"cw_home_state_running": "در حال اجرا",
|
||||
"cw_home_state_done": "انجام شد",
|
||||
"cw_home_state_failed": "ناموفق",
|
||||
"cw_home_cancel_task": "لغو",
|
||||
"cw_home_vol_internal": "حافظه داخلی",
|
||||
"cw_home_vol_usb": "درایو USB",
|
||||
"cw_home_vol_usb_ext": "حافظه توسعهیافته USB",
|
||||
"cw_home_vol_m2": "حافظه M.2",
|
||||
"cw_home_vol_ext": "حافظه توسعهیافته",
|
||||
"cw_home_vol_readonly": "فقطخواندنی",
|
||||
"cw_home_free_of": "{free} آزاد از {total}",
|
||||
"cw_home_err_offline": "کنسول پاسخ نداد. دوباره تلاش کنید.",
|
||||
"cw_home_err_busy": "کنسول مشغول است. کمی بعد دوباره تلاش کنید.",
|
||||
"cw_home_err_gone": "این کار قبلاً تمام شده است.",
|
||||
"cw_home_err_failed": "انجام نشد ({code}).",
|
||||
"cw_home_err_load_tasks": "فهرست کارها خوانده نشد.",
|
||||
"cw_home_err_load_storage": "درایوها خوانده نشدند.",
|
||||
"cw_home_rest_sent": "کنسول به حالت استراحت میرود.",
|
||||
"cw_home_restart_sent": "کنسول در حال راهاندازی مجدد است. وقتی برگشت این صفحه را دوباره بارگذاری کنید.",
|
||||
"cw_home_confirm_cancel_title": "این کار لغو شود؟",
|
||||
"cw_home_confirm_cancel_body": "همین حالا متوقف میشود. این کار برگشتپذیر نیست.",
|
||||
"cw_home_confirm_cancel_body_pc": "این کار را یک رایانه شروع کرده است. روی رایانه هم متوقف میشود.",
|
||||
"cw_home_cancel_task_confirm": "لغو کار",
|
||||
"cw_home_keep_task": "ادامه بده",
|
||||
"cw_home_confirm_close_title": "{name} بسته شود؟",
|
||||
"cw_home_confirm_close_body": "پیشرفت ذخیرهنشده در بازی از بین میرود.",
|
||||
"cw_home_close_game": "بستن بازی",
|
||||
"cw_home_confirm_rest_title": "کنسول به حالت استراحت برود؟",
|
||||
"cw_home_confirm_rest_body": "این صفحه تا بیدار شدن کنسول قطع میشود.",
|
||||
"cw_home_rest": "حالت استراحت",
|
||||
"cw_home_confirm_restart_title": "کنسول دوباره راهاندازی شود؟",
|
||||
"cw_home_confirm_restart_body": "بازیها و کارهای در حال اجرا متوقف میشوند. این صفحه تا برگشتن کنسول قطع میشود.",
|
||||
"cw_home_restart": "راهاندازی مجدد",
|
||||
"cw_home_loading": "در حال بارگذاری…",
|
||||
"cw_home_no_tasks": "در حال حاضر کاری نیست.",
|
||||
"cw_home_quick": "اقدامات سریع",
|
||||
"cw_home_browse_usb": "مرور USB",
|
||||
"cw_home_now_running": "در حال اجرا",
|
||||
"cw_home_no_game": "هیچ بازیای در حال اجرا نیست.",
|
||||
"cw_home_tasks": "کارها",
|
||||
"cw_home_storage": "حافظه",
|
||||
};
|
||||
|
||||
export default fa;
|
||||
@@ -4064,6 +4064,58 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}",
|
||||
"cw_status_connecting": "Connexion…",
|
||||
"cw_status_offline": "Non connecté",
|
||||
"cw_hints_label": "Boutons de la manette",
|
||||
"cw_task_from_console": "Depuis cette console",
|
||||
"cw_task_from_pc": "Depuis un PC",
|
||||
"cw_dialog_cancel": "Annuler",
|
||||
"cw_home_kind_upload": "Envoi",
|
||||
"cw_home_kind_download": "Téléchargement",
|
||||
"cw_home_kind_copy": "Copie",
|
||||
"cw_home_kind_op": "Opération sur les fichiers",
|
||||
"cw_home_kind_other": "Tâche",
|
||||
"cw_home_progress_bytes": "{done} sur {total}",
|
||||
"cw_home_progress_files": "{done} fichiers sur {total}",
|
||||
"cw_home_percent": "{n} %",
|
||||
"cw_home_state_running": "En cours",
|
||||
"cw_home_state_done": "Terminé",
|
||||
"cw_home_state_failed": "Échec",
|
||||
"cw_home_cancel_task": "Annuler",
|
||||
"cw_home_vol_internal": "Stockage interne",
|
||||
"cw_home_vol_usb": "Clé USB",
|
||||
"cw_home_vol_usb_ext": "Stockage étendu USB",
|
||||
"cw_home_vol_m2": "Stockage M.2",
|
||||
"cw_home_vol_ext": "Stockage étendu",
|
||||
"cw_home_vol_readonly": "Lecture seule",
|
||||
"cw_home_free_of": "{free} libres sur {total}",
|
||||
"cw_home_err_offline": "La console n'a pas répondu. Réessayez.",
|
||||
"cw_home_err_busy": "La console est occupée. Réessayez dans un instant.",
|
||||
"cw_home_err_gone": "Cette tâche est déjà terminée.",
|
||||
"cw_home_err_failed": "Cela n'a pas fonctionné ({code}).",
|
||||
"cw_home_err_load_tasks": "Impossible de lire la liste des tâches.",
|
||||
"cw_home_err_load_storage": "Impossible de lire les disques.",
|
||||
"cw_home_rest_sent": "La console passe en mode repos.",
|
||||
"cw_home_restart_sent": "La console redémarre. Rechargez cette page quand elle sera revenue.",
|
||||
"cw_home_confirm_cancel_title": "Annuler cette tâche ?",
|
||||
"cw_home_confirm_cancel_body": "Elle s'arrête maintenant. Cette action est irréversible.",
|
||||
"cw_home_confirm_cancel_body_pc": "Un PC a lancé cette tâche. Elle s'arrête aussi sur le PC.",
|
||||
"cw_home_cancel_task_confirm": "Annuler la tâche",
|
||||
"cw_home_keep_task": "Continuer",
|
||||
"cw_home_confirm_close_title": "Fermer {name} ?",
|
||||
"cw_home_confirm_close_body": "La progression non sauvegardée du jeu sera perdue.",
|
||||
"cw_home_close_game": "Fermer le jeu",
|
||||
"cw_home_confirm_rest_title": "Mettre la console en mode repos ?",
|
||||
"cw_home_confirm_rest_body": "Cette page est déconnectée jusqu'au réveil de la console.",
|
||||
"cw_home_rest": "Mode repos",
|
||||
"cw_home_confirm_restart_title": "Redémarrer la console ?",
|
||||
"cw_home_confirm_restart_body": "Les jeux et tâches en cours s'arrêtent. Cette page est déconnectée jusqu'au retour de la console.",
|
||||
"cw_home_restart": "Redémarrer",
|
||||
"cw_home_loading": "Chargement…",
|
||||
"cw_home_no_tasks": "Aucune tâche pour le moment.",
|
||||
"cw_home_quick": "Actions rapides",
|
||||
"cw_home_browse_usb": "Parcourir l'USB",
|
||||
"cw_home_now_running": "En cours d'exécution",
|
||||
"cw_home_no_game": "Aucun jeu en cours.",
|
||||
"cw_home_tasks": "Tâches",
|
||||
"cw_home_storage": "Stockage",
|
||||
};
|
||||
|
||||
export default fr;
|
||||
@@ -4062,6 +4062,58 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
|
||||
"cw_status_connecting": "कनेक्ट हो रहा है…",
|
||||
"cw_status_offline": "कनेक्ट नहीं है",
|
||||
"cw_hints_label": "कंट्रोलर के बटन",
|
||||
"cw_task_from_console": "इस कंसोल से",
|
||||
"cw_task_from_pc": "PC से",
|
||||
"cw_dialog_cancel": "रद्द करें",
|
||||
"cw_home_kind_upload": "अपलोड",
|
||||
"cw_home_kind_download": "डाउनलोड",
|
||||
"cw_home_kind_copy": "कॉपी",
|
||||
"cw_home_kind_op": "फ़ाइल कार्रवाई",
|
||||
"cw_home_kind_other": "कार्य",
|
||||
"cw_home_progress_bytes": "{total} में से {done}",
|
||||
"cw_home_progress_files": "{total} में से {done} फ़ाइलें",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "चल रहा है",
|
||||
"cw_home_state_done": "पूरा हुआ",
|
||||
"cw_home_state_failed": "विफल",
|
||||
"cw_home_cancel_task": "रद्द करें",
|
||||
"cw_home_vol_internal": "आंतरिक स्टोरेज",
|
||||
"cw_home_vol_usb": "USB ड्राइव",
|
||||
"cw_home_vol_usb_ext": "USB विस्तारित स्टोरेज",
|
||||
"cw_home_vol_m2": "M.2 स्टोरेज",
|
||||
"cw_home_vol_ext": "विस्तारित स्टोरेज",
|
||||
"cw_home_vol_readonly": "केवल पढ़ने योग्य",
|
||||
"cw_home_free_of": "{total} में से {free} खाली",
|
||||
"cw_home_err_offline": "कंसोल ने जवाब नहीं दिया। फिर से कोशिश करें।",
|
||||
"cw_home_err_busy": "कंसोल व्यस्त है। थोड़ी देर बाद फिर से कोशिश करें।",
|
||||
"cw_home_err_gone": "वह कार्य पहले ही खत्म हो चुका है।",
|
||||
"cw_home_err_failed": "यह काम नहीं किया ({code})।",
|
||||
"cw_home_err_load_tasks": "कार्य सूची पढ़ी नहीं जा सकी।",
|
||||
"cw_home_err_load_storage": "ड्राइव पढ़ी नहीं जा सकीं।",
|
||||
"cw_home_rest_sent": "कंसोल रेस्ट मोड में जा रहा है।",
|
||||
"cw_home_restart_sent": "कंसोल फिर से शुरू हो रहा है। वापस आने पर यह पेज फिर से लोड करें।",
|
||||
"cw_home_confirm_cancel_title": "यह कार्य रद्द करें?",
|
||||
"cw_home_confirm_cancel_body": "यह अभी रुक जाएगा। इसे पूर्ववत नहीं किया जा सकता।",
|
||||
"cw_home_confirm_cancel_body_pc": "यह कार्य एक PC ने शुरू किया था। यह PC पर भी रुक जाएगा।",
|
||||
"cw_home_cancel_task_confirm": "कार्य रद्द करें",
|
||||
"cw_home_keep_task": "चलने दें",
|
||||
"cw_home_confirm_close_title": "{name} बंद करें?",
|
||||
"cw_home_confirm_close_body": "गेम की बिना सहेजी प्रगति खो जाएगी।",
|
||||
"cw_home_close_game": "गेम बंद करें",
|
||||
"cw_home_confirm_rest_title": "कंसोल को रेस्ट मोड में डालें?",
|
||||
"cw_home_confirm_rest_body": "कंसोल के जागने तक यह पेज डिस्कनेक्ट रहेगा।",
|
||||
"cw_home_rest": "रेस्ट मोड",
|
||||
"cw_home_confirm_restart_title": "कंसोल फिर से शुरू करें?",
|
||||
"cw_home_confirm_restart_body": "चल रहे गेम और कार्य रुक जाएंगे। कंसोल के वापस आने तक यह पेज डिस्कनेक्ट रहेगा।",
|
||||
"cw_home_restart": "फिर से शुरू करें",
|
||||
"cw_home_loading": "लोड हो रहा है…",
|
||||
"cw_home_no_tasks": "अभी कोई कार्य नहीं है।",
|
||||
"cw_home_quick": "त्वरित कार्रवाइयाँ",
|
||||
"cw_home_browse_usb": "USB ब्राउज़ करें",
|
||||
"cw_home_now_running": "अभी चल रहा है",
|
||||
"cw_home_no_game": "कोई गेम नहीं चल रहा है।",
|
||||
"cw_home_tasks": "कार्य",
|
||||
"cw_home_storage": "स्टोरेज",
|
||||
};
|
||||
|
||||
export default hi;
|
||||
@@ -4361,6 +4361,58 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}",
|
||||
"cw_status_connecting": "Kapcsolódás…",
|
||||
"cw_status_offline": "Nincs kapcsolat",
|
||||
"cw_hints_label": "Kontroller gombjai",
|
||||
"cw_task_from_console": "Erről a konzolról",
|
||||
"cw_task_from_pc": "Egy PC-ről",
|
||||
"cw_dialog_cancel": "Mégse",
|
||||
"cw_home_kind_upload": "Feltöltés",
|
||||
"cw_home_kind_download": "Letöltés",
|
||||
"cw_home_kind_copy": "Másolás",
|
||||
"cw_home_kind_op": "Fájlművelet",
|
||||
"cw_home_kind_other": "Feladat",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total} fájl",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "Folyamatban",
|
||||
"cw_home_state_done": "Kész",
|
||||
"cw_home_state_failed": "Sikertelen",
|
||||
"cw_home_cancel_task": "Megszakítás",
|
||||
"cw_home_vol_internal": "Belső tárhely",
|
||||
"cw_home_vol_usb": "USB-meghajtó",
|
||||
"cw_home_vol_usb_ext": "USB bővített tárhely",
|
||||
"cw_home_vol_m2": "M.2 tárhely",
|
||||
"cw_home_vol_ext": "Bővített tárhely",
|
||||
"cw_home_vol_readonly": "Csak olvasható",
|
||||
"cw_home_free_of": "{free} szabad / {total}",
|
||||
"cw_home_err_offline": "A konzol nem válaszolt. Próbáld újra.",
|
||||
"cw_home_err_busy": "A konzol foglalt. Próbáld újra egy pillanat múlva.",
|
||||
"cw_home_err_gone": "Ez a feladat már befejeződött.",
|
||||
"cw_home_err_failed": "Ez nem sikerült ({code}).",
|
||||
"cw_home_err_load_tasks": "Nem sikerült beolvasni a feladatlistát.",
|
||||
"cw_home_err_load_storage": "Nem sikerült beolvasni a meghajtókat.",
|
||||
"cw_home_rest_sent": "A konzol nyugalmi módba lép.",
|
||||
"cw_home_restart_sent": "A konzol újraindul. Töltsd újra ezt az oldalt, ha újra elérhető.",
|
||||
"cw_home_confirm_cancel_title": "Megszakítod ezt a feladatot?",
|
||||
"cw_home_confirm_cancel_body": "Azonnal leáll. Ez nem vonható vissza.",
|
||||
"cw_home_confirm_cancel_body_pc": "Ezt a feladatot egy PC indította. A PC-n is leáll.",
|
||||
"cw_home_cancel_task_confirm": "Feladat megszakítása",
|
||||
"cw_home_keep_task": "Folytatás",
|
||||
"cw_home_confirm_close_title": "Bezárod: {name}?",
|
||||
"cw_home_confirm_close_body": "A játékban el nem mentett haladás elvész.",
|
||||
"cw_home_close_game": "Játék bezárása",
|
||||
"cw_home_confirm_rest_title": "Nyugalmi módba teszed a konzolt?",
|
||||
"cw_home_confirm_rest_body": "Ez az oldal a konzol felébredéséig nem kapcsolódik.",
|
||||
"cw_home_rest": "Nyugalmi mód",
|
||||
"cw_home_confirm_restart_title": "Újraindítod a konzolt?",
|
||||
"cw_home_confirm_restart_body": "A futó játékok és feladatok leállnak. Ez az oldal a konzol visszatéréséig nem kapcsolódik.",
|
||||
"cw_home_restart": "Újraindítás",
|
||||
"cw_home_loading": "Betöltés…",
|
||||
"cw_home_no_tasks": "Most nincs feladat.",
|
||||
"cw_home_quick": "Gyorsműveletek",
|
||||
"cw_home_browse_usb": "USB tallózása",
|
||||
"cw_home_now_running": "Most fut",
|
||||
"cw_home_no_game": "Nem fut játék.",
|
||||
"cw_home_tasks": "Feladatok",
|
||||
"cw_home_storage": "Tárhely",
|
||||
};
|
||||
|
||||
export default hu;
|
||||
@@ -4055,6 +4055,58 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}",
|
||||
"cw_status_connecting": "Menghubungkan…",
|
||||
"cw_status_offline": "Tidak terhubung",
|
||||
"cw_hints_label": "Tombol kontroler",
|
||||
"cw_task_from_console": "Dari konsol ini",
|
||||
"cw_task_from_pc": "Dari PC",
|
||||
"cw_dialog_cancel": "Batal",
|
||||
"cw_home_kind_upload": "Unggah",
|
||||
"cw_home_kind_download": "Unduh",
|
||||
"cw_home_kind_copy": "Salin",
|
||||
"cw_home_kind_op": "Operasi file",
|
||||
"cw_home_kind_other": "Tugas",
|
||||
"cw_home_progress_bytes": "{done} dari {total}",
|
||||
"cw_home_progress_files": "{done} dari {total} file",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "Berjalan",
|
||||
"cw_home_state_done": "Selesai",
|
||||
"cw_home_state_failed": "Gagal",
|
||||
"cw_home_cancel_task": "Batalkan",
|
||||
"cw_home_vol_internal": "Penyimpanan internal",
|
||||
"cw_home_vol_usb": "Drive USB",
|
||||
"cw_home_vol_usb_ext": "Penyimpanan tambahan USB",
|
||||
"cw_home_vol_m2": "Penyimpanan M.2",
|
||||
"cw_home_vol_ext": "Penyimpanan tambahan",
|
||||
"cw_home_vol_readonly": "Hanya baca",
|
||||
"cw_home_free_of": "{free} kosong dari {total}",
|
||||
"cw_home_err_offline": "Konsol tidak menjawab. Coba lagi.",
|
||||
"cw_home_err_busy": "Konsol sedang sibuk. Coba lagi sebentar lagi.",
|
||||
"cw_home_err_gone": "Tugas itu sudah berakhir.",
|
||||
"cw_home_err_failed": "Itu tidak berhasil ({code}).",
|
||||
"cw_home_err_load_tasks": "Tidak dapat membaca daftar tugas.",
|
||||
"cw_home_err_load_storage": "Tidak dapat membaca drive.",
|
||||
"cw_home_rest_sent": "Konsol masuk ke mode istirahat.",
|
||||
"cw_home_restart_sent": "Konsol sedang memulai ulang. Muat ulang halaman ini setelah konsol kembali.",
|
||||
"cw_home_confirm_cancel_title": "Batalkan tugas ini?",
|
||||
"cw_home_confirm_cancel_body": "Tugas berhenti sekarang. Ini tidak bisa dibatalkan.",
|
||||
"cw_home_confirm_cancel_body_pc": "Tugas ini dimulai dari PC. Tugas juga berhenti di PC.",
|
||||
"cw_home_cancel_task_confirm": "Batalkan tugas",
|
||||
"cw_home_keep_task": "Lanjutkan",
|
||||
"cw_home_confirm_close_title": "Tutup {name}?",
|
||||
"cw_home_confirm_close_body": "Progres game yang belum disimpan akan hilang.",
|
||||
"cw_home_close_game": "Tutup game",
|
||||
"cw_home_confirm_rest_title": "Masukkan konsol ke mode istirahat?",
|
||||
"cw_home_confirm_rest_body": "Halaman ini terputus sampai konsol bangun.",
|
||||
"cw_home_rest": "Mode istirahat",
|
||||
"cw_home_confirm_restart_title": "Mulai ulang konsol?",
|
||||
"cw_home_confirm_restart_body": "Game dan tugas yang berjalan akan berhenti. Halaman ini terputus sampai konsol kembali.",
|
||||
"cw_home_restart": "Mulai ulang",
|
||||
"cw_home_loading": "Memuat…",
|
||||
"cw_home_no_tasks": "Tidak ada tugas saat ini.",
|
||||
"cw_home_quick": "Tindakan cepat",
|
||||
"cw_home_browse_usb": "Jelajahi USB",
|
||||
"cw_home_now_running": "Sedang berjalan",
|
||||
"cw_home_no_game": "Tidak ada game yang berjalan.",
|
||||
"cw_home_tasks": "Tugas",
|
||||
"cw_home_storage": "Penyimpanan",
|
||||
};
|
||||
|
||||
export default id;
|
||||
@@ -3996,6 +3996,58 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}",
|
||||
"cw_status_connecting": "Connessione…",
|
||||
"cw_status_offline": "Non connesso",
|
||||
"cw_hints_label": "Tasti del controller",
|
||||
"cw_task_from_console": "Da questa console",
|
||||
"cw_task_from_pc": "Da un PC",
|
||||
"cw_dialog_cancel": "Annulla",
|
||||
"cw_home_kind_upload": "Caricamento",
|
||||
"cw_home_kind_download": "Download",
|
||||
"cw_home_kind_copy": "Copia",
|
||||
"cw_home_kind_op": "Operazione sui file",
|
||||
"cw_home_kind_other": "Attività",
|
||||
"cw_home_progress_bytes": "{done} di {total}",
|
||||
"cw_home_progress_files": "{done} di {total} file",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "In corso",
|
||||
"cw_home_state_done": "Completata",
|
||||
"cw_home_state_failed": "Non riuscita",
|
||||
"cw_home_cancel_task": "Annulla",
|
||||
"cw_home_vol_internal": "Memoria interna",
|
||||
"cw_home_vol_usb": "Unità USB",
|
||||
"cw_home_vol_usb_ext": "Memoria estesa USB",
|
||||
"cw_home_vol_m2": "Memoria M.2",
|
||||
"cw_home_vol_ext": "Memoria estesa",
|
||||
"cw_home_vol_readonly": "Sola lettura",
|
||||
"cw_home_free_of": "{free} liberi su {total}",
|
||||
"cw_home_err_offline": "La console non ha risposto. Riprova.",
|
||||
"cw_home_err_busy": "La console è occupata. Riprova tra un momento.",
|
||||
"cw_home_err_gone": "Quell'attività è già terminata.",
|
||||
"cw_home_err_failed": "Non ha funzionato ({code}).",
|
||||
"cw_home_err_load_tasks": "Impossibile leggere l'elenco delle attività.",
|
||||
"cw_home_err_load_storage": "Impossibile leggere le unità.",
|
||||
"cw_home_rest_sent": "La console sta entrando in modalità riposo.",
|
||||
"cw_home_restart_sent": "La console si sta riavviando. Ricarica questa pagina quando torna disponibile.",
|
||||
"cw_home_confirm_cancel_title": "Annullare questa attività?",
|
||||
"cw_home_confirm_cancel_body": "Si interrompe subito. L'operazione non può essere annullata.",
|
||||
"cw_home_confirm_cancel_body_pc": "Questa attività è stata avviata da un PC. Si interrompe anche sul PC.",
|
||||
"cw_home_cancel_task_confirm": "Annulla attività",
|
||||
"cw_home_keep_task": "Continua",
|
||||
"cw_home_confirm_close_title": "Chiudere {name}?",
|
||||
"cw_home_confirm_close_body": "I progressi di gioco non salvati andranno persi.",
|
||||
"cw_home_close_game": "Chiudi gioco",
|
||||
"cw_home_confirm_rest_title": "Mettere la console in modalità riposo?",
|
||||
"cw_home_confirm_rest_body": "Questa pagina resta disconnessa finché la console non si riattiva.",
|
||||
"cw_home_rest": "Modalità riposo",
|
||||
"cw_home_confirm_restart_title": "Riavviare la console?",
|
||||
"cw_home_confirm_restart_body": "Giochi e attività in corso si interrompono. Questa pagina resta disconnessa finché la console non torna disponibile.",
|
||||
"cw_home_restart": "Riavvia",
|
||||
"cw_home_loading": "Caricamento…",
|
||||
"cw_home_no_tasks": "Nessuna attività al momento.",
|
||||
"cw_home_quick": "Azioni rapide",
|
||||
"cw_home_browse_usb": "Sfoglia USB",
|
||||
"cw_home_now_running": "In esecuzione",
|
||||
"cw_home_no_game": "Nessun gioco in esecuzione.",
|
||||
"cw_home_tasks": "Attività",
|
||||
"cw_home_storage": "Archiviazione",
|
||||
};
|
||||
|
||||
export default it;
|
||||
@@ -4067,6 +4067,58 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
|
||||
"cw_status_connecting": "接続中…",
|
||||
"cw_status_offline": "未接続",
|
||||
"cw_hints_label": "コントローラーのボタン",
|
||||
"cw_task_from_console": "このコンソールから",
|
||||
"cw_task_from_pc": "PCから",
|
||||
"cw_dialog_cancel": "キャンセル",
|
||||
"cw_home_kind_upload": "アップロード",
|
||||
"cw_home_kind_download": "ダウンロード",
|
||||
"cw_home_kind_copy": "コピー",
|
||||
"cw_home_kind_op": "ファイル操作",
|
||||
"cw_home_kind_other": "タスク",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total} ファイル",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "実行中",
|
||||
"cw_home_state_done": "完了",
|
||||
"cw_home_state_failed": "失敗",
|
||||
"cw_home_cancel_task": "キャンセル",
|
||||
"cw_home_vol_internal": "本体ストレージ",
|
||||
"cw_home_vol_usb": "USBドライブ",
|
||||
"cw_home_vol_usb_ext": "USB拡張ストレージ",
|
||||
"cw_home_vol_m2": "M.2ストレージ",
|
||||
"cw_home_vol_ext": "拡張ストレージ",
|
||||
"cw_home_vol_readonly": "読み取り専用",
|
||||
"cw_home_free_of": "空き {free} / {total}",
|
||||
"cw_home_err_offline": "コンソールが応答しませんでした。もう一度お試しください。",
|
||||
"cw_home_err_busy": "コンソールが処理中です。少ししてからもう一度お試しください。",
|
||||
"cw_home_err_gone": "そのタスクはすでに終了しています。",
|
||||
"cw_home_err_failed": "うまくいきませんでした({code})。",
|
||||
"cw_home_err_load_tasks": "タスク一覧を読み込めませんでした。",
|
||||
"cw_home_err_load_storage": "ドライブを読み込めませんでした。",
|
||||
"cw_home_rest_sent": "コンソールがレストモードに入ります。",
|
||||
"cw_home_restart_sent": "コンソールを再起動しています。復帰したらこのページを再読み込みしてください。",
|
||||
"cw_home_confirm_cancel_title": "このタスクをキャンセルしますか?",
|
||||
"cw_home_confirm_cancel_body": "すぐに停止します。元に戻せません。",
|
||||
"cw_home_confirm_cancel_body_pc": "このタスクはPCから開始されました。PC側でも停止します。",
|
||||
"cw_home_cancel_task_confirm": "タスクをキャンセル",
|
||||
"cw_home_keep_task": "続ける",
|
||||
"cw_home_confirm_close_title": "{name} を終了しますか?",
|
||||
"cw_home_confirm_close_body": "ゲーム内の保存していない進行状況は失われます。",
|
||||
"cw_home_close_game": "ゲームを終了",
|
||||
"cw_home_confirm_rest_title": "コンソールをレストモードにしますか?",
|
||||
"cw_home_confirm_rest_body": "コンソールが復帰するまで、このページは切断されます。",
|
||||
"cw_home_rest": "レストモード",
|
||||
"cw_home_confirm_restart_title": "コンソールを再起動しますか?",
|
||||
"cw_home_confirm_restart_body": "実行中のゲームとタスクは停止します。コンソールが戻るまで、このページは切断されます。",
|
||||
"cw_home_restart": "再起動",
|
||||
"cw_home_loading": "読み込み中…",
|
||||
"cw_home_no_tasks": "現在タスクはありません。",
|
||||
"cw_home_quick": "クイックアクション",
|
||||
"cw_home_browse_usb": "USBを参照",
|
||||
"cw_home_now_running": "実行中",
|
||||
"cw_home_no_game": "実行中のゲームはありません。",
|
||||
"cw_home_tasks": "タスク",
|
||||
"cw_home_storage": "ストレージ",
|
||||
};
|
||||
|
||||
export default ja;
|
||||
@@ -4065,6 +4065,58 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
|
||||
"cw_status_connecting": "연결 중…",
|
||||
"cw_status_offline": "연결되지 않음",
|
||||
"cw_hints_label": "컨트롤러 버튼",
|
||||
"cw_task_from_console": "이 콘솔에서",
|
||||
"cw_task_from_pc": "PC에서",
|
||||
"cw_dialog_cancel": "취소",
|
||||
"cw_home_kind_upload": "업로드",
|
||||
"cw_home_kind_download": "다운로드",
|
||||
"cw_home_kind_copy": "복사",
|
||||
"cw_home_kind_op": "파일 작업",
|
||||
"cw_home_kind_other": "작업",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total}개 파일",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "진행 중",
|
||||
"cw_home_state_done": "완료",
|
||||
"cw_home_state_failed": "실패",
|
||||
"cw_home_cancel_task": "취소",
|
||||
"cw_home_vol_internal": "내부 저장소",
|
||||
"cw_home_vol_usb": "USB 드라이브",
|
||||
"cw_home_vol_usb_ext": "USB 확장 저장소",
|
||||
"cw_home_vol_m2": "M.2 저장소",
|
||||
"cw_home_vol_ext": "확장 저장소",
|
||||
"cw_home_vol_readonly": "읽기 전용",
|
||||
"cw_home_free_of": "{total} 중 {free} 남음",
|
||||
"cw_home_err_offline": "콘솔이 응답하지 않았습니다. 다시 시도하세요.",
|
||||
"cw_home_err_busy": "콘솔이 사용 중입니다. 잠시 후 다시 시도하세요.",
|
||||
"cw_home_err_gone": "이 작업은 이미 끝났습니다.",
|
||||
"cw_home_err_failed": "실패했습니다({code}).",
|
||||
"cw_home_err_load_tasks": "작업 목록을 읽을 수 없습니다.",
|
||||
"cw_home_err_load_storage": "드라이브를 읽을 수 없습니다.",
|
||||
"cw_home_rest_sent": "콘솔이 휴식 모드로 전환됩니다.",
|
||||
"cw_home_restart_sent": "콘솔을 다시 시작하는 중입니다. 다시 켜지면 이 페이지를 새로 고치세요.",
|
||||
"cw_home_confirm_cancel_title": "이 작업을 취소할까요?",
|
||||
"cw_home_confirm_cancel_body": "지금 바로 멈춥니다. 되돌릴 수 없습니다.",
|
||||
"cw_home_confirm_cancel_body_pc": "PC에서 시작한 작업입니다. PC에서도 멈춥니다.",
|
||||
"cw_home_cancel_task_confirm": "작업 취소",
|
||||
"cw_home_keep_task": "계속 진행",
|
||||
"cw_home_confirm_close_title": "{name}을(를) 닫을까요?",
|
||||
"cw_home_confirm_close_body": "게임에서 저장하지 않은 진행 상황은 사라집니다.",
|
||||
"cw_home_close_game": "게임 닫기",
|
||||
"cw_home_confirm_rest_title": "콘솔을 휴식 모드로 전환할까요?",
|
||||
"cw_home_confirm_rest_body": "콘솔이 깨어날 때까지 이 페이지는 연결이 끊깁니다.",
|
||||
"cw_home_rest": "휴식 모드",
|
||||
"cw_home_confirm_restart_title": "콘솔을 다시 시작할까요?",
|
||||
"cw_home_confirm_restart_body": "실행 중인 게임과 작업이 멈춥니다. 콘솔이 다시 켜질 때까지 이 페이지는 연결이 끊깁니다.",
|
||||
"cw_home_restart": "다시 시작",
|
||||
"cw_home_loading": "불러오는 중…",
|
||||
"cw_home_no_tasks": "현재 작업이 없습니다.",
|
||||
"cw_home_quick": "빠른 작업",
|
||||
"cw_home_browse_usb": "USB 탐색",
|
||||
"cw_home_now_running": "실행 중",
|
||||
"cw_home_no_game": "실행 중인 게임이 없습니다.",
|
||||
"cw_home_tasks": "작업",
|
||||
"cw_home_storage": "저장소",
|
||||
};
|
||||
|
||||
export default ko;
|
||||
@@ -4301,6 +4301,58 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}",
|
||||
"cw_status_connecting": "Łączenie…",
|
||||
"cw_status_offline": "Brak połączenia",
|
||||
"cw_hints_label": "Przyciski kontrolera",
|
||||
"cw_task_from_console": "Z tej konsoli",
|
||||
"cw_task_from_pc": "Z komputera",
|
||||
"cw_dialog_cancel": "Anuluj",
|
||||
"cw_home_kind_upload": "Wysyłanie",
|
||||
"cw_home_kind_download": "Pobieranie",
|
||||
"cw_home_kind_copy": "Kopiowanie",
|
||||
"cw_home_kind_op": "Operacja na plikach",
|
||||
"cw_home_kind_other": "Zadanie",
|
||||
"cw_home_progress_bytes": "{done} z {total}",
|
||||
"cw_home_progress_files": "{done} z {total} plików",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "W toku",
|
||||
"cw_home_state_done": "Gotowe",
|
||||
"cw_home_state_failed": "Niepowodzenie",
|
||||
"cw_home_cancel_task": "Anuluj",
|
||||
"cw_home_vol_internal": "Pamięć wewnętrzna",
|
||||
"cw_home_vol_usb": "Dysk USB",
|
||||
"cw_home_vol_usb_ext": "Rozszerzona pamięć USB",
|
||||
"cw_home_vol_m2": "Pamięć M.2",
|
||||
"cw_home_vol_ext": "Rozszerzona pamięć",
|
||||
"cw_home_vol_readonly": "Tylko do odczytu",
|
||||
"cw_home_free_of": "{free} wolne z {total}",
|
||||
"cw_home_err_offline": "Konsola nie odpowiedziała. Spróbuj ponownie.",
|
||||
"cw_home_err_busy": "Konsola jest zajęta. Spróbuj ponownie za chwilę.",
|
||||
"cw_home_err_gone": "To zadanie już się zakończyło.",
|
||||
"cw_home_err_failed": "Nie udało się ({code}).",
|
||||
"cw_home_err_load_tasks": "Nie udało się odczytać listy zadań.",
|
||||
"cw_home_err_load_storage": "Nie udało się odczytać dysków.",
|
||||
"cw_home_rest_sent": "Konsola przechodzi w tryb odpoczynku.",
|
||||
"cw_home_restart_sent": "Konsola uruchamia się ponownie. Przeładuj tę stronę, gdy wróci.",
|
||||
"cw_home_confirm_cancel_title": "Anulować to zadanie?",
|
||||
"cw_home_confirm_cancel_body": "Zatrzyma się teraz. Tego nie można cofnąć.",
|
||||
"cw_home_confirm_cancel_body_pc": "To zadanie uruchomił komputer. Zatrzyma się także na komputerze.",
|
||||
"cw_home_cancel_task_confirm": "Anuluj zadanie",
|
||||
"cw_home_keep_task": "Kontynuuj",
|
||||
"cw_home_confirm_close_title": "Zamknąć {name}?",
|
||||
"cw_home_confirm_close_body": "Niezapisane postępy w grze zostaną utracone.",
|
||||
"cw_home_close_game": "Zamknij grę",
|
||||
"cw_home_confirm_rest_title": "Przełączyć konsolę w tryb odpoczynku?",
|
||||
"cw_home_confirm_rest_body": "Ta strona będzie rozłączona, dopóki konsola się nie wybudzi.",
|
||||
"cw_home_rest": "Tryb odpoczynku",
|
||||
"cw_home_confirm_restart_title": "Uruchomić konsolę ponownie?",
|
||||
"cw_home_confirm_restart_body": "Uruchomione gry i zadania zostaną zatrzymane. Ta strona będzie rozłączona, dopóki konsola nie wróci.",
|
||||
"cw_home_restart": "Uruchom ponownie",
|
||||
"cw_home_loading": "Wczytywanie…",
|
||||
"cw_home_no_tasks": "Brak zadań.",
|
||||
"cw_home_quick": "Szybkie akcje",
|
||||
"cw_home_browse_usb": "Przeglądaj USB",
|
||||
"cw_home_now_running": "Teraz uruchomione",
|
||||
"cw_home_no_game": "Żadna gra nie jest uruchomiona.",
|
||||
"cw_home_tasks": "Zadania",
|
||||
"cw_home_storage": "Pamięć",
|
||||
};
|
||||
|
||||
export default pl;
|
||||
@@ -4063,6 +4063,58 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}",
|
||||
"cw_status_connecting": "Conectando…",
|
||||
"cw_status_offline": "Não conectado",
|
||||
"cw_hints_label": "Botões do controle",
|
||||
"cw_task_from_console": "Deste console",
|
||||
"cw_task_from_pc": "De um PC",
|
||||
"cw_dialog_cancel": "Cancelar",
|
||||
"cw_home_kind_upload": "Envio",
|
||||
"cw_home_kind_download": "Download",
|
||||
"cw_home_kind_copy": "Cópia",
|
||||
"cw_home_kind_op": "Operação de arquivos",
|
||||
"cw_home_kind_other": "Tarefa",
|
||||
"cw_home_progress_bytes": "{done} de {total}",
|
||||
"cw_home_progress_files": "{done} de {total} arquivos",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "Em andamento",
|
||||
"cw_home_state_done": "Concluída",
|
||||
"cw_home_state_failed": "Falhou",
|
||||
"cw_home_cancel_task": "Cancelar",
|
||||
"cw_home_vol_internal": "Armazenamento interno",
|
||||
"cw_home_vol_usb": "Unidade USB",
|
||||
"cw_home_vol_usb_ext": "Armazenamento estendido USB",
|
||||
"cw_home_vol_m2": "Armazenamento M.2",
|
||||
"cw_home_vol_ext": "Armazenamento estendido",
|
||||
"cw_home_vol_readonly": "Somente leitura",
|
||||
"cw_home_free_of": "{free} livres de {total}",
|
||||
"cw_home_err_offline": "O console não respondeu. Tente novamente.",
|
||||
"cw_home_err_busy": "O console está ocupado. Tente novamente em instantes.",
|
||||
"cw_home_err_gone": "Essa tarefa já terminou.",
|
||||
"cw_home_err_failed": "Não deu certo ({code}).",
|
||||
"cw_home_err_load_tasks": "Não foi possível ler a lista de tarefas.",
|
||||
"cw_home_err_load_storage": "Não foi possível ler as unidades.",
|
||||
"cw_home_rest_sent": "O console está entrando no modo de descanso.",
|
||||
"cw_home_restart_sent": "O console está reiniciando. Recarregue esta página quando ele voltar.",
|
||||
"cw_home_confirm_cancel_title": "Cancelar esta tarefa?",
|
||||
"cw_home_confirm_cancel_body": "Ela para agora. Isso não pode ser desfeito.",
|
||||
"cw_home_confirm_cancel_body_pc": "Um PC iniciou esta tarefa. Ela também para no PC.",
|
||||
"cw_home_cancel_task_confirm": "Cancelar tarefa",
|
||||
"cw_home_keep_task": "Continuar",
|
||||
"cw_home_confirm_close_title": "Fechar {name}?",
|
||||
"cw_home_confirm_close_body": "O progresso não salvo no jogo será perdido.",
|
||||
"cw_home_close_game": "Fechar jogo",
|
||||
"cw_home_confirm_rest_title": "Colocar o console no modo de descanso?",
|
||||
"cw_home_confirm_rest_body": "Esta página fica desconectada até o console despertar.",
|
||||
"cw_home_rest": "Modo de descanso",
|
||||
"cw_home_confirm_restart_title": "Reiniciar o console?",
|
||||
"cw_home_confirm_restart_body": "Jogos e tarefas em andamento param. Esta página fica desconectada até o console voltar.",
|
||||
"cw_home_restart": "Reiniciar",
|
||||
"cw_home_loading": "Carregando…",
|
||||
"cw_home_no_tasks": "Nenhuma tarefa no momento.",
|
||||
"cw_home_quick": "Ações rápidas",
|
||||
"cw_home_browse_usb": "Navegar no USB",
|
||||
"cw_home_now_running": "Em execução",
|
||||
"cw_home_no_game": "Nenhum jogo em execução.",
|
||||
"cw_home_tasks": "Tarefas",
|
||||
"cw_home_storage": "Armazenamento",
|
||||
};
|
||||
|
||||
export default pt_BR;
|
||||
@@ -3996,6 +3996,58 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
|
||||
"cw_status_connecting": "Подключение…",
|
||||
"cw_status_offline": "Нет подключения",
|
||||
"cw_hints_label": "Кнопки геймпада",
|
||||
"cw_task_from_console": "С этой консоли",
|
||||
"cw_task_from_pc": "С ПК",
|
||||
"cw_dialog_cancel": "Отмена",
|
||||
"cw_home_kind_upload": "Загрузка на консоль",
|
||||
"cw_home_kind_download": "Скачивание",
|
||||
"cw_home_kind_copy": "Копирование",
|
||||
"cw_home_kind_op": "Операция с файлами",
|
||||
"cw_home_kind_other": "Задача",
|
||||
"cw_home_progress_bytes": "{done} из {total}",
|
||||
"cw_home_progress_files": "{done} из {total} файлов",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "Выполняется",
|
||||
"cw_home_state_done": "Готово",
|
||||
"cw_home_state_failed": "Ошибка",
|
||||
"cw_home_cancel_task": "Отменить",
|
||||
"cw_home_vol_internal": "Внутреннее хранилище",
|
||||
"cw_home_vol_usb": "USB-накопитель",
|
||||
"cw_home_vol_usb_ext": "Расширенное хранилище USB",
|
||||
"cw_home_vol_m2": "Хранилище M.2",
|
||||
"cw_home_vol_ext": "Расширенное хранилище",
|
||||
"cw_home_vol_readonly": "Только чтение",
|
||||
"cw_home_free_of": "Свободно {free} из {total}",
|
||||
"cw_home_err_offline": "Консоль не ответила. Попробуйте ещё раз.",
|
||||
"cw_home_err_busy": "Консоль занята. Попробуйте чуть позже.",
|
||||
"cw_home_err_gone": "Эта задача уже завершилась.",
|
||||
"cw_home_err_failed": "Не получилось ({code}).",
|
||||
"cw_home_err_load_tasks": "Не удалось прочитать список задач.",
|
||||
"cw_home_err_load_storage": "Не удалось прочитать накопители.",
|
||||
"cw_home_rest_sent": "Консоль переходит в режим покоя.",
|
||||
"cw_home_restart_sent": "Консоль перезагружается. Обновите эту страницу, когда она снова будет доступна.",
|
||||
"cw_home_confirm_cancel_title": "Отменить эту задачу?",
|
||||
"cw_home_confirm_cancel_body": "Она остановится сейчас. Это нельзя отменить.",
|
||||
"cw_home_confirm_cancel_body_pc": "Эту задачу запустил ПК. На ПК она тоже остановится.",
|
||||
"cw_home_cancel_task_confirm": "Отменить задачу",
|
||||
"cw_home_keep_task": "Продолжить",
|
||||
"cw_home_confirm_close_title": "Закрыть {name}?",
|
||||
"cw_home_confirm_close_body": "Несохранённый прогресс в игре будет потерян.",
|
||||
"cw_home_close_game": "Закрыть игру",
|
||||
"cw_home_confirm_rest_title": "Перевести консоль в режим покоя?",
|
||||
"cw_home_confirm_rest_body": "Страница будет отключена, пока консоль не проснётся.",
|
||||
"cw_home_rest": "Режим покоя",
|
||||
"cw_home_confirm_restart_title": "Перезагрузить консоль?",
|
||||
"cw_home_confirm_restart_body": "Запущенные игры и задачи остановятся. Страница будет отключена, пока консоль не вернётся.",
|
||||
"cw_home_restart": "Перезагрузить",
|
||||
"cw_home_loading": "Загрузка…",
|
||||
"cw_home_no_tasks": "Сейчас задач нет.",
|
||||
"cw_home_quick": "Быстрые действия",
|
||||
"cw_home_browse_usb": "Открыть USB",
|
||||
"cw_home_now_running": "Сейчас запущено",
|
||||
"cw_home_no_game": "Игра не запущена.",
|
||||
"cw_home_tasks": "Задачи",
|
||||
"cw_home_storage": "Хранилище",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@@ -4055,6 +4055,58 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
|
||||
"cw_status_connecting": "กำลังเชื่อมต่อ…",
|
||||
"cw_status_offline": "ไม่ได้เชื่อมต่อ",
|
||||
"cw_hints_label": "ปุ่มคอนโทรลเลอร์",
|
||||
"cw_task_from_console": "จากคอนโซลนี้",
|
||||
"cw_task_from_pc": "จาก PC",
|
||||
"cw_dialog_cancel": "ยกเลิก",
|
||||
"cw_home_kind_upload": "อัปโหลด",
|
||||
"cw_home_kind_download": "ดาวน์โหลด",
|
||||
"cw_home_kind_copy": "คัดลอก",
|
||||
"cw_home_kind_op": "การจัดการไฟล์",
|
||||
"cw_home_kind_other": "งาน",
|
||||
"cw_home_progress_bytes": "{done} จาก {total}",
|
||||
"cw_home_progress_files": "{done} จาก {total} ไฟล์",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "กำลังทำงาน",
|
||||
"cw_home_state_done": "เสร็จแล้ว",
|
||||
"cw_home_state_failed": "ล้มเหลว",
|
||||
"cw_home_cancel_task": "ยกเลิก",
|
||||
"cw_home_vol_internal": "ที่เก็บข้อมูลภายใน",
|
||||
"cw_home_vol_usb": "ไดรฟ์ USB",
|
||||
"cw_home_vol_usb_ext": "ที่เก็บข้อมูลเสริม USB",
|
||||
"cw_home_vol_m2": "ที่เก็บข้อมูล M.2",
|
||||
"cw_home_vol_ext": "ที่เก็บข้อมูลเสริม",
|
||||
"cw_home_vol_readonly": "อ่านอย่างเดียว",
|
||||
"cw_home_free_of": "ว่าง {free} จาก {total}",
|
||||
"cw_home_err_offline": "คอนโซลไม่ตอบสนอง ลองอีกครั้ง",
|
||||
"cw_home_err_busy": "คอนโซลไม่ว่าง ลองอีกครั้งในอีกสักครู่",
|
||||
"cw_home_err_gone": "งานนั้นสิ้นสุดไปแล้ว",
|
||||
"cw_home_err_failed": "ไม่สำเร็จ ({code})",
|
||||
"cw_home_err_load_tasks": "อ่านรายการงานไม่ได้",
|
||||
"cw_home_err_load_storage": "อ่านไดรฟ์ไม่ได้",
|
||||
"cw_home_rest_sent": "คอนโซลกำลังเข้าสู่โหมดพัก",
|
||||
"cw_home_restart_sent": "คอนโซลกำลังรีสตาร์ท โหลดหน้านี้ใหม่เมื่อกลับมาแล้ว",
|
||||
"cw_home_confirm_cancel_title": "ยกเลิกงานนี้ไหม",
|
||||
"cw_home_confirm_cancel_body": "งานจะหยุดทันทีและย้อนกลับไม่ได้",
|
||||
"cw_home_confirm_cancel_body_pc": "งานนี้เริ่มจาก PC และจะหยุดบน PC ด้วย",
|
||||
"cw_home_cancel_task_confirm": "ยกเลิกงาน",
|
||||
"cw_home_keep_task": "ทำงานต่อ",
|
||||
"cw_home_confirm_close_title": "ปิด {name} ไหม",
|
||||
"cw_home_confirm_close_body": "ความคืบหน้าในเกมที่ยังไม่ได้บันทึกจะหายไป",
|
||||
"cw_home_close_game": "ปิดเกม",
|
||||
"cw_home_confirm_rest_title": "ให้คอนโซลเข้าสู่โหมดพักไหม",
|
||||
"cw_home_confirm_rest_body": "หน้านี้จะขาดการเชื่อมต่อจนกว่าคอนโซลจะตื่น",
|
||||
"cw_home_rest": "โหมดพัก",
|
||||
"cw_home_confirm_restart_title": "รีสตาร์ทคอนโซลไหม",
|
||||
"cw_home_confirm_restart_body": "เกมและงานที่กำลังทำงานจะหยุด หน้านี้จะขาดการเชื่อมต่อจนกว่าคอนโซลจะกลับมา",
|
||||
"cw_home_restart": "รีสตาร์ท",
|
||||
"cw_home_loading": "กำลังโหลด…",
|
||||
"cw_home_no_tasks": "ตอนนี้ไม่มีงาน",
|
||||
"cw_home_quick": "การทำงานด่วน",
|
||||
"cw_home_browse_usb": "เรียกดู USB",
|
||||
"cw_home_now_running": "กำลังทำงานอยู่",
|
||||
"cw_home_no_game": "ไม่มีเกมที่กำลังทำงาน",
|
||||
"cw_home_tasks": "งาน",
|
||||
"cw_home_storage": "ที่เก็บข้อมูล",
|
||||
};
|
||||
|
||||
export default th;
|
||||
@@ -4053,6 +4053,58 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}",
|
||||
"cw_status_connecting": "Bağlanıyor…",
|
||||
"cw_status_offline": "Bağlı değil",
|
||||
"cw_hints_label": "Kumanda düğmeleri",
|
||||
"cw_task_from_console": "Bu konsoldan",
|
||||
"cw_task_from_pc": "Bir PC'den",
|
||||
"cw_dialog_cancel": "İptal",
|
||||
"cw_home_kind_upload": "Yükleme",
|
||||
"cw_home_kind_download": "İndirme",
|
||||
"cw_home_kind_copy": "Kopyalama",
|
||||
"cw_home_kind_op": "Dosya işlemi",
|
||||
"cw_home_kind_other": "Görev",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total} dosya",
|
||||
"cw_home_percent": "%{n}",
|
||||
"cw_home_state_running": "Çalışıyor",
|
||||
"cw_home_state_done": "Bitti",
|
||||
"cw_home_state_failed": "Başarısız",
|
||||
"cw_home_cancel_task": "İptal et",
|
||||
"cw_home_vol_internal": "Dahili depolama",
|
||||
"cw_home_vol_usb": "USB sürücü",
|
||||
"cw_home_vol_usb_ext": "USB genişletilmiş depolama",
|
||||
"cw_home_vol_m2": "M.2 depolama",
|
||||
"cw_home_vol_ext": "Genişletilmiş depolama",
|
||||
"cw_home_vol_readonly": "Salt okunur",
|
||||
"cw_home_free_of": "{total} alanın {free} kadarı boş",
|
||||
"cw_home_err_offline": "Konsol yanıt vermedi. Tekrar dene.",
|
||||
"cw_home_err_busy": "Konsol meşgul. Birazdan tekrar dene.",
|
||||
"cw_home_err_gone": "Bu görev zaten bitti.",
|
||||
"cw_home_err_failed": "Bu işe yaramadı ({code}).",
|
||||
"cw_home_err_load_tasks": "Görev listesi okunamadı.",
|
||||
"cw_home_err_load_storage": "Sürücüler okunamadı.",
|
||||
"cw_home_rest_sent": "Konsol dinlenme moduna geçiyor.",
|
||||
"cw_home_restart_sent": "Konsol yeniden başlıyor. Geri geldiğinde bu sayfayı yenile.",
|
||||
"cw_home_confirm_cancel_title": "Bu görev iptal edilsin mi?",
|
||||
"cw_home_confirm_cancel_body": "Hemen durur. Bu geri alınamaz.",
|
||||
"cw_home_confirm_cancel_body_pc": "Bu görevi bir PC başlattı. PC'de de durur.",
|
||||
"cw_home_cancel_task_confirm": "Görevi iptal et",
|
||||
"cw_home_keep_task": "Devam etsin",
|
||||
"cw_home_confirm_close_title": "{name} kapatılsın mı?",
|
||||
"cw_home_confirm_close_body": "Oyunda kaydedilmemiş ilerleme kaybolur.",
|
||||
"cw_home_close_game": "Oyunu kapat",
|
||||
"cw_home_confirm_rest_title": "Konsol dinlenme moduna alınsın mı?",
|
||||
"cw_home_confirm_rest_body": "Bu sayfanın bağlantısı konsol uyanana kadar kesilir.",
|
||||
"cw_home_rest": "Dinlenme modu",
|
||||
"cw_home_confirm_restart_title": "Konsol yeniden başlatılsın mı?",
|
||||
"cw_home_confirm_restart_body": "Çalışan oyunlar ve görevler durur. Bu sayfanın bağlantısı konsol geri gelene kadar kesilir.",
|
||||
"cw_home_restart": "Yeniden başlat",
|
||||
"cw_home_loading": "Yükleniyor…",
|
||||
"cw_home_no_tasks": "Şu anda görev yok.",
|
||||
"cw_home_quick": "Hızlı işlemler",
|
||||
"cw_home_browse_usb": "USB'ye göz at",
|
||||
"cw_home_now_running": "Şu anda çalışıyor",
|
||||
"cw_home_no_game": "Çalışan oyun yok.",
|
||||
"cw_home_tasks": "Görevler",
|
||||
"cw_home_storage": "Depolama",
|
||||
};
|
||||
|
||||
export default tr;
|
||||
@@ -4048,6 +4048,58 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}",
|
||||
"cw_status_connecting": "Đang kết nối…",
|
||||
"cw_status_offline": "Chưa kết nối",
|
||||
"cw_hints_label": "Nút tay cầm",
|
||||
"cw_task_from_console": "Từ máy này",
|
||||
"cw_task_from_pc": "Từ PC",
|
||||
"cw_dialog_cancel": "Hủy",
|
||||
"cw_home_kind_upload": "Tải lên",
|
||||
"cw_home_kind_download": "Tải xuống",
|
||||
"cw_home_kind_copy": "Sao chép",
|
||||
"cw_home_kind_op": "Thao tác tệp",
|
||||
"cw_home_kind_other": "Tác vụ",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total} tệp",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "Đang chạy",
|
||||
"cw_home_state_done": "Xong",
|
||||
"cw_home_state_failed": "Thất bại",
|
||||
"cw_home_cancel_task": "Hủy",
|
||||
"cw_home_vol_internal": "Bộ nhớ trong",
|
||||
"cw_home_vol_usb": "Ổ USB",
|
||||
"cw_home_vol_usb_ext": "Bộ nhớ mở rộng USB",
|
||||
"cw_home_vol_m2": "Bộ nhớ M.2",
|
||||
"cw_home_vol_ext": "Bộ nhớ mở rộng",
|
||||
"cw_home_vol_readonly": "Chỉ đọc",
|
||||
"cw_home_free_of": "Còn trống {free} / {total}",
|
||||
"cw_home_err_offline": "Máy không phản hồi. Hãy thử lại.",
|
||||
"cw_home_err_busy": "Máy đang bận. Hãy thử lại sau giây lát.",
|
||||
"cw_home_err_gone": "Tác vụ đó đã kết thúc.",
|
||||
"cw_home_err_failed": "Không thành công ({code}).",
|
||||
"cw_home_err_load_tasks": "Không đọc được danh sách tác vụ.",
|
||||
"cw_home_err_load_storage": "Không đọc được các ổ đĩa.",
|
||||
"cw_home_rest_sent": "Máy đang chuyển sang chế độ nghỉ.",
|
||||
"cw_home_restart_sent": "Máy đang khởi động lại. Hãy tải lại trang này khi máy hoạt động trở lại.",
|
||||
"cw_home_confirm_cancel_title": "Hủy tác vụ này?",
|
||||
"cw_home_confirm_cancel_body": "Tác vụ dừng ngay. Không thể hoàn tác.",
|
||||
"cw_home_confirm_cancel_body_pc": "Một PC đã bắt đầu tác vụ này. Tác vụ cũng dừng trên PC.",
|
||||
"cw_home_cancel_task_confirm": "Hủy tác vụ",
|
||||
"cw_home_keep_task": "Tiếp tục",
|
||||
"cw_home_confirm_close_title": "Đóng {name}?",
|
||||
"cw_home_confirm_close_body": "Tiến trình chưa lưu trong game sẽ mất.",
|
||||
"cw_home_close_game": "Đóng game",
|
||||
"cw_home_confirm_rest_title": "Đưa máy vào chế độ nghỉ?",
|
||||
"cw_home_confirm_rest_body": "Trang này mất kết nối cho đến khi máy thức dậy.",
|
||||
"cw_home_rest": "Chế độ nghỉ",
|
||||
"cw_home_confirm_restart_title": "Khởi động lại máy?",
|
||||
"cw_home_confirm_restart_body": "Các game và tác vụ đang chạy sẽ dừng. Trang này mất kết nối cho đến khi máy hoạt động trở lại.",
|
||||
"cw_home_restart": "Khởi động lại",
|
||||
"cw_home_loading": "Đang tải…",
|
||||
"cw_home_no_tasks": "Hiện không có tác vụ nào.",
|
||||
"cw_home_quick": "Thao tác nhanh",
|
||||
"cw_home_browse_usb": "Duyệt USB",
|
||||
"cw_home_now_running": "Đang chạy",
|
||||
"cw_home_no_game": "Không có game nào đang chạy.",
|
||||
"cw_home_tasks": "Tác vụ",
|
||||
"cw_home_storage": "Bộ nhớ",
|
||||
};
|
||||
|
||||
export default vi;
|
||||
@@ -4002,6 +4002,58 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
|
||||
"cw_status_connecting": "正在连接…",
|
||||
"cw_status_offline": "未连接",
|
||||
"cw_hints_label": "手柄按键",
|
||||
"cw_task_from_console": "来自本主机",
|
||||
"cw_task_from_pc": "来自电脑",
|
||||
"cw_dialog_cancel": "取消",
|
||||
"cw_home_kind_upload": "上传",
|
||||
"cw_home_kind_download": "下载",
|
||||
"cw_home_kind_copy": "复制",
|
||||
"cw_home_kind_op": "文件操作",
|
||||
"cw_home_kind_other": "任务",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total} 个文件",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "进行中",
|
||||
"cw_home_state_done": "已完成",
|
||||
"cw_home_state_failed": "失败",
|
||||
"cw_home_cancel_task": "取消",
|
||||
"cw_home_vol_internal": "内置存储",
|
||||
"cw_home_vol_usb": "USB 驱动器",
|
||||
"cw_home_vol_usb_ext": "USB 扩展存储",
|
||||
"cw_home_vol_m2": "M.2 存储",
|
||||
"cw_home_vol_ext": "扩展存储",
|
||||
"cw_home_vol_readonly": "只读",
|
||||
"cw_home_free_of": "可用 {free},共 {total}",
|
||||
"cw_home_err_offline": "主机没有响应,请重试。",
|
||||
"cw_home_err_busy": "主机正忙,请稍后重试。",
|
||||
"cw_home_err_gone": "该任务已经结束。",
|
||||
"cw_home_err_failed": "操作未成功({code})。",
|
||||
"cw_home_err_load_tasks": "无法读取任务列表。",
|
||||
"cw_home_err_load_storage": "无法读取驱动器。",
|
||||
"cw_home_rest_sent": "主机正在进入待机模式。",
|
||||
"cw_home_restart_sent": "主机正在重启。恢复后请重新加载此页面。",
|
||||
"cw_home_confirm_cancel_title": "取消此任务?",
|
||||
"cw_home_confirm_cancel_body": "任务会立即停止,且无法撤销。",
|
||||
"cw_home_confirm_cancel_body_pc": "此任务由电脑发起,电脑端也会停止。",
|
||||
"cw_home_cancel_task_confirm": "取消任务",
|
||||
"cw_home_keep_task": "继续运行",
|
||||
"cw_home_confirm_close_title": "关闭 {name}?",
|
||||
"cw_home_confirm_close_body": "游戏中未保存的进度将丢失。",
|
||||
"cw_home_close_game": "关闭游戏",
|
||||
"cw_home_confirm_rest_title": "让主机进入待机模式?",
|
||||
"cw_home_confirm_rest_body": "主机唤醒前,此页面将断开连接。",
|
||||
"cw_home_rest": "待机模式",
|
||||
"cw_home_confirm_restart_title": "重启主机?",
|
||||
"cw_home_confirm_restart_body": "正在运行的游戏和任务会停止。主机恢复前,此页面将断开连接。",
|
||||
"cw_home_restart": "重启",
|
||||
"cw_home_loading": "正在加载…",
|
||||
"cw_home_no_tasks": "当前没有任务。",
|
||||
"cw_home_quick": "快捷操作",
|
||||
"cw_home_browse_usb": "浏览 USB",
|
||||
"cw_home_now_running": "正在运行",
|
||||
"cw_home_no_game": "没有正在运行的游戏。",
|
||||
"cw_home_tasks": "任务",
|
||||
"cw_home_storage": "存储",
|
||||
};
|
||||
|
||||
export default zh_CN;
|
||||
@@ -3996,6 +3996,58 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
|
||||
"cw_status_connecting": "正在連線…",
|
||||
"cw_status_offline": "未連線",
|
||||
"cw_hints_label": "控制器按鍵",
|
||||
"cw_task_from_console": "來自本主機",
|
||||
"cw_task_from_pc": "來自電腦",
|
||||
"cw_dialog_cancel": "取消",
|
||||
"cw_home_kind_upload": "上傳",
|
||||
"cw_home_kind_download": "下載",
|
||||
"cw_home_kind_copy": "複製",
|
||||
"cw_home_kind_op": "檔案操作",
|
||||
"cw_home_kind_other": "工作",
|
||||
"cw_home_progress_bytes": "{done} / {total}",
|
||||
"cw_home_progress_files": "{done} / {total} 個檔案",
|
||||
"cw_home_percent": "{n}%",
|
||||
"cw_home_state_running": "進行中",
|
||||
"cw_home_state_done": "已完成",
|
||||
"cw_home_state_failed": "失敗",
|
||||
"cw_home_cancel_task": "取消",
|
||||
"cw_home_vol_internal": "內建儲存空間",
|
||||
"cw_home_vol_usb": "USB 磁碟機",
|
||||
"cw_home_vol_usb_ext": "USB 延伸儲存空間",
|
||||
"cw_home_vol_m2": "M.2 儲存空間",
|
||||
"cw_home_vol_ext": "延伸儲存空間",
|
||||
"cw_home_vol_readonly": "唯讀",
|
||||
"cw_home_free_of": "可用 {free},共 {total}",
|
||||
"cw_home_err_offline": "主機沒有回應,請再試一次。",
|
||||
"cw_home_err_busy": "主機忙碌中,請稍後再試。",
|
||||
"cw_home_err_gone": "這項工作已經結束。",
|
||||
"cw_home_err_failed": "操作未成功({code})。",
|
||||
"cw_home_err_load_tasks": "無法讀取工作清單。",
|
||||
"cw_home_err_load_storage": "無法讀取磁碟機。",
|
||||
"cw_home_rest_sent": "主機正在進入休眠模式。",
|
||||
"cw_home_restart_sent": "主機正在重新啟動。恢復後請重新載入此頁面。",
|
||||
"cw_home_confirm_cancel_title": "要取消這項工作嗎?",
|
||||
"cw_home_confirm_cancel_body": "工作會立即停止,且無法復原。",
|
||||
"cw_home_confirm_cancel_body_pc": "這項工作由電腦啟動,電腦端也會停止。",
|
||||
"cw_home_cancel_task_confirm": "取消工作",
|
||||
"cw_home_keep_task": "繼續執行",
|
||||
"cw_home_confirm_close_title": "要關閉 {name} 嗎?",
|
||||
"cw_home_confirm_close_body": "遊戲中未儲存的進度將會遺失。",
|
||||
"cw_home_close_game": "關閉遊戲",
|
||||
"cw_home_confirm_rest_title": "要讓主機進入休眠模式嗎?",
|
||||
"cw_home_confirm_rest_body": "主機喚醒前,此頁面會中斷連線。",
|
||||
"cw_home_rest": "休眠模式",
|
||||
"cw_home_confirm_restart_title": "要重新啟動主機嗎?",
|
||||
"cw_home_confirm_restart_body": "執行中的遊戲和工作會停止。主機恢復前,此頁面會中斷連線。",
|
||||
"cw_home_restart": "重新啟動",
|
||||
"cw_home_loading": "載入中…",
|
||||
"cw_home_no_tasks": "目前沒有工作。",
|
||||
"cw_home_quick": "快速動作",
|
||||
"cw_home_browse_usb": "瀏覽 USB",
|
||||
"cw_home_now_running": "執行中",
|
||||
"cw_home_no_game": "沒有執行中的遊戲。",
|
||||
"cw_home_tasks": "工作",
|
||||
"cw_home_storage": "儲存空間",
|
||||
};
|
||||
|
||||
export default zh_TW;
|
||||
Reference in new issue
Block a user