console files: search and disk usage

Helper: /api/index/start|status|search|cancel over INDEX_START / STATUS /
SEARCH / CANCEL (handlers unchanged; the engine never calls them). Their
bodies are scanned quote to quote, so every root and pattern is checked
for no '"', '\' or control character and < 256 bytes (codec, selftested).
Roots: 1-8, absolute, never the trust store (403). The page's last start
(roots, started_at, cancelled) is remembered so status says whether the
index there is the page's and what it covers. Search answers at most 500
results, filters by `under` (asking the helper's 5,000 for a folder) and
never returns a trust-store path; truncated when the helper's limit or
the page's is reached. JSON buffers on the heap.

Page: Search (toolbar) — name or * / ? pattern (a word matches anywhere),
all drives or this folder; the first search builds the index for that
scope with a running count and Cancel, then searches; a covering index
is reused; results (name, folder, size) open in Files at the item. An
older helper (404) is told. Disk usage (toolbar) — the drive's
subfolders as bars, biggest first, measured one after another with
folderSize's walker under one budget (500k entries, 3 min, 2,000
subfolders), cancellable, one at a time page-wide (refuses while a
folder-size walk runs); "›" looks inside (○ comes back up), ✕ opens in
Files. Strings cw_files_search_* / cw_files_usage_* 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:
phantomptrandClaude Opus 5.5 committed 2026-10-17 14:11:33 -07:00
1 parent 3a1e8019b6
commit ec2c8f3a6b
55 files changed
+3794

No files matched your search

+36
View File
@@ -6,12 +6,14 @@ import {
ClipboardPaste,
ClipboardX,
Ellipsis,
ChartBar,
Filter,
FilterX,
FolderPlus,
ListChecks,
ListX,
RotateCw,
Search,
Square,
SquareCheck,
Star,
@@ -79,6 +81,8 @@ import { useDir, type DirState, type Entry } from "./useDir";
import { Viewer, type ViewKind } from "./Viewer";
import { GameBanner } from "./viewer/GameBanner";
import { sceSysName } from "./viewer/gameFolder";
import { SearchDialog } from "./search/SearchDialog";
import { UsageDialog } from "./usage/UsageDialog";
export { upFrom, viewKind };
@@ -100,6 +104,9 @@ export type FilesAsk =
| { kind: "pick"; op: "copy" | "move"; names: string[] }
| { kind: "info"; name: string }
| { kind: "view"; name: string; type: ViewKind }
/** Search by name (the helper's index) and Disk usage: Files tools over the folder shown. */
| { kind: "search" }
| { kind: "usage" }
/** A pinned or recent folder's menu (the drive list). */
| { kind: "place"; path: string }
/** Eject a USB drive; `force` after the helper said it is still in use. */
@@ -247,6 +254,8 @@ export interface FilesViewProps {
onUnpack?: (name: string, outName: string | null) => void;
onMkdir: (name: string) => void;
onPicked: (op: "copy" | "move", names: string[], destDir: string) => void;
/** Open `dir` in Files with `name` focused (a search result); null name: the folder itself. */
onReveal?: (dir: string, name: string | null) => void;
}
/** "Copying 3 of 40" (item `at` is being sent or is running), or "Stopping…". */
@@ -577,6 +586,17 @@ function askView(p: FilesViewProps) {
onClose={p.onDismiss}
/>
);
case "search":
return (
<SearchDialog
folder={path}
volumes={p.volumes ?? []}
onReveal={(dir, name) => p.onReveal?.(dir, name)}
onClose={p.onDismiss}
/>
);
case "usage":
return <UsageDialog folder={path} volumes={p.volumes ?? []} onOpen={(dir) => p.onReveal?.(dir, null)} onClose={p.onDismiss} />;
default:
return null;
}
@@ -785,6 +805,12 @@ export function FilesView(p: FilesViewProps) {
onClick={() => p.onAsk({ kind: "mkdir" })}
/>
<ToolButton icon={SquareCheck} label={t("cw_select")} action="select" onClick={p.onToggleSelecting} />
{p.onReveal ? (
<>
<ToolButton icon={Search} label={t("cw_files_search")} action="search" onClick={() => p.onAsk({ kind: "search" })} />
<ToolButton icon={ChartBar} label={t("cw_files_usage")} action="usage" onClick={() => p.onAsk({ kind: "usage" })} />
</>
) : null}
{p.onView ? (
<>
<ToolButton
@@ -1493,6 +1519,15 @@ export function Files({ ctx }: { ctx: SectionContext }) {
[go],
);
/** A search result or a disk usage row: open its folder with it focused. */
const onReveal = useCallback(
(dir: string, name: string | null) => {
setAsk(null);
go(dir, name ?? "");
},
[go],
);
return (
<div
ref={root}
@@ -1563,6 +1598,7 @@ export function Files({ ctx }: { ctx: SectionContext }) {
onUnpack={onUnpack}
onMkdir={onMkdir}
onPicked={onPicked}
onReveal={onReveal}
/>
</div>
);
@@ -0,0 +1,251 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { File, RotateCw, Search, Square } from "lucide-react";
import { formatBytes } from "../../../lib/format";
import { errorText, type Volume } from "../../home/Home";
import { t } from "../../strings";
import { Modal } from "../../ui/Modal";
import {
createSearchFlow,
driveRoots,
INITIAL_SEARCH,
namePattern,
searchApi,
type Hit,
type Scope,
type SearchFlow,
type SearchState,
} from "./search";
import "./search.css";
/** The index line: none yet, building (with its count), or ready (and how complete it is). */
export function indexText(s: SearchState): string | null {
if (s.unsupported) return t("cw_files_search_unsupported");
const i = s.index;
if (!i) return t("cw_home_loading");
const n = i.files.toLocaleString();
if (i.phase === "building") return s.stopping ? t("cw_files_search_stopping") : t("cw_files_search_building", { n });
if (i.phase === "idle" || (i.phase === "ready" && !i.roots)) return t("cw_files_search_no_index");
const notes = [t("cw_files_search_ready", { n })];
if (i.cancelled) notes.push(t("cw_files_search_partial"));
else if (i.truncated) notes.push(t("cw_files_search_capped"));
return notes.join(" ");
}
/** The results line: how many, whether there are more, or nothing found. */
export function resultsText(s: SearchState): string | null {
if (s.searching) return t("cw_files_search_searching");
if (!s.done) return null;
const n = s.done.results.hits.length;
if (!n) return t("cw_files_search_none", { q: s.done.pattern });
return s.done.results.truncated ? t("cw_files_search_more", { n }) : t("cw_files_search_found", { n });
}
/** The props-only view (tests render it). */
export function SearchView({
state,
field,
scope,
folder,
onField,
onScope,
onSubmit,
onCancel,
onRebuild,
onReveal,
onClose,
}: {
state: SearchState;
field: string;
scope: Scope;
folder: string;
onField: (v: string) => void;
onScope: (s: Scope) => void;
onSubmit: () => void;
onCancel: () => void;
onRebuild: () => void;
onReveal: (hit: Hit) => void;
onClose: () => void;
}) {
const building = state.index?.phase === "building";
const bad = field.trim() !== "" && namePattern(field) == null;
const idx = indexText(state);
const res = resultsText(state);
const err = state.error ? errorText(state.error) : null;
return (
<>
<div className="cw-viewer-head">
<h2 className="cw-file-name">{t("cw_files_search")}</h2>
<button type="button" className="cw-focusable cw-button" data-action="search-close" onClick={onClose}>
<span>{t("cw_files_close")}</span>
</button>
</div>
<form
className="cw-tv-bar cw-search-bar"
role="search"
onSubmit={(e) => {
e.preventDefault();
if (!bad) onSubmit();
}}
>
<label className="cw-tv-find">
<Search aria-hidden />
<input
className="cw-focusable cw-input cw-search-field"
type="text"
value={field}
placeholder={t("cw_files_search_field")}
aria-label={t("cw_files_search_field")}
data-nav-default=""
data-action="search-field"
autoComplete="off"
autoCapitalize="off"
spellCheck={false}
disabled={state.unsupported}
onChange={(e) => onField(e.target.value)}
/>
</label>
<button
type="submit"
className="cw-focusable cw-button is-primary"
data-action="search-go"
disabled={state.unsupported || !field.trim() || bad || state.searching}
>
<span>{t("cw_files_search_go")}</span>
</button>
<span className="cw-search-scope" role="group" aria-label={t("cw_files_search_scope")}>
<button
type="button"
className="cw-focusable cw-button"
data-scope="all"
aria-pressed={scope === "all"}
onClick={() => onScope("all")}
>
<span>{t("cw_files_search_all")}</span>
</button>
<button
type="button"
className="cw-focusable cw-button"
data-scope="folder"
aria-pressed={scope === "folder"}
onClick={() => onScope("folder")}
>
<span>{t("cw_files_search_here")}</span>
</button>
</span>
</form>
<p className="cw-dim cw-search-hint">
{scope === "folder" ? <span className="cw-mono">{folder} · </span> : null}
{t("cw_files_search_hint")}
</p>
{bad ? (
<p className="cw-field-error" role="alert">
{t("cw_files_search_bad")}
</p>
) : null}
<div className="cw-msg cw-search-index" role="status" data-index-phase={state.index?.phase ?? ""}>
<span>{idx}</span>
{building && !state.stopping ? (
<button type="button" className="cw-focusable cw-button cw-msg-action" data-action="index-cancel" onClick={onCancel}>
<Square aria-hidden />
<span>{t("cw_files_search_cancel")}</span>
</button>
) : null}
{!building && state.index?.phase === "ready" && state.index.roots ? (
<button type="button" className="cw-focusable cw-button cw-msg-action" data-action="index-rebuild" onClick={onRebuild}>
<RotateCw aria-hidden />
<span>{t("cw_files_search_rebuild")}</span>
</button>
) : null}
{building ? (
<div className="cw-bar" aria-hidden>
<div className="cw-bar-fill is-unknown" style={{ width: "100%" }} />
</div>
) : null}
{state.pending && building ? <span className="cw-dim cw-search-pending">{t("cw_files_search_waiting", { q: state.pending })}</span> : null}
</div>
{err ? (
<p className="cw-field-error" role="alert">
{err}
</p>
) : null}
<div className="cw-search-body">
{res ? (
<p className="cw-dim" role="status" data-results-line="">
{res}
</p>
) : null}
{state.done && state.done.results.hits.length ? (
<ul className="cw-files" data-nav-list aria-label={t("cw_files_search_results")}>
{state.done.results.hits.map((h) => (
<li key={h.path} className="cw-file is-plain" data-nav-item>
<button type="button" className="cw-focusable cw-file-main" data-hit={h.path} onClick={() => onReveal(h)}>
<File className="cw-file-icon" aria-hidden />
<span className="cw-file-text">
<span className="cw-file-name">{h.name}</span>
<span className="cw-dim cw-mono cw-file-sub">{h.folder}</span>
</span>
<span className="cw-file-meta cw-dim">{formatBytes(h.size)}</span>
</button>
</li>
))}
</ul>
) : null}
</div>
</>
);
}
/**
* Search (a Files tool): a name or pattern, all drives or this folder. The first
* search builds the helper's index for that scope (progress, Cancel); results
* open in Files at the item (✕). Closing leaves a running build to finish in the
* helper (the next search finds it ready).
*/
export function SearchDialog({
folder,
volumes,
onReveal,
onClose,
flowFactory,
}: {
folder: string;
volumes: Volume[];
onReveal: (dir: string, name: string) => void;
onClose: () => void;
/** Tests: a flow over fake calls. */
flowFactory?: (onChange: (s: SearchState) => void) => SearchFlow;
}) {
const [state, setState] = useState<SearchState>(INITIAL_SEARCH);
const [field, setField] = useState("");
const [scope, setScope] = useState<Scope>("all");
const flow = useRef<SearchFlow | null>(null);
const roots = useMemo(() => (scope === "all" ? driveRoots(volumes) : [folder]), [scope, volumes, folder]);
useEffect(() => {
const f = flowFactory
? flowFactory(setState)
: createSearchFlow(
{ api: searchApi, setTimer: (fn, ms) => setTimeout(fn, ms), clearTimer: (x) => clearTimeout(x as ReturnType<typeof setTimeout>) },
setState,
);
flow.current = f;
void f.open();
return () => f.close();
}, [flowFactory]);
return (
<Modal label={t("cw_files_search")} className="cw-viewer cw-search" onBack={onClose}>
<SearchView
state={state}
field={field}
scope={scope}
folder={folder}
onField={setField}
onScope={setScope}
onSubmit={() => void flow.current?.submit(field, scope, roots, scope === "folder" ? folder : null)}
onCancel={() => void flow.current?.cancel()}
onRebuild={() => void flow.current?.rebuild(roots)}
onReveal={(h) => onReveal(h.folder, h.name)}
onClose={onClose}
/>
</Modal>
);
}
@@ -0,0 +1,79 @@
/* ---- Files: search and disk usage ---- */
.cw-dialog.cw-search,
.cw-dialog.cw-usage {
display: flex;
flex-direction: column;
}
.cw-search-field {
width: 24rem;
}
.cw-search-scope {
display: inline-flex;
margin-left: 0.4rem;
}
.cw-search-scope .cw-button {
margin-right: 0.4rem;
}
.cw-search-scope [aria-pressed="true"] {
background: var(--color-accent-soft);
color: var(--color-accent);
}
.cw-search-hint {
margin: 0 0 0.5rem;
font-size: 0.9rem;
}
.cw-search-index {
flex: none;
}
.cw-search-index .cw-bar {
margin-top: 0.4rem;
}
.cw-search-pending {
display: block;
margin-top: 0.3rem;
}
.cw-search-body,
.cw-usage-body {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
.cw-search-body > p,
.cw-usage-body > p {
margin: 0 0 0.4rem;
}
.cw-usage-row .cw-file-main {
flex: 1 1 auto;
min-width: 0;
}
.cw-usage-bar {
display: block;
flex: 0 0 22rem;
margin: 0 1rem;
}
.cw-usage-bar .cw-bar {
display: block;
height: 0.7rem;
margin: 0;
}
.cw-usage-size {
flex: none;
width: 7.5rem;
text-align: right;
}
.cw-usage-path {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cw-usage-row .cw-button,
.cw-usage-spacer {
flex: none;
box-sizing: border-box;
width: 2.8rem;
margin-left: 0.4rem;
padding: 0.3rem 0;
justify-content: center;
}
@@ -0,0 +1,247 @@
import { describe, expect, it, vi } from "vitest";
import { ApiError } from "../../api";
import type { Volume } from "../../home/Home";
import {
covers,
createSearchFlow,
driveRoots,
namePattern,
POLL_MS,
toResults,
toStatus,
type IndexStatus,
type Results,
type SearchApi,
type SearchState,
} from "./search";
const vol = (path: string, mount_from = "/dev/x"): Volume => ({ path, mount_from, fs_type: "exfatfs", total_bytes: 1, free_bytes: 1, writable: true });
describe("search helpers", () => {
it("a word matches anywhere in the name; a pattern is used as typed", () => {
expect(namePattern(" eboot ")).toBe("*eboot*");
expect(namePattern("*.pkg")).toBe("*.pkg");
expect(namePattern("CUSA0784?")).toBe("CUSA0784?");
expect(namePattern("")).toBeNull();
expect(namePattern(" ")).toBeNull();
});
it("refuses what the helper's quote-to-quote scan would misread, and long patterns", () => {
expect(namePattern('a"b')).toBeNull();
expect(namePattern("a\\b")).toBeNull();
expect(namePattern("a\tb")).toBeNull();
expect(namePattern("x".repeat(253))).toBe(`*${"x".repeat(253)}*`); // 255 bytes: the helper's limit
expect(namePattern("x".repeat(254))).toBeNull(); // 256 bytes
expect(namePattern("é".repeat(127))).toBeNull(); // 254 UTF-8 bytes + 2
});
it("all drives = the drive list's drives (internal, extended, USB), never the system volumes", () => {
const v = [vol("/data", "/dev/ssd0.user"), vol("/system"), vol("/mnt/ext0", "/dev/nvme0"), vol("/mnt/usb0", "/dev/da0s1"), vol("/mnt/ps5upload/img")];
expect(driveRoots(v)).toEqual(["/data", "/mnt/ext0", "/mnt/usb0"]);
});
it("an index covers a scope inside one of its roots", () => {
expect(covers(["/data", "/mnt/usb0"], ["/data/games"])).toBe(true);
expect(covers(["/data", "/mnt/usb0"], ["/data", "/mnt/usb0"])).toBe(true);
expect(covers(["/data/games"], ["/data"])).toBe(false);
expect(covers(["/data"], ["/datax"])).toBe(false);
expect(covers(null, ["/data"])).toBe(false);
expect(covers(["/data"], [])).toBe(false);
});
it("status and results replies are checked", () => {
expect(toStatus({ phase: "ready", files: 12, truncated: true, cancelled: false, roots: ["/data", 3] })).toEqual({
phase: "ready",
files: 12,
truncated: true,
cancelled: false,
roots: ["/data"],
});
expect(toStatus({ phase: "done" })).toBeNull();
expect(toStatus(null)).toBeNull();
expect(toResults({ results: [{ path: "/data/a/b.pkg", size: 5 }, { path: "rel" }, { size: 1 }, null], truncated: true })).toEqual({
hits: [{ path: "/data/a/b.pkg", name: "b.pkg", folder: "/data/a", size: 5 }],
truncated: true,
});
expect(toResults({})).toEqual({ hits: [], truncated: false });
});
});
/* ---- the flow, over a fake helper and a manual clock ---- */
function fakeHelper(start: Partial<IndexStatus> = {}) {
let status: IndexStatus = { phase: "idle", files: 0, truncated: false, cancelled: false, roots: null, ...start };
const results: Results = { hits: [{ path: "/data/g/eboot.bin", name: "eboot.bin", folder: "/data/g", size: 9 }], truncated: false };
const api = {
status: vi.fn(async () => status),
start: vi.fn(async (roots: string[]) => {
status = { phase: "building", files: 0, truncated: false, cancelled: false, roots };
}),
cancel: vi.fn(async () => {
status = { ...status, phase: "ready", cancelled: true };
}),
search: vi.fn(async () => results),
} satisfies SearchApi;
return {
api,
set(s: Partial<IndexStatus>) {
status = { ...status, ...s };
},
};
}
function flowOver(api: SearchApi) {
const timers: (() => void)[] = [];
const states: SearchState[] = [];
const flow = createSearchFlow(
{
api,
setTimer: (fn, ms) => {
expect(ms).toBe(POLL_MS);
timers.push(fn);
return timers.length;
},
clearTimer: () => undefined,
},
(s) => states.push(s),
);
const tick = async () => {
const fn = timers.shift();
if (fn) fn();
await new Promise((r) => setTimeout(r, 0));
};
return { flow, timers, states, tick };
}
describe("search flow", () => {
it("first use builds the index for the scope (progress), then searches", async () => {
const h = fakeHelper();
const { flow, timers, tick } = flowOver(h.api);
await flow.open();
expect(flow.state().index?.phase).toBe("idle");
await flow.submit("eboot", "all", ["/data", "/mnt/usb0"], null);
expect(h.api.start).toHaveBeenCalledWith(["/data", "/mnt/usb0"]);
expect(flow.state().index?.phase).toBe("building");
expect(flow.state().pending).toBe("*eboot*");
expect(h.api.search).not.toHaveBeenCalled();
h.set({ files: 5000 });
await tick();
expect(flow.state().index?.files).toBe(5000);
h.set({ phase: "ready", files: 9000 });
await tick();
expect(h.api.search).toHaveBeenCalledWith("*eboot*", null);
expect(flow.state().done?.results.hits[0].name).toBe("eboot.bin");
expect(flow.state().pending).toBeNull();
expect(timers).toHaveLength(0); // no polling once ready
});
it("an index that covers the scope is searched at once (a folder scope filters by `under`)", async () => {
const h = fakeHelper({ phase: "ready", files: 10, roots: ["/data", "/mnt/usb0"] });
const { flow } = flowOver(h.api);
await flow.open();
await flow.submit("*.pkg", "folder", ["/data/games"], "/data/games");
expect(h.api.start).not.toHaveBeenCalled();
expect(h.api.search).toHaveBeenCalledWith("*.pkg", "/data/games");
expect(flow.state().done?.scope).toBe("folder");
});
it("an index that does not cover the scope (or another client's) is rebuilt for it", async () => {
const h = fakeHelper({ phase: "ready", files: 10, roots: ["/data/games"] });
const { flow } = flowOver(h.api);
await flow.open();
await flow.submit("a", "all", ["/data"], null);
expect(h.api.start).toHaveBeenCalledWith(["/data"]);
});
it("a search while it builds waits for the build; another client's build gets one build of ours", async () => {
const h = fakeHelper({ phase: "building", roots: null });
const { flow, tick } = flowOver(h.api);
await flow.open();
await flow.submit("x", "all", ["/data"], null);
expect(h.api.start).not.toHaveBeenCalled();
h.set({ phase: "ready", roots: null });
await tick(); // not ours: build once for the scope
expect(h.api.start).toHaveBeenCalledTimes(1);
h.set({ phase: "ready" });
await tick();
expect(h.api.search).toHaveBeenCalledTimes(1);
expect(h.api.start).toHaveBeenCalledTimes(1);
});
it("Cancel stops the build: the partial index stays, nothing is searched", async () => {
const h = fakeHelper();
const { flow, tick } = flowOver(h.api);
await flow.open();
await flow.submit("x", "all", ["/data"], null);
await flow.cancel();
expect(h.api.cancel).toHaveBeenCalled();
expect(flow.state().stopping).toBe(true);
expect(flow.state().pending).toBeNull();
await tick();
expect(flow.state().index).toMatchObject({ phase: "ready", cancelled: true });
expect(flow.state().stopping).toBe(false);
expect(h.api.search).not.toHaveBeenCalled();
// the partial index covers the scope: the next search uses it
await flow.submit("x", "all", ["/data"], null);
expect(h.api.search).toHaveBeenCalledTimes(1);
expect(h.api.start).toHaveBeenCalledTimes(1);
});
it("another build already running (409 busy) is waited on instead", async () => {
const h = fakeHelper();
h.api.start.mockImplementationOnce(async () => {
h.set({ phase: "building", roots: ["/data"] });
throw new ApiError(409, "busy");
});
const { flow, tick } = flowOver(h.api);
await flow.open();
await flow.submit("x", "all", ["/data"], null);
expect(flow.state().error).toBeNull();
h.set({ phase: "ready" });
await tick();
expect(h.api.search).toHaveBeenCalled();
});
it("an older helper (no index routes) says so; nothing else is tried", async () => {
const h = fakeHelper();
h.api.status.mockRejectedValue(new ApiError(404, "not_found"));
const { flow } = flowOver(h.api);
await flow.open();
expect(flow.state().unsupported).toBe(true);
await flow.submit("x", "all", ["/data"], null);
expect(h.api.start).not.toHaveBeenCalled();
});
it("a failed start shows its error and forgets the pending search", async () => {
const h = fakeHelper();
h.api.start.mockRejectedValueOnce(new ApiError(403, "path_not_allowed"));
const { flow } = flowOver(h.api);
await flow.open();
await flow.submit("x", "folder", ["/data/ps5upload"], "/data/ps5upload");
expect(flow.state().error).toBeInstanceOf(ApiError);
expect(flow.state().pending).toBeNull();
});
it("closing stops the polling (the build itself goes on in the helper)", async () => {
const h = fakeHelper();
const { flow, timers, states } = flowOver(h.api);
await flow.open();
await flow.submit("x", "all", ["/data"], null);
expect(timers).toHaveLength(1);
flow.close();
const seen = states.length;
timers.shift()?.();
await new Promise((r) => setTimeout(r, 0));
expect(states.length).toBe(seen);
expect(h.api.cancel).not.toHaveBeenCalled();
});
it("an invalid pattern is not sent", async () => {
const h = fakeHelper({ phase: "ready", roots: ["/data"] });
const { flow } = flowOver(h.api);
await flow.open();
await flow.submit('say "hi"', "all", ["/data"], null);
expect(h.api.search).not.toHaveBeenCalled();
expect(h.api.start).not.toHaveBeenCalled();
});
});
+319
View File
@@ -0,0 +1,319 @@
/**
* Search by name with the helper's file index (/api/index/*, over INDEX_START /
* STATUS / SEARCH / CANCEL). One index for the whole helper: built by a thread
* of its own over up to 8 roots (at most 200,000 files and 32 MiB of paths, ten
* folders deep, dot names skipped), cancellable between folders. A search
* matches the whole file NAME against a pattern (* and ?, ASCII
* case-insensitive); a plain word is looked for anywhere in the name.
*
* The flow (createSearchFlow) is a small state machine with injected calls, so
* the page's behaviour is tested without a DOM: first use builds the index for
* the scope (progress + Cancel), then searches; a later search reuses an index
* that covers the scope.
*/
import { ApiError, call } from "../../api";
import { isDrive, type Volume } from "../../home/Home";
import { baseName, parentPath } from "../move";
export interface IndexStatus {
phase: "idle" | "building" | "ready";
files: number;
truncated: boolean;
/** The page stopped the build: what it found is all there is. */
cancelled: boolean;
/** The roots the page built this index for; null when it is not the page's (never built, or another client's). */
roots: string[] | null;
}
export interface Hit {
path: string;
name: string;
folder: string;
size: number;
}
export interface Results {
hits: Hit[];
/** There may be more than these (the helper's limit, or the page's 500). */
truncated: boolean;
}
/** A root or pattern the helper's scan can carry: no quote, backslash or control character, < 256 bytes. */
function plain(s: string): boolean {
if (!s) return false;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c < 0x20 || c === 0x7f || c === 0x22 || c === 0x5c) return false;
}
return new TextEncoder().encode(s).length < 256;
}
/**
* What the field holds as the helper's pattern: as written when it has * or ?,
* else "*word*" (anywhere in the name). null: empty, or a character the helper
* cannot take (" \ or a control character), or too long.
*/
export function namePattern(text: string): string | null {
const q = text.trim();
if (!q) return null;
const p = /[*?]/.test(q) ? q : `*${q}*`;
return plain(p) ? p : null;
}
/** The drives the drive list shows (internal, M.2/extended, USB), as index roots (at most 8). */
export function driveRoots(volumes: Volume[]): string[] {
return volumes
.filter(isDrive)
.map((v) => v.path.replace(/\/+$/, "") || "/")
.filter(plain)
.slice(0, 8);
}
/** `path` is `root` or inside it. */
export function isUnder(path: string, root: string): boolean {
const r = root.replace(/\/+$/, "");
if (r === "") return true;
return path === r || path.startsWith(`${r}/`);
}
/** The index (its roots) covers every one of `scope`'s roots. */
export function covers(roots: string[] | null, scope: string[]): boolean {
if (!roots || !roots.length || !scope.length) return false;
return scope.every((s) => roots.some((r) => isUnder(s, r)));
}
/** /api/index/status's reply, checked. */
export function toStatus(raw: unknown): IndexStatus | null {
if (!raw || typeof raw !== "object") return null;
const o = raw as Record<string, unknown>;
if (o.phase !== "idle" && o.phase !== "building" && o.phase !== "ready") return null;
return {
phase: o.phase,
files: typeof o.files === "number" && o.files >= 0 ? o.files : 0,
truncated: o.truncated === true,
cancelled: o.cancelled === true,
roots: Array.isArray(o.roots) ? o.roots.filter((r): r is string => typeof r === "string") : null,
};
}
/** /api/index/search's reply as hits (entries without a usable path are left out). */
export function toResults(raw: unknown): Results {
const o = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
const list = Array.isArray(o.results) ? o.results : [];
const hits: Hit[] = [];
for (const r of list) {
if (!r || typeof r !== "object") continue;
const x = r as Record<string, unknown>;
if (typeof x.path !== "string" || !x.path.startsWith("/")) continue;
const name = baseName(x.path);
if (!name) continue;
hits.push({ path: x.path, name, folder: parentPath(x.path), size: typeof x.size === "number" ? x.size : 0 });
}
return { hits, truncated: o.truncated === true };
}
export interface SearchApi {
status(): Promise<IndexStatus>;
start(roots: string[]): Promise<void>;
cancel(): Promise<void>;
search(pattern: string, under: string | null): Promise<Results>;
}
export const searchApi: SearchApi = {
async status() {
const s = toStatus(await call<unknown>("/api/index/status"));
if (!s) throw new ApiError(500, "bad_index_reply");
return s;
},
async start(roots) {
await call("/api/index/start", { roots });
},
async cancel() {
await call("/api/index/cancel");
},
async search(pattern, under) {
return toResults(await call<unknown>("/api/index/search", under ? { query: pattern, under } : { query: pattern }));
},
};
/* ---- the flow ---- */
export type Scope = "all" | "folder";
export interface SearchState {
/** null until the first status read. */
index: IndexStatus | null;
/** The helper has no index routes (an older helper). */
unsupported: boolean;
/** The last call's failure (shown as a line), or null. */
error: unknown;
/** The search that runs once the index is built (the field's pattern), or null. */
pending: string | null;
/** A search request is running. */
searching: boolean;
/** The last search's pattern and scope, with its results. */
done: { pattern: string; scope: Scope; results: Results } | null;
/** Cancel was asked and the build has not stopped yet. */
stopping: boolean;
}
export const INITIAL_SEARCH: SearchState = {
index: null,
unsupported: false,
error: null,
pending: null,
searching: false,
done: null,
stopping: false,
};
/** How often the status is read while the index builds. */
export const POLL_MS = 1000;
export interface FlowDeps {
api: SearchApi;
setTimer: (fn: () => void, ms: number) => unknown;
clearTimer: (t: unknown) => void;
}
export interface SearchFlow {
state(): SearchState;
/** Read the index status (on open); polls while it builds. */
open(): Promise<void>;
/**
* Search for `text` in `scope` (`roots` its index roots, `under` the folder
* to search within, or null for every root): builds the index first when the
* one there does not cover `roots`.
*/
submit(text: string, scope: Scope, roots: string[], under: string | null): Promise<void>;
/** Build the index again for `roots` (results may be stale). */
rebuild(roots: string[]): Promise<void>;
/** Stop the build: what it found so far stays searchable. */
cancel(): Promise<void>;
/** The dialog closed: stop polling and forget the pending search (a build keeps running in the helper). */
close(): void;
}
const isMissingRoute = (e: unknown) => e instanceof ApiError && e.status === 404 && e.code === "not_found";
export function createSearchFlow(deps: FlowDeps, onChange: (s: SearchState) => void): SearchFlow {
let s: SearchState = INITIAL_SEARCH;
let timer: unknown = null;
let closed = false;
/** The scope the pending search runs in once the build ends. */
let pendingScope: { scope: Scope; under: string | null; roots: string[]; built: boolean } | null = null;
const set = (patch: Partial<SearchState>) => {
if (closed) return;
s = { ...s, ...patch };
onChange(s);
};
const stopPoll = () => {
if (timer != null) deps.clearTimer(timer);
timer = null;
};
const runSearch = async (pattern: string, scope: Scope, under: string | null) => {
set({ searching: true, error: null, pending: null });
try {
const results = await deps.api.search(pattern, under);
set({ searching: false, done: { pattern, scope, results } });
} catch (e) {
set({ searching: false, error: e });
}
};
const poll = async (): Promise<void> => {
timer = null;
if (closed) return;
let st: IndexStatus;
try {
st = await deps.api.status();
} catch (e) {
if (isMissingRoute(e)) return set({ unsupported: true });
set({ error: e });
// A read that failed (the helper busy, a blip) is tried again while the build may still run.
if (!closed) timer = deps.setTimer(() => void poll(), POLL_MS);
return;
}
set({ index: st, error: null, ...(st.phase !== "building" ? { stopping: false } : {}) });
if (st.phase === "building") {
if (!closed) timer = deps.setTimer(() => void poll(), POLL_MS);
return;
}
const pending = s.pending;
const where = pendingScope;
pendingScope = null;
if (!pending || !where) return;
// The build this search asked for (or one that covers it) ended: search. A build that was
// someone else's and does not cover the scope: build once for it, then search.
if (where.built || covers(st.roots, where.roots)) await runSearch(pending, where.scope, where.under);
else {
pendingScope = { ...where, built: true };
await build(where.roots);
}
};
const build = async (roots: string[]): Promise<boolean> => {
stopPoll();
try {
await deps.api.start(roots);
} catch (e) {
// Already building (another start won the race): wait for that one instead.
if (!(e instanceof ApiError && e.status === 409)) {
pendingScope = null;
if (isMissingRoute(e)) set({ unsupported: true, pending: null });
else set({ error: e, pending: null });
return false;
}
}
set({ index: { phase: "building", files: 0, truncated: false, cancelled: false, roots }, stopping: false, done: null });
await poll();
return true;
};
return {
state: () => s,
async open() {
closed = false;
await poll();
},
async submit(text, scope, roots, under) {
const pattern = namePattern(text);
if (!pattern || s.unsupported) return;
set({ error: null });
const idx = s.index;
if (idx?.phase === "building") {
// Searched while it builds: run it when the build ends (the latest one wins).
pendingScope = { scope, under, roots, built: false };
set({ pending: pattern });
return;
}
if (idx?.phase === "ready" && covers(idx.roots, roots)) {
await runSearch(pattern, scope, under);
return;
}
pendingScope = { scope, under, roots, built: true };
set({ pending: pattern });
await build(roots);
},
async rebuild(roots) {
if (s.index?.phase === "building") return;
await build(roots);
},
async cancel() {
if (s.index?.phase !== "building") return;
set({ stopping: true, pending: null });
pendingScope = null;
try {
await deps.api.cancel();
} catch (e) {
set({ stopping: false, error: e });
}
},
close() {
closed = true;
stopPoll();
pendingScope = null;
},
};
}
+197
View File
@@ -0,0 +1,197 @@
import { isValidElement, type ReactElement, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
vi.mock("../api", async (importOriginal) => {
const real = await importOriginal<typeof import("../api")>();
return { ...real, call: vi.fn(), callBlob: vi.fn() };
});
import { formatBytes } from "../../lib/format";
import { ApiError } from "../api";
import type { Volume } from "../home/Home";
import { t } from "../strings";
import { FilesView, type FilesViewProps } from "./Files";
import { SearchDialog, SearchView, indexText, resultsText } from "./search/SearchDialog";
import { INITIAL_SEARCH, type SearchState } from "./search/search";
import type { DirState, Entry } from "./useDir";
import { UsageDialog, UsageView, rowSize, usageLine } from "./usage/UsageDialog";
import type { UsageSnapshot } from "./usage/usage";
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;
}
function find<T>(node: ReactNode, type: T): ReactElement<Record<string, unknown>> {
const el = elements(node).find((e) => e.type === type);
if (!el) throw new Error("not rendered");
return el;
}
const entry = (name: string, kind: Entry["kind"] = "file", size = 10): Entry => ({ name, kind, size, mtime: 0 });
const dir = (entries: Entry[]): DirState => ({ entries, more: false, loading: false, loaded: true, error: null });
const vol = (path: string): Volume => ({ path, mount_from: "/dev/ssd0.user", fs_type: "ufs", total_bytes: 1000, free_bytes: 400, writable: true });
const ENTRIES = [entry("sce_sys", "dir"), entry("b.png"), entry("notes.txt"), entry("a.jpg"), entry("eboot.bin"), entry("c.PNG")];
function props(over: Partial<FilesViewProps> = {}): FilesViewProps {
return {
path: "/data/games/Astro",
volumes: [vol("/data")],
volumesError: false,
dir: dir(ENTRIES),
selecting: false,
selected: [],
ask: null,
busy: null,
message: null,
tasks: [],
onOpen: vi.fn(),
onUp: vi.fn(),
onEntry: vi.fn(),
onLoadMore: vi.fn(),
onToggleSelecting: vi.fn(),
onToggleItem: vi.fn(),
onAsk: vi.fn(),
onDismiss: vi.fn(),
onConfirmDelete: vi.fn(),
onConfirmFixPerm: vi.fn(),
onRename: vi.fn(),
onMkdir: vi.fn(),
onPicked: vi.fn(),
...over,
};
}
describe("search page", () => {
it("is a Files tool: the toolbar button, and the dialog for the folder shown", () => {
const onReveal = vi.fn();
const html = renderToStaticMarkup(<>{FilesView(props({ onReveal }))}</>);
expect(html).toContain('data-action="search"');
expect(html).toContain('data-action="usage"');
expect(renderToStaticMarkup(<>{FilesView(props())}</>)).not.toContain('data-action="search"');
const d = find(FilesView(props({ onReveal, ask: { kind: "search" } })), SearchDialog);
expect(d.props.folder).toBe("/data/games/Astro");
(d.props.onReveal as (dir: string, name: string) => void)("/data/x", "eboot.bin");
expect(onReveal).toHaveBeenCalledWith("/data/x", "eboot.bin");
});
const base = (over: Partial<SearchState>): SearchState => ({ ...INITIAL_SEARCH, ...over });
const view = (s: SearchState, field = "eboot") =>
renderToStaticMarkup(
<SearchView
state={s}
field={field}
scope="all"
folder="/data/g"
onField={vi.fn()}
onScope={vi.fn()}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRebuild={vi.fn()}
onReveal={vi.fn()}
onClose={vi.fn()}
/>,
);
it("building: the count, a Cancel button, and the search that waits", () => {
const s = base({ index: { phase: "building", files: 12345, truncated: false, cancelled: false, roots: ["/data"] }, pending: "*eboot*" });
const html = view(s);
expect(html).toContain(t("cw_files_search_building", { n: (12345).toLocaleString() }));
expect(html).toContain('data-action="index-cancel"');
expect(html).toContain(t("cw_files_search_waiting", { q: "*eboot*" }));
expect(indexText(base({ ...s, stopping: true }))).toBe(t("cw_files_search_stopping"));
});
it("results: name, folder and size per row; more-than-shown said", () => {
const s = base({
index: { phase: "ready", files: 9, truncated: false, cancelled: true, roots: ["/data"] },
done: { pattern: "*eboot*", scope: "all", results: { hits: [{ path: "/data/g/eboot.bin", name: "eboot.bin", folder: "/data/g", size: 2048 }], truncated: true } },
});
const html = view(s);
expect(html).toContain('data-hit="/data/g/eboot.bin"');
expect(html).toContain("eboot.bin");
expect(html).toContain("/data/g");
expect(html).toContain("2.00 KiB");
expect(html).toContain(t("cw_files_search_more", { n: 1 }));
expect(html).toContain(t("cw_files_search_partial"));
expect(html).toContain('data-action="index-rebuild"');
expect(resultsText(base({ done: { pattern: "*x*", scope: "all", results: { hits: [], truncated: false } } }))).toBe(
t("cw_files_search_none", { q: "*x*" }),
);
});
it("an older helper, a bad pattern, an error", () => {
expect(view(base({ unsupported: true }))).toContain(t("cw_files_search_unsupported"));
const bad = view(base({ index: { phase: "idle", files: 0, truncated: false, cancelled: false, roots: null } }), 'a"b');
expect(bad).toContain(t("cw_files_search_bad"));
expect(bad).toMatch(/data-action="search-go"[^>]*disabled/);
expect(bad).toContain(t("cw_files_search_no_index"));
expect(view(base({ error: new ApiError(500, "oom") }))).toContain("oom");
});
});
describe("disk usage page", () => {
it("is a Files tool; a row opens its folder in Files", () => {
const onReveal = vi.fn();
const d = find(FilesView(props({ onReveal, ask: { kind: "usage" } })), UsageDialog);
expect(d.props.folder).toBe("/data/games/Astro");
(d.props.onOpen as (p: string) => void)("/data/games");
expect(onReveal).toHaveBeenCalledWith("/data/games", null);
});
const snap: UsageSnapshot = {
root: "/data",
rows: [
{ name: "", path: "/data", files: true, bytes: 150, entries: 2, state: "done" },
{ name: "music", path: "/data/music", files: false, bytes: 700, entries: 1, state: "done" },
{ name: "games", path: "/data/games", files: false, bytes: 8000, entries: 3, state: "walking" },
{ name: "zz", path: "/data/zz", files: false, bytes: 0, entries: 0, state: "waiting" },
],
measured: 1,
folders: 3,
entries: 6,
end: null,
};
it("bars biggest first, drill-down and open buttons, Cancel while measuring", () => {
const html = renderToStaticMarkup(
<UsageView
root="/data"
canUp={false}
snap={snap}
refused={false}
onUp={vi.fn()}
onDrill={vi.fn()}
onOpen={vi.fn()}
onCancel={vi.fn()}
onAgain={vi.fn()}
onClose={vi.fn()}
/>,
);
const order = ["games", "music", "", "zz"].map((n) => html.indexOf(`data-usage-row="${n}"`));
expect(order).toEqual([...order].sort((a, b) => a - b));
expect(html).toContain('style="display:block;width:100%"');
expect(html).toContain('data-usage-drill="/data/games"');
expect(html).not.toContain('data-usage-drill="/data"');
expect(html).toContain('data-action="usage-cancel"');
expect(html).toMatch(/data-action="usage-up"[^>]*disabled/);
expect(html).toContain(t("cw_files_usage_running", { at: 1, n: 3, items: "6" }));
expect(rowSize(snap.rows[2])).toMatch(/\+$/);
expect(rowSize(snap.rows[3])).toBe("…");
});
it("how it ended", () => {
expect(usageLine({ ...snap, end: "done" }, false)?.text).toBe(t("cw_files_usage_done", { size: formatBytes(8850), items: "6" }));
expect(usageLine({ ...snap, end: "cancelled" }, false)?.tone).toBe("info");
expect(usageLine(null, true)?.text).toBe(t("cw_files_usage_busy"));
expect(usageLine({ ...snap, end: "error", error: new ApiError(401, "token") }, false)).toBeNull();
});
});
@@ -0,0 +1,240 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowUp, ChevronRight, Files as FilesIcon, Folder, FolderOpen, RotateCw, Square } from "lucide-react";
import { formatBytes } from "../../../lib/format";
import { errorText, type Volume } from "../../home/Home";
import { focusElement } from "../../nav";
import { t } from "../../strings";
import { Modal } from "../../ui/Modal";
import { parentPath, volumeOf } from "../move";
import { barPercent, sortRows, startUsage, stopUsage, type UsageRow, type UsageSnapshot } from "./usage";
import "../search/search.css";
/** The status line under the path: progress while measuring, else how it ended. */
export function usageLine(s: UsageSnapshot | null, refused: boolean): { text: string; tone: "info" | "error" | "plain" } | null {
if (refused) return { text: t("cw_files_usage_busy"), tone: "error" };
if (!s) return { text: t("cw_home_loading"), tone: "plain" };
const total = s.rows.reduce((a, r) => a + r.bytes, 0);
const items = s.entries.toLocaleString();
switch (s.end) {
case null:
return { text: t("cw_files_usage_running", { at: s.measured, n: s.folders, items }), tone: "plain" };
case "done":
return { text: t("cw_files_usage_done", { size: formatBytes(total), items }), tone: "plain" };
case "capped":
return { text: t("cw_files_usage_capped", { size: formatBytes(total), items }), tone: "info" };
case "timeout":
return { text: t("cw_files_usage_timeout", { size: formatBytes(total) }), tone: "info" };
case "cancelled":
return { text: t("cw_files_usage_cancelled", { size: formatBytes(total) }), tone: "info" };
default: {
const why = errorText(s.error);
return why ? { text: why, tone: "error" } : null;
}
}
}
/** A row's size column: its size, with a mark when it is not the whole story. */
export function rowSize(r: UsageRow): string {
if (r.state === "waiting") return "…";
if (r.state === "skipped") return t("cw_files_usage_skipped");
if (r.state === "unreadable") return t("cw_files_usage_unreadable");
const s = formatBytes(r.bytes);
return r.state === "partial" || r.state === "walking" ? `${s}+` : s;
}
/** The props-only view (tests render it). */
export function UsageView({
root,
canUp,
snap,
refused,
onUp,
onDrill,
onOpen,
onCancel,
onAgain,
onClose,
}: {
root: string;
canUp: boolean;
snap: UsageSnapshot | null;
refused: boolean;
onUp: () => void;
onDrill: (path: string) => void;
onOpen: (path: string) => void;
onCancel: () => void;
onAgain: () => void;
onClose: () => void;
}) {
const rows = snap ? sortRows(snap.rows).filter((r) => !r.files || r.bytes > 0 || r.entries > 0) : [];
const max = rows.reduce((a, r) => Math.max(a, r.bytes), 0);
const line = usageLine(snap, refused);
const running = snap != null && snap.end == null;
return (
<>
<div className="cw-viewer-head">
<h2 className="cw-file-name">{t("cw_files_usage")}</h2>
<button type="button" className="cw-focusable cw-button" data-action="usage-close" onClick={onClose}>
<span>{t("cw_files_close")}</span>
</button>
</div>
<div className="cw-tv-bar">
<button type="button" className="cw-focusable cw-button" data-action="usage-up" disabled={!canUp} onClick={onUp}>
<ArrowUp aria-hidden />
<span>{t("cw_files_up")}</span>
</button>
<span className="cw-mono cw-usage-path" data-usage-root={root}>
{root}
</span>
<button type="button" className="cw-focusable cw-button" data-action="usage-open-root" onClick={() => onOpen(root)}>
<FolderOpen aria-hidden />
<span>{t("cw_files_usage_open")}</span>
</button>
</div>
{line ? (
<div className={`cw-msg${line.tone !== "plain" ? ` is-${line.tone}` : ""}`} role="status" data-usage-end={snap?.end ?? "running"}>
{line.text}
{running ? (
<button type="button" className="cw-focusable cw-button cw-msg-action" data-action="usage-cancel" onClick={onCancel}>
<Square aria-hidden />
<span>{t("cw_files_search_cancel")}</span>
</button>
) : (
<button type="button" className="cw-focusable cw-button cw-msg-action" data-action="usage-again" onClick={onAgain}>
<RotateCw aria-hidden />
<span>{t("cw_files_usage_again")}</span>
</button>
)}
</div>
) : null}
<div className="cw-usage-body">
{snap && snap.end != null && !rows.length ? <p className="cw-dim">{t("cw_files_empty")}</p> : null}
<ul className="cw-files" data-nav-list aria-label={root}>
{rows.map((r) => {
const Icon = r.files ? FilesIcon : Folder;
const pct = barPercent(r.bytes, max);
return (
<li key={r.path + (r.files ? "/" : "")} className="cw-file cw-usage-row" data-nav-item data-usage-row={r.files ? "" : r.name}>
<button
type="button"
className="cw-focusable cw-file-main"
data-usage-open={r.path}
aria-label={t("cw_files_usage_open_row", { name: r.files ? t("cw_files_usage_files_here") : r.name })}
onClick={() => onOpen(r.path)}
>
<Icon className="cw-file-icon" aria-hidden />
<span className="cw-file-text">
<span className="cw-file-name">{r.files ? t("cw_files_usage_files_here") : r.name}</span>
</span>
<span className="cw-usage-bar" aria-hidden>
<span className="cw-bar">
<span className={`cw-bar-fill${r.state === "walking" ? " is-unknown" : ""}`} style={{ display: "block", width: `${pct}%` }} />
</span>
</span>
<span className="cw-file-meta cw-dim cw-usage-size">{rowSize(r)}</span>
</button>
{!r.files ? (
<button
type="button"
className="cw-focusable cw-button"
data-usage-drill={r.path}
aria-label={t("cw_files_usage_inside", { name: r.name })}
data-tip={t("cw_files_usage_inside", { name: r.name })}
onClick={() => onDrill(r.path)}
>
<ChevronRight aria-hidden />
</button>
) : (
<span className="cw-usage-spacer" aria-hidden />
)}
</li>
);
})}
</ul>
</div>
</>
);
}
/**
* Disk usage (a Files tool): the subfolders of a folder (at first the drive
* Files is on) as bars, biggest first, measured one after another. "›" looks
* inside a folder (a new measurement; ○ comes back up); ✕ on a row opens it in
* Files. One measurement at a time; closing the dialog cancels it.
*/
export function UsageDialog({
folder,
volumes,
onOpen,
onClose,
}: {
/** The folder Files shows: the measurement starts at its drive. */
folder: string;
volumes: Volume[];
onOpen: (path: string) => void;
onClose: () => void;
}) {
const top = (volumeOf(folder, volumes)?.path ?? folder).replace(/\/+$/, "") || "/";
const [root, setRoot] = useState(top);
const [snap, setSnap] = useState<UsageSnapshot | null>(null);
const [refused, setRefused] = useState(false);
const [tick, setTick] = useState(0);
useEffect(() => {
let run: ReturnType<typeof startUsage> = null;
let gone = false;
setSnap(null);
setRefused(false);
// One at a time: the measurement shown before (a drill or Up) ends first.
void stopUsage().then(() => {
if (gone) return;
run = startUsage(root, (s) => {
if (!gone) setSnap(s);
});
if (!run) setRefused(true);
});
return () => {
gone = true;
run?.cancel();
};
}, [root, tick]);
/** Where focus goes once it can: Up after looking inside, the folder we came out of after Up. */
const focusNext = useRef<[string, string] | null>(null);
useEffect(() => {
const want = focusNext.current;
if (!want || typeof document === "undefined") return;
const [attr, value] = want;
// Compared as-is (no CSS escaping of folder names).
const el = Array.from(document.querySelectorAll<HTMLElement>(`[${attr}]`)).find((x) => x.getAttribute(attr) === value);
if (el && !(el as HTMLButtonElement).disabled) {
focusNext.current = null;
focusElement(el);
} else if (snap?.end != null) focusNext.current = null;
}, [root, snap]);
const canUp = root !== top && root !== "/";
const onUp = useCallback(() => {
if (root === top) return;
focusNext.current = ["data-usage-drill", root];
setRoot(parentPath(root));
}, [root, top]);
const onDrill = useCallback((path: string) => {
focusNext.current = ["data-action", "usage-up"];
setRoot(path);
}, []);
return (
<Modal label={t("cw_files_usage")} className="cw-viewer cw-usage" onBack={canUp ? onUp : onClose}>
<UsageView
root={root}
canUp={canUp}
snap={snap}
refused={refused}
onUp={onUp}
onDrill={onDrill}
onOpen={onOpen}
onCancel={() => void stopUsage()}
onAgain={() => setTick((n) => n + 1)}
onClose={onClose}
/>
</Modal>
);
}
@@ -0,0 +1,167 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { ApiError } from "../../api";
import { startFolderSize, type ListPage, type WalkDeps } from "../folderSize";
import type { Entry } from "../useDir";
import { barPercent, measureUsage, sortRows, startUsage, stopUsage, usageRunning, type UsageSnapshot } from "./usage";
const f = (name: string, size: number): Entry => ({ name, kind: "file", size, mtime: 0 });
const d = (name: string): Entry => ({ name, kind: "dir", size: 0, mtime: 0 });
/** A fake tree: path -> entries; pages of `page` entries. */
function tree(t: Record<string, Entry[]>, opts: { page?: number; fail?: Record<string, unknown>; clock?: { t: number; per: number } } = {}) {
const page = opts.page ?? 2;
const calls: string[] = [];
const deps: WalkDeps = {
list: vi.fn(async (path: string, offset: number): Promise<ListPage> => {
calls.push(`${path}@${offset}`);
if (opts.clock) opts.clock.t += opts.clock.per;
if (opts.fail && path in opts.fail) throw opts.fail[path];
const all = t[path];
if (!all) throw new ApiError(404, "not_found");
const entries = all.slice(offset, offset + page);
return { entries, more: offset + page < all.length, next: offset + entries.length };
}),
now: () => opts.clock?.t ?? 0,
};
return { deps, calls };
}
const TREE: Record<string, Entry[]> = {
"/data": [d("games"), d("music"), f("a.bin", 100), f("b.bin", 50), d("empty")],
"/data/games": [d("A"), f("g.pkg", 5000)],
"/data/games/A": [f("eboot.bin", 3000), f("x", 1)],
"/data/music": [f("m.mp3", 700)],
"/data/empty": [],
};
describe("disk usage", () => {
afterEach(async () => {
await stopUsage();
});
it("each subfolder's size (all of its tree) and the files here, with progress", async () => {
const { deps } = tree(TREE);
const updates: UsageSnapshot[] = [];
const s = await measureUsage("/data", { cancelled: () => false, onUpdate: (u) => updates.push(u) }, deps);
expect(s.end).toBe("done");
const by = Object.fromEntries(s.rows.map((r) => [r.files ? "(files)" : r.name, [r.bytes, r.state]]));
expect(by).toEqual({
"(files)": [150, "done"],
games: [8001, "done"],
music: [700, "done"],
empty: [0, "done"],
});
expect(s.folders).toBe(3);
expect(s.measured).toBe(3);
expect(updates.length).toBeGreaterThan(3);
// a row is "walking" while its walk runs
expect(updates.some((u) => u.rows.some((r) => r.name === "games" && r.state === "walking"))).toBe(true);
});
it("biggest first; waiting rows after the measured ones; bars against the biggest", () => {
const rows = sortRows([
{ name: "a", path: "/a", files: false, bytes: 5, entries: 1, state: "done" },
{ name: "b", path: "/b", files: false, bytes: 0, entries: 0, state: "waiting" },
{ name: "c", path: "/c", files: false, bytes: 50, entries: 1, state: "done" },
]);
expect(rows.map((r) => r.name)).toEqual(["c", "a", "b"]);
expect(barPercent(50, 50)).toBe(100);
expect(barPercent(1, 1000)).toBe(1); // tiny but not invisible
expect(barPercent(0, 1000)).toBe(0);
expect(barPercent(5, 0)).toBe(0);
});
it("is bounded for the whole measurement: an entry cap stops it, the rest is not measured", async () => {
const { deps } = tree(TREE);
const s = await measureUsage("/data", { cancelled: () => false, onUpdate: () => undefined, maxEntries: 8 }, deps);
expect(s.end).toBe("capped");
expect(s.entries).toBeLessThanOrEqual(8);
expect(s.rows.find((r) => r.name === "games")?.state).toBe("partial");
expect(s.rows.find((r) => r.name === "music")?.state).toBe("skipped");
});
it("and by time", async () => {
const clock = { t: 0, per: 1000 };
const { deps } = tree(TREE, { clock });
const s = await measureUsage("/data", { cancelled: () => false, onUpdate: () => undefined, maxMs: 4500 }, deps);
expect(s.end).toBe("timeout");
expect(s.rows.some((r) => r.state === "skipped")).toBe(true);
});
it("subfolders past the folder cap are not listed, and the end says capped", async () => {
const big: Record<string, Entry[]> = { "/x": [d("1"), d("2"), d("3")], "/x/1": [], "/x/2": [], "/x/3": [] };
const { deps } = tree(big);
const s = await measureUsage("/x", { cancelled: () => false, onUpdate: () => undefined, maxFolders: 2 }, deps);
expect(s.folders).toBe(2);
expect(s.end).toBe("capped");
});
it("Cancel stops before the next page", async () => {
let n = 0;
const { deps, calls } = tree(TREE);
const s = await measureUsage("/data", { cancelled: () => ++n > 4, onUpdate: () => undefined }, deps);
expect(s.end).toBe("cancelled");
const after = calls.length;
expect(after).toBeLessThan(8);
});
it("an unreadable subfolder is marked and skipped; the folder itself failing is an error", async () => {
const { deps } = tree(TREE, { fail: { "/data/music": new ApiError(403, "path_not_allowed") } });
const s = await measureUsage("/data", { cancelled: () => false, onUpdate: () => undefined }, deps);
expect(s.end).toBe("done");
expect(s.rows.find((r) => r.name === "music")?.state).toBe("unreadable");
const bad = tree(TREE, { fail: { "/data": new ApiError(404, "not_found") } });
const e = await measureUsage("/data", { cancelled: () => false, onUpdate: () => undefined }, bad.deps);
expect(e.end).toBe("error");
expect(e.error).toBeInstanceOf(ApiError);
});
it("a 401 inside stops it (the page must reload)", async () => {
const { deps } = tree(TREE, { fail: { "/data/games": new ApiError(401, "token") } });
const s = await measureUsage("/data", { cancelled: () => false, onUpdate: () => undefined }, deps);
expect(s.end).toBe("error");
});
it("one at a time: a second start, or a folder-size walk, is refused; stopUsage waits for the end", async () => {
let release: () => void = () => undefined;
const gate = new Promise<void>((r) => {
release = r;
});
const slow: WalkDeps = {
list: async () => {
await gate;
return { entries: [], more: false, next: 0 };
},
now: () => 0,
};
const run = startUsage("/data", () => undefined, slow);
expect(run).not.toBeNull();
expect(usageRunning()).toBe(true);
expect(startUsage("/mnt/usb0", () => undefined, slow)).toBeNull();
const stopping = stopUsage();
release();
await stopping;
expect(usageRunning()).toBe(false);
expect((await run?.done)?.end).toBe("cancelled");
// a "Calculate folder size" walk running blocks a measurement too
let release2: () => void = () => undefined;
const gate2 = new Promise<void>((r) => {
release2 = r;
});
const walk = startFolderSize("/data", () => undefined, {
list: async () => {
await gate2;
return { entries: [], more: false };
},
now: () => 0,
});
expect(startUsage("/data", () => undefined, slow)).toBeNull();
walk?.cancel();
release2();
await walk?.done;
const again = startUsage("/data", () => undefined, slow);
expect(again).not.toBeNull();
await again?.done;
});
});
+217
View File
@@ -0,0 +1,217 @@
/**
* Disk usage: how much each subfolder of a folder holds, walked with
* /api/fs/list paging (folderSize.ts's walker, one subfolder after another).
* Bounded for the whole measurement (USAGE_MAX_ENTRIES entries, USAGE_MAX_MS),
* cancellable between pages, and one at a time page-wide: it refuses to start
* while another measurement or a "Calculate folder size" runs.
*/
import { ApiError, call } from "../../api";
import { folderSizeRunning, walkFolder, type ListPage, type WalkDeps } from "../folderSize";
import { joinPath } from "../move";
import { LIST_PAGE } from "../useDir";
export const USAGE_MAX_ENTRIES = 500_000;
export const USAGE_MAX_MS = 180_000;
/** Subfolders measured at most (the rest are listed as not measured). */
export const USAGE_MAX_FOLDERS = 2000;
export type RowState = "waiting" | "walking" | "done" | "partial" | "unreadable" | "skipped";
export interface UsageRow {
/** A subfolder's name; "" for the files directly in the folder. */
name: string;
path: string;
files: boolean;
bytes: number;
/** Entries counted inside it. */
entries: number;
state: RowState;
}
/** How a measurement ended (null while it runs). */
export type UsageEnd = "done" | "capped" | "timeout" | "cancelled" | "error" | null;
export interface UsageSnapshot {
root: string;
rows: UsageRow[];
/** Subfolders measured so far, and how many there are. */
measured: number;
folders: number;
entries: number;
end: UsageEnd;
error?: unknown;
}
export interface UsageLimits {
maxEntries?: number;
maxMs?: number;
maxFolders?: number;
}
const defaultDeps: WalkDeps = {
list: (path, offset, limit) => call<ListPage>("/api/fs/list", { path, offset, limit }),
now: () => Date.now(),
};
/** Biggest first; rows still waiting (or skipped) after the measured ones, by name. */
export function sortRows(rows: UsageRow[]): UsageRow[] {
const rank = (r: UsageRow) => (r.state === "waiting" || r.state === "skipped" ? 1 : 0);
return rows.slice().sort((a, b) => rank(a) - rank(b) || b.bytes - a.bytes || a.name.localeCompare(b.name));
}
/** A row's bar, 0-100, against the biggest row. */
export function barPercent(bytes: number, max: number): number {
if (!(max > 0) || !(bytes > 0)) return 0;
return Math.max(1, Math.round((bytes / max) * 100));
}
/** Measure `root` (see the module comment). Never throws: a failure of `root` itself is end "error". */
export async function measureUsage(
root: string,
opts: { cancelled: () => boolean; onUpdate: (s: UsageSnapshot) => void } & UsageLimits,
deps: WalkDeps = defaultDeps,
): Promise<UsageSnapshot> {
const maxEntries = opts.maxEntries ?? USAGE_MAX_ENTRIES;
const maxMs = opts.maxMs ?? USAGE_MAX_MS;
const maxFolders = opts.maxFolders ?? USAGE_MAX_FOLDERS;
const started = deps.now();
const here: UsageRow = { name: "", path: root, files: true, bytes: 0, entries: 0, state: "walking" };
const subs: UsageRow[] = [];
let entries = 0;
const snap = (end: UsageEnd, error?: unknown): UsageSnapshot => {
const s: UsageSnapshot = {
root,
rows: [here, ...subs].map((r) => ({ ...r })),
measured: subs.filter((r) => r.state !== "waiting" && r.state !== "walking").length,
folders: subs.length,
entries,
end,
};
if (error !== undefined) s.error = error;
return s;
};
const finish = (end: Exclude<UsageEnd, null>, error?: unknown) => {
for (const r of subs) if (r.state === "waiting" || r.state === "walking") r.state = "skipped";
if (here.state === "walking") here.state = end === "done" ? "done" : "partial";
const s = snap(end, error);
opts.onUpdate(s);
return s;
};
// 1. The folder itself: its files add up here, its subfolders become rows.
let offset = 0;
let extra = 0;
for (;;) {
if (opts.cancelled()) return finish("cancelled");
if (deps.now() - started > maxMs) return finish("timeout");
let page: ListPage;
try {
page = await deps.list(root, offset, LIST_PAGE);
} catch (e) {
return finish("error", e);
}
if (opts.cancelled()) return finish("cancelled");
const list = Array.isArray(page?.entries) ? page.entries : [];
for (const e of list) {
entries++;
if (e.kind === "dir") {
if (subs.length < maxFolders) {
subs.push({ name: e.name, path: joinPath(root, e.name), files: false, bytes: 0, entries: 0, state: "waiting" });
} else extra++;
} else if (e.kind === "file" && typeof e.size === "number" && e.size > 0) {
here.bytes += e.size;
here.entries++;
} else here.entries++;
if (entries >= maxEntries) return finish("capped");
}
const next = typeof page?.next === "number" ? page.next : offset + list.length;
if (!page?.more || next <= offset) break;
offset = next;
}
here.state = extra ? "partial" : "done";
opts.onUpdate(snap(null));
// 2. Each subfolder, one after another, out of what is left of the budget.
for (const row of subs) {
if (opts.cancelled()) return finish("cancelled");
const left = maxEntries - entries;
const msLeft = maxMs - (deps.now() - started);
if (left <= 0) return finish("capped");
if (msLeft <= 0) return finish("timeout");
row.state = "walking";
opts.onUpdate(snap(null));
const r = await walkFolder(
row.path,
{
cancelled: opts.cancelled,
maxEntries: left,
maxMs: msLeft,
onProgress: (c) => {
row.bytes = c.bytes;
row.entries = c.entries;
opts.onUpdate(snap(null));
},
},
deps,
);
row.bytes = r.bytes;
row.entries = r.entries;
entries += r.entries;
if (r.end === "error") {
// The token is gone: stop; another failure of this one folder: it is unreadable, go on.
if (r.error instanceof ApiError && r.error.status === 401) return finish("error", r.error);
row.state = "unreadable";
continue;
}
if (r.end === "cancelled") {
row.state = "partial";
return finish("cancelled");
}
if (r.end === "capped" || r.end === "timeout") {
row.state = "partial";
return finish(r.end);
}
row.state = r.unreadable ? "partial" : "done";
opts.onUpdate(snap(null));
}
return finish(extra ? "capped" : "done");
}
/* ---- one at a time ---- */
let running: { cancel: () => void; done: Promise<UsageSnapshot> } | null = null;
/** A disk usage measurement runs now. */
export function usageRunning(): boolean {
return running != null;
}
/**
* Start the page's one measurement of `root`: null while another (or a folder
* size walk) runs. `cancel()` stops it before its next page.
*/
export function startUsage(
root: string,
onUpdate: (s: UsageSnapshot) => void,
deps?: WalkDeps,
limits?: UsageLimits,
): { done: Promise<UsageSnapshot>; cancel: () => void } | null {
if (running || folderSizeRunning()) return null;
let cancelled = false;
const cancel = () => {
cancelled = true;
};
const done = measureUsage(root, { cancelled: () => cancelled, onUpdate, ...limits }, deps).finally(() => {
if (running?.done === done) running = null;
});
running = { cancel, done };
return { done, cancel };
}
/** Stop the running measurement (if any) and wait until it has ended. */
export async function stopUsage(): Promise<void> {
const r = running;
if (!r) return;
r.cancel();
await r.done.catch(() => undefined);
}
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "تعذّر حذف العنصر المراد استبداله، لذلك لم يُلصق.",
"cw_files_retry": "إعادة المحاولة",
"cw_files_rows_more": "عرض المزيد من الصفوف (تبقى {n})",
"cw_files_search": "بحث",
"cw_files_search_all": "كل الأقراص",
"cw_files_search_bad": "لا يمكن البحث عن علامات الاقتباس والشرطات المائلة العكسية.",
"cw_files_search_building": "جارٍ إنشاء الفهرس… {n} ملف حتى الآن.",
"cw_files_search_cancel": "إلغاء",
"cw_files_search_capped": "امتلأ (200,000 ملف)، لذا تنقصه بعض الملفات.",
"cw_files_search_field": "الاسم أو النمط",
"cw_files_search_found": "عُثر على {n}.",
"cw_files_search_go": "بحث",
"cw_files_search_here": "هذا المجلد",
"cw_files_search_hint": "يبحث عن الملفات بالاسم. تطابق الكلمة أي جزء من الاسم؛ استخدم * و ? للنمط (*.pkg).",
"cw_files_search_more": "تُعرض أول {n} نتيجة. هناك المزيد: جرّب اسمًا أطول.",
"cw_files_search_no_index": "لا يوجد فهرس بعد. أول بحث ينشئ فهرسًا: يقرأ كل مجلد وقد يستغرق بضع دقائق.",
"cw_files_search_none": "لا شيء يطابق «{q}».",
"cw_files_search_partial": "أُوقف مبكرًا، لذا تنقصه بعض الملفات.",
"cw_files_search_ready": "الفهرس: {n} ملف.",
"cw_files_search_rebuild": "إنشاء من جديد",
"cw_files_search_results": "نتائج البحث",
"cw_files_search_scope": "مكان البحث",
"cw_files_search_searching": "جارٍ البحث…",
"cw_files_search_stopping": "جارٍ إيقاف الفهرس…",
"cw_files_search_unsupported": "إصدار PS5Upload على هذا الجهاز قديم جدًا للبحث. حدّثه لاستخدام البحث.",
"cw_files_search_waiting": "سيبحث عن «{q}» عندما يجهز الفهرس.",
"cw_files_select_all": "تحديد الكل",
"cw_files_select_invert": "عكس التحديد",
"cw_files_select_none": "إلغاء التحديد",
@@ -251,6 +274,20 @@
"cw_files_unpin": "إلغاء التثبيت",
"cw_files_unpin_done": "تم إلغاء تثبيت {name}.",
"cw_files_up": "لأعلى",
"cw_files_usage": "استخدام القرص",
"cw_files_usage_again": "القياس مجددًا",
"cw_files_usage_busy": "يجري قياس حجم مجلد آخر. أعد المحاولة عند انتهائه.",
"cw_files_usage_cancelled": "أُلغي: حُسب {size}، ولم يُقس الباقي.",
"cw_files_usage_capped": "توقف عند {items} عنصر: حُسب {size}، والمجلد يحوي المزيد.",
"cw_files_usage_done": "{size} في {items} عنصر.",
"cw_files_usage_files_here": "الملفات في هذا المجلد",
"cw_files_usage_inside": "عرض محتوى {name}",
"cw_files_usage_open": "فتح في الملفات",
"cw_files_usage_open_row": "فتح {name} في الملفات",
"cw_files_usage_running": "جارٍ القياس… {at} من {n} مجلد، {items} عنصر.",
"cw_files_usage_skipped": "لم يُقس",
"cw_files_usage_timeout": "توقف بعد 3 دقائق: حُسب {size}، ولم يُقس الباقي.",
"cw_files_usage_unreadable": "غير قابل للقراءة",
"cw_files_verify": "تحقق (مجموع اختباري)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "أُلغي.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "প্রতিস্থাপনের আইটেমটি মোছা যায়নি, তাই পেস্ট করা হয়নি।",
"cw_files_retry": "আবার চেষ্টা করুন",
"cw_files_rows_more": "আরও সারি দেখান ({n}টি বাকি)",
"cw_files_search": "খুঁজুন",
"cw_files_search_all": "সব ড্রাইভ",
"cw_files_search_bad": "উদ্ধৃতিচিহ্ন ও ব্যাকস্ল্যাশ খোঁজা যায় না।",
"cw_files_search_building": "ইনডেক্স তৈরি হচ্ছে… এখন পর্যন্ত {n}টি ফাইল।",
"cw_files_search_cancel": "বাতিল",
"cw_files_search_capped": "এটি পূর্ণ (200,000টি ফাইল), তাই কিছু ফাইল নেই।",
"cw_files_search_field": "নাম বা প্যাটার্ন",
"cw_files_search_found": "{n}টি পাওয়া গেছে।",
"cw_files_search_go": "খুঁজুন",
"cw_files_search_here": "এই ফোল্ডার",
"cw_files_search_hint": "নাম ধরে ফাইল খোঁজে। একটি শব্দ নামের যেকোনো জায়গায় মিলে যায়; প্যাটার্নের জন্য * ও ? ব্যবহার করুন (*.pkg)।",
"cw_files_search_more": "প্রথম {n}টি দেখানো হচ্ছে। আরও আছে: লম্বা নাম দিয়ে চেষ্টা করুন।",
"cw_files_search_no_index": "এখনও কোনো ইনডেক্স নেই। প্রথম খোঁজ একটি তৈরি করে: এটি প্রতিটি ফোল্ডার পড়ে এবং কয়েক মিনিট লাগতে পারে।",
"cw_files_search_none": "“{q}”-এর সাথে কিছু মেলে না।",
"cw_files_search_partial": "এটি আগেই থামানো হয়েছিল, তাই কিছু ফাইল নেই।",
"cw_files_search_ready": "ইনডেক্স: {n}টি ফাইল।",
"cw_files_search_rebuild": "আবার তৈরি করুন",
"cw_files_search_results": "খোঁজের ফলাফল",
"cw_files_search_scope": "কোথায় খুঁজবেন",
"cw_files_search_searching": "খোঁজা হচ্ছে…",
"cw_files_search_stopping": "ইনডেক্স থামানো হচ্ছে…",
"cw_files_search_unsupported": "এই কনসোলের PS5Upload খোঁজার জন্য খুব পুরনো। খোঁজ ব্যবহার করতে এটি আপডেট করুন।",
"cw_files_search_waiting": "ইনডেক্স তৈরি হলে “{q}” খুঁজবে।",
"cw_files_select_all": "সব বাছুন",
"cw_files_select_invert": "উল্টে দিন",
"cw_files_select_none": "বাছাই সরান",
@@ -251,6 +274,20 @@
"cw_files_unpin": "পিন সরান",
"cw_files_unpin_done": "{name}-এর পিন সরানো হয়েছে।",
"cw_files_up": "উপরে",
"cw_files_usage": "ডিস্ক ব্যবহার",
"cw_files_usage_again": "আবার মাপুন",
"cw_files_usage_busy": "অন্য একটি ফোল্ডারের আকার মাপা হচ্ছে। সেটি শেষ হলে আবার চেষ্টা করুন।",
"cw_files_usage_cancelled": "বাতিল হয়েছে: {size} গোনা হয়েছে, বাকিটা মাপা হয়নি।",
"cw_files_usage_capped": "{items}টি আইটেমে থেমেছে: {size} গোনা হয়েছে, ফোল্ডারে আরও আছে।",
"cw_files_usage_done": "{items}টি আইটেমে {size}।",
"cw_files_usage_files_here": "এই ফোল্ডারের ফাইল",
"cw_files_usage_inside": "{name}-এর ভেতরে দেখুন",
"cw_files_usage_open": "ফাইল-এ খুলুন",
"cw_files_usage_open_row": "{name} ফাইল-এ খুলুন",
"cw_files_usage_running": "মাপা হচ্ছে… {n}টি ফোল্ডারের মধ্যে {at}, {items}টি আইটেম।",
"cw_files_usage_skipped": "মাপা হয়নি",
"cw_files_usage_timeout": "৩ মিনিট পর থেমেছে: {size} গোনা হয়েছে, বাকিটা মাপা হয়নি।",
"cw_files_usage_unreadable": "পড়া যায় না",
"cw_files_verify": "যাচাই (চেকসাম)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "বাতিল করা হয়েছে।",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Das zu ersetzende Element ließ sich nicht löschen, daher wurde nichts eingefügt.",
"cw_files_retry": "Erneut versuchen",
"cw_files_rows_more": "Mehr Zeilen anzeigen ({n} übrig)",
"cw_files_search": "Suchen",
"cw_files_search_all": "Alle Laufwerke",
"cw_files_search_bad": "Nach Anführungszeichen und Backslashes kann nicht gesucht werden.",
"cw_files_search_building": "Index wird erstellt… bisher {n} Dateien.",
"cw_files_search_cancel": "Abbrechen",
"cw_files_search_capped": "Er ist voll (200.000 Dateien), daher fehlen einige Dateien.",
"cw_files_search_field": "Name oder Muster",
"cw_files_search_found": "{n} gefunden.",
"cw_files_search_go": "Suchen",
"cw_files_search_here": "Dieser Ordner",
"cw_files_search_hint": "Findet Dateien nach Namen. Ein Wort passt überall im Namen; mit * und ? als Muster (*.pkg).",
"cw_files_search_more": "Die ersten {n} werden angezeigt. Es gibt mehr: Versuche einen längeren Namen.",
"cw_files_search_no_index": "Noch kein Index. Die erste Suche erstellt einen: Sie liest jeden Ordner und kann einige Minuten dauern.",
"cw_files_search_none": "Nichts passt zu „{q}“.",
"cw_files_search_partial": "Er wurde vorzeitig gestoppt, daher fehlen einige Dateien.",
"cw_files_search_ready": "Index: {n} Dateien.",
"cw_files_search_rebuild": "Neu erstellen",
"cw_files_search_results": "Suchergebnisse",
"cw_files_search_scope": "Wo suchen",
"cw_files_search_searching": "Suche…",
"cw_files_search_stopping": "Index wird gestoppt…",
"cw_files_search_unsupported": "PS5Upload auf dieser Konsole ist zu alt für die Suche. Aktualisiere es, um zu suchen.",
"cw_files_search_waiting": "Sucht nach „{q}“, sobald der Index fertig ist.",
"cw_files_select_all": "Alle auswählen",
"cw_files_select_invert": "Umkehren",
"cw_files_select_none": "Auswahl aufheben",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Lösen",
"cw_files_unpin_done": "{name} gelöst.",
"cw_files_up": "Nach oben",
"cw_files_usage": "Speicherbelegung",
"cw_files_usage_again": "Erneut messen",
"cw_files_usage_busy": "Gerade wird eine andere Ordnergröße gemessen. Versuche es danach erneut.",
"cw_files_usage_cancelled": "Abgebrochen: {size} gezählt, der Rest wurde nicht gemessen.",
"cw_files_usage_capped": "Bei {items} Elementen gestoppt: {size} gezählt, der Ordner enthält mehr.",
"cw_files_usage_done": "{size} in {items} Elementen.",
"cw_files_usage_files_here": "Dateien in diesem Ordner",
"cw_files_usage_inside": "In {name} hineinsehen",
"cw_files_usage_open": "In Dateien öffnen",
"cw_files_usage_open_row": "{name} in Dateien öffnen",
"cw_files_usage_running": "Messe… {at} von {n} Ordnern, {items} Elemente.",
"cw_files_usage_skipped": "nicht gemessen",
"cw_files_usage_timeout": "Nach 3 Minuten gestoppt: {size} gezählt, der Rest wurde nicht gemessen.",
"cw_files_usage_unreadable": "nicht lesbar",
"cw_files_verify": "Prüfen (Prüfsumme)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Abgebrochen.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Couldn't delete the item being replaced, so it was not pasted.",
"cw_files_retry": "Try again",
"cw_files_rows_more": "Show more rows ({n} left)",
"cw_files_search": "Search",
"cw_files_search_all": "All drives",
"cw_files_search_bad": "Quotes and backslashes cannot be searched for.",
"cw_files_search_building": "Building the index… {n} files so far.",
"cw_files_search_cancel": "Cancel",
"cw_files_search_capped": "It is full (200,000 files), so some files are missing.",
"cw_files_search_field": "Name or pattern",
"cw_files_search_found": "{n} found.",
"cw_files_search_go": "Search",
"cw_files_search_here": "This folder",
"cw_files_search_hint": "Finds files by name. A word matches anywhere in the name; use * and ? for a pattern (*.pkg).",
"cw_files_search_more": "Showing the first {n}. There are more: try a longer name.",
"cw_files_search_no_index": "No index yet. The first search builds one: it reads every folder and can take a few minutes.",
"cw_files_search_none": "Nothing matches “{q}”.",
"cw_files_search_partial": "It was stopped early, so some files are missing.",
"cw_files_search_ready": "Index: {n} files.",
"cw_files_search_rebuild": "Build again",
"cw_files_search_results": "Search results",
"cw_files_search_scope": "Where to search",
"cw_files_search_searching": "Searching…",
"cw_files_search_stopping": "Stopping the index…",
"cw_files_search_unsupported": "PS5Upload on this console is too old to search. Update it to use search.",
"cw_files_search_waiting": "Searches for “{q}” when the index is ready.",
"cw_files_select_all": "Select all",
"cw_files_select_invert": "Invert",
"cw_files_select_none": "Select none",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Unpin",
"cw_files_unpin_done": "Unpinned {name}.",
"cw_files_up": "Up",
"cw_files_usage": "Disk usage",
"cw_files_usage_again": "Measure again",
"cw_files_usage_busy": "Another folder size is being measured. Try again when it ends.",
"cw_files_usage_cancelled": "Cancelled: {size} counted, the rest was not measured.",
"cw_files_usage_capped": "Stopped at {items} items: {size} counted, the folder holds more.",
"cw_files_usage_done": "{size} in {items} items.",
"cw_files_usage_files_here": "Files in this folder",
"cw_files_usage_inside": "Look inside {name}",
"cw_files_usage_open": "Open in Files",
"cw_files_usage_open_row": "Open {name} in Files",
"cw_files_usage_running": "Measuring… {at} of {n} folders, {items} items.",
"cw_files_usage_skipped": "not measured",
"cw_files_usage_timeout": "Stopped after 3 minutes: {size} counted, the rest was not measured.",
"cw_files_usage_unreadable": "unreadable",
"cw_files_verify": "Verify (checksum)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Cancelled.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "No se pudo eliminar el elemento a reemplazar, así que no se pegó.",
"cw_files_retry": "Reintentar",
"cw_files_rows_more": "Mostrar más filas (quedan {n})",
"cw_files_search": "Buscar",
"cw_files_search_all": "Todas las unidades",
"cw_files_search_bad": "No se pueden buscar comillas ni barras invertidas.",
"cw_files_search_building": "Creando el índice… {n} archivos hasta ahora.",
"cw_files_search_cancel": "Cancelar",
"cw_files_search_capped": "Está lleno (200.000 archivos), así que faltan algunos archivos.",
"cw_files_search_field": "Nombre o patrón",
"cw_files_search_found": "{n} encontrados.",
"cw_files_search_go": "Buscar",
"cw_files_search_here": "Esta carpeta",
"cw_files_search_hint": "Busca archivos por nombre. Una palabra coincide en cualquier parte del nombre; usa * y ? para un patrón (*.pkg).",
"cw_files_search_more": "Se muestran los primeros {n}. Hay más: prueba con un nombre más largo.",
"cw_files_search_no_index": "Aún no hay índice. La primera búsqueda crea uno: lee todas las carpetas y puede tardar unos minutos.",
"cw_files_search_none": "Nada coincide con «{q}».",
"cw_files_search_partial": "Se detuvo antes de tiempo, así que faltan algunos archivos.",
"cw_files_search_ready": "Índice: {n} archivos.",
"cw_files_search_rebuild": "Crear de nuevo",
"cw_files_search_results": "Resultados de la búsqueda",
"cw_files_search_scope": "Dónde buscar",
"cw_files_search_searching": "Buscando…",
"cw_files_search_stopping": "Deteniendo el índice…",
"cw_files_search_unsupported": "PS5Upload en esta consola es demasiado antiguo para buscar. Actualízalo para usar la búsqueda.",
"cw_files_search_waiting": "Buscará «{q}» cuando el índice esté listo.",
"cw_files_select_all": "Seleccionar todo",
"cw_files_select_invert": "Invertir",
"cw_files_select_none": "Quitar selección",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Quitar fijación",
"cw_files_unpin_done": "{name} ya no está fijada.",
"cw_files_up": "Subir",
"cw_files_usage": "Uso del disco",
"cw_files_usage_again": "Medir de nuevo",
"cw_files_usage_busy": "Se está midiendo el tamaño de otra carpeta. Vuelve a intentarlo cuando termine.",
"cw_files_usage_cancelled": "Cancelado: {size} contados, el resto no se midió.",
"cw_files_usage_capped": "Detenido en {items} elementos: {size} contados, la carpeta contiene más.",
"cw_files_usage_done": "{size} en {items} elementos.",
"cw_files_usage_files_here": "Archivos de esta carpeta",
"cw_files_usage_inside": "Ver dentro de {name}",
"cw_files_usage_open": "Abrir en Archivos",
"cw_files_usage_open_row": "Abrir {name} en Archivos",
"cw_files_usage_running": "Midiendo… {at} de {n} carpetas, {items} elementos.",
"cw_files_usage_skipped": "sin medir",
"cw_files_usage_timeout": "Detenido tras 3 minutos: {size} contados, el resto no se midió.",
"cw_files_usage_unreadable": "ilegible",
"cw_files_verify": "Verificar (suma de comprobación)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Cancelado.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "حذف مورد جایگزین‌شونده ممکن نشد، پس چسبانده نشد.",
"cw_files_retry": "تلاش دوباره",
"cw_files_rows_more": "نمایش ردیف‌های بیشتر ({n} مانده)",
"cw_files_search": "جستجو",
"cw_files_search_all": "همهٔ درایوها",
"cw_files_search_bad": "نمی‌توان نقل‌قول و بک‌اسلش را جستجو کرد.",
"cw_files_search_building": "در حال ساخت نمایه… تاکنون {n} فایل.",
"cw_files_search_cancel": "لغو",
"cw_files_search_capped": "پر شده است (۲۰۰٬۰۰۰ فایل)، پس برخی فایل‌ها در آن نیستند.",
"cw_files_search_field": "نام یا الگو",
"cw_files_search_found": "{n} مورد پیدا شد.",
"cw_files_search_go": "جستجو",
"cw_files_search_here": "همین پوشه",
"cw_files_search_hint": "فایل‌ها را بر اساس نام پیدا می‌کند. یک واژه با هر بخشی از نام جور می‌شود؛ برای الگو از * و ? استفاده کنید (*.pkg).",
"cw_files_search_more": "{n} مورد نخست نمایش داده می‌شود. موارد بیشتری هست: نام بلندتری امتحان کنید.",
"cw_files_search_no_index": "هنوز نمایه‌ای نیست. نخستین جستجو آن را می‌سازد: همهٔ پوشه‌ها را می‌خواند و ممکن است چند دقیقه طول بکشد.",
"cw_files_search_none": "چیزی با «{q}» جور نیست.",
"cw_files_search_partial": "زودتر متوقف شد، پس برخی فایل‌ها در آن نیستند.",
"cw_files_search_ready": "نمایه: {n} فایل.",
"cw_files_search_rebuild": "ساخت دوباره",
"cw_files_search_results": "نتایج جستجو",
"cw_files_search_scope": "محل جستجو",
"cw_files_search_searching": "در حال جستجو…",
"cw_files_search_stopping": "در حال توقف نمایه…",
"cw_files_search_unsupported": "PS5Upload روی این کنسول برای جستجو قدیمی است. برای جستجو آن را به‌روز کنید.",
"cw_files_search_waiting": "وقتی نمایه آماده شد «{q}» را جستجو می‌کند.",
"cw_files_select_all": "انتخاب همه",
"cw_files_select_invert": "معکوس کردن",
"cw_files_select_none": "لغو انتخاب",
@@ -251,6 +274,20 @@
"cw_files_unpin": "برداشتن سنجاق",
"cw_files_unpin_done": "سنجاق {name} برداشته شد.",
"cw_files_up": "بالا",
"cw_files_usage": "فضای اشغال‌شده",
"cw_files_usage_again": "اندازه‌گیری دوباره",
"cw_files_usage_busy": "اندازهٔ پوشهٔ دیگری در حال اندازه‌گیری است. پس از پایان آن دوباره امتحان کنید.",
"cw_files_usage_cancelled": "لغو شد: {size} شمرده شد، بقیه اندازه‌گیری نشد.",
"cw_files_usage_capped": "در {items} مورد متوقف شد: {size} شمرده شد، پوشه بیشتر دارد.",
"cw_files_usage_done": "{size} در {items} مورد.",
"cw_files_usage_files_here": "فایل‌های این پوشه",
"cw_files_usage_inside": "دیدن درون {name}",
"cw_files_usage_open": "باز کردن در فایل‌ها",
"cw_files_usage_open_row": "باز کردن {name} در فایل‌ها",
"cw_files_usage_running": "در حال اندازه‌گیری… {at} از {n} پوشه، {items} مورد.",
"cw_files_usage_skipped": "اندازه‌گیری نشده",
"cw_files_usage_timeout": "پس از ۳ دقیقه متوقف شد: {size} شمرده شد، بقیه اندازه‌گیری نشد.",
"cw_files_usage_unreadable": "خواندنی نیست",
"cw_files_verify": "بررسی (چک‌سام)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "لغو شد.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Impossible de supprimer l’élément à remplacer : rien n’a été collé.",
"cw_files_retry": "Réessayer",
"cw_files_rows_more": "Afficher plus de lignes ({n} restantes)",
"cw_files_search": "Rechercher",
"cw_files_search_all": "Tous les disques",
"cw_files_search_bad": "Impossible de rechercher des guillemets ou des barres obliques inverses.",
"cw_files_search_building": "Création de l’index… {n} fichiers jusqu’ici.",
"cw_files_search_cancel": "Annuler",
"cw_files_search_capped": "Il est plein (200 000 fichiers) ; il manque donc des fichiers.",
"cw_files_search_field": "Nom ou motif",
"cw_files_search_found": "{n} trouvés.",
"cw_files_search_go": "Rechercher",
"cw_files_search_here": "Ce dossier",
"cw_files_search_hint": "Trouve des fichiers par leur nom. Un mot correspond n’importe où dans le nom ; utilisez * et ? pour un motif (*.pkg).",
"cw_files_search_more": "Les {n} premiers sont affichés. Il y en a d’autres : essayez un nom plus long.",
"cw_files_search_no_index": "Pas encore d’index. La première recherche en crée un : elle lit chaque dossier et peut prendre quelques minutes.",
"cw_files_search_none": "Rien ne correspond à « {q} ».",
"cw_files_search_partial": "Il a été arrêté avant la fin ; il manque donc des fichiers.",
"cw_files_search_ready": "Index : {n} fichiers.",
"cw_files_search_rebuild": "Recréer",
"cw_files_search_results": "Résultats de la recherche",
"cw_files_search_scope": "Où chercher",
"cw_files_search_searching": "Recherche…",
"cw_files_search_stopping": "Arrêt de l’index…",
"cw_files_search_unsupported": "PS5Upload sur cette console est trop ancien pour rechercher. Mettez-le à jour pour utiliser la recherche.",
"cw_files_search_waiting": "Recherchera « {q} » quand l’index sera prêt.",
"cw_files_select_all": "Tout sélectionner",
"cw_files_select_invert": "Inverser",
"cw_files_select_none": "Tout désélectionner",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Désépingler",
"cw_files_unpin_done": "{name} désépinglé.",
"cw_files_up": "Remonter",
"cw_files_usage": "Utilisation du disque",
"cw_files_usage_again": "Mesurer à nouveau",
"cw_files_usage_busy": "La taille d’un autre dossier est en cours de mesure. Réessayez quand elle sera terminée.",
"cw_files_usage_cancelled": "Annulé : {size} comptés, le reste n’a pas été mesuré.",
"cw_files_usage_capped": "Arrêté à {items} éléments : {size} comptés, le dossier en contient davantage.",
"cw_files_usage_done": "{size} en {items} éléments.",
"cw_files_usage_files_here": "Fichiers de ce dossier",
"cw_files_usage_inside": "Voir le contenu de {name}",
"cw_files_usage_open": "Ouvrir dans Fichiers",
"cw_files_usage_open_row": "Ouvrir {name} dans Fichiers",
"cw_files_usage_running": "Mesure… {at} sur {n} dossiers, {items} éléments.",
"cw_files_usage_skipped": "non mesuré",
"cw_files_usage_timeout": "Arrêté après 3 minutes : {size} comptés, le reste n’a pas été mesuré.",
"cw_files_usage_unreadable": "illisible",
"cw_files_verify": "Vérifier (somme de contrôle)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Annulé.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "बदला जाने वाला आइटम हटाया नहीं जा सका, इसलिए पेस्ट नहीं किया गया।",
"cw_files_retry": "फिर से कोशिश करें",
"cw_files_rows_more": "और पंक्तियाँ दिखाएँ ({n} बाकी)",
"cw_files_search": "खोजें",
"cw_files_search_all": "सभी ड्राइव",
"cw_files_search_bad": "उद्धरण चिह्न और बैकस्लैश नहीं खोजे जा सकते।",
"cw_files_search_building": "इंडेक्स बन रहा है… अब तक {n} फ़ाइलें।",
"cw_files_search_cancel": "रद्द करें",
"cw_files_search_capped": "यह भर गया है (200,000 फ़ाइलें), इसलिए कुछ फ़ाइलें नहीं हैं।",
"cw_files_search_field": "नाम या पैटर्न",
"cw_files_search_found": "{n} मिले।",
"cw_files_search_go": "खोजें",
"cw_files_search_here": "यह फ़ोल्डर",
"cw_files_search_hint": "नाम से फ़ाइलें ढूँढता है। कोई शब्द नाम में कहीं भी मेल खाता है; पैटर्न के लिए * और ? का उपयोग करें (*.pkg)।",
"cw_files_search_more": "पहले {n} दिखाए जा रहे हैं। और भी हैं: लंबा नाम आज़माएँ।",
"cw_files_search_no_index": "अभी कोई इंडेक्स नहीं है। पहली खोज इसे बनाती है: यह हर फ़ोल्डर पढ़ती है और कुछ मिनट लग सकते हैं।",
"cw_files_search_none": "“{q}” से कुछ मेल नहीं खाता।",
"cw_files_search_partial": "इसे पहले ही रोक दिया गया, इसलिए कुछ फ़ाइलें नहीं हैं।",
"cw_files_search_ready": "इंडेक्स: {n} फ़ाइलें।",
"cw_files_search_rebuild": "फिर से बनाएँ",
"cw_files_search_results": "खोज परिणाम",
"cw_files_search_scope": "कहाँ खोजें",
"cw_files_search_searching": "खोज रहा है…",
"cw_files_search_stopping": "इंडेक्स रोका जा रहा है…",
"cw_files_search_unsupported": "इस कंसोल पर PS5Upload खोज के लिए बहुत पुराना है। खोज इस्तेमाल करने के लिए इसे अपडेट करें।",
"cw_files_search_waiting": "इंडेक्स तैयार होने पर “{q}” खोजेगा।",
"cw_files_select_all": "सब चुनें",
"cw_files_select_invert": "चयन उलटें",
"cw_files_select_none": "चयन हटाएँ",
@@ -251,6 +274,20 @@
"cw_files_unpin": "पिन हटाएँ",
"cw_files_unpin_done": "{name} का पिन हटाया गया।",
"cw_files_up": "ऊपर",
"cw_files_usage": "डिस्क उपयोग",
"cw_files_usage_again": "फिर से मापें",
"cw_files_usage_busy": "किसी दूसरे फ़ोल्डर का आकार मापा जा रहा है। उसके खत्म होने पर फिर कोशिश करें।",
"cw_files_usage_cancelled": "रद्द किया गया: {size} गिना गया, बाकी नहीं मापा गया।",
"cw_files_usage_capped": "{items} आइटम पर रुका: {size} गिना गया, फ़ोल्डर में और है।",
"cw_files_usage_done": "{items} आइटम में {size}।",
"cw_files_usage_files_here": "इस फ़ोल्डर की फ़ाइलें",
"cw_files_usage_inside": "{name} के अंदर देखें",
"cw_files_usage_open": "फ़ाइलें सेक्शन में खोलें",
"cw_files_usage_open_row": "{name} को फ़ाइलें सेक्शन में खोलें",
"cw_files_usage_running": "माप रहा है… {n} में से {at} फ़ोल्डर, {items} आइटम।",
"cw_files_usage_skipped": "नहीं मापा गया",
"cw_files_usage_timeout": "3 मिनट बाद रुका: {size} गिना गया, बाकी नहीं मापा गया।",
"cw_files_usage_unreadable": "पढ़ा नहीं जा सकता",
"cw_files_verify": "जाँचें (चेकसम)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "रद्द किया।",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "A lecserélendő elemet nem sikerült törölni, ezért nem lett beillesztve.",
"cw_files_retry": "Újra",
"cw_files_rows_more": "További sorok ({n} maradt)",
"cw_files_search": "Keresés",
"cw_files_search_all": "Minden meghajtó",
"cw_files_search_bad": "Idézőjelre és fordított perjelre nem lehet keresni.",
"cw_files_search_building": "Index készítése… eddig {n} fájl.",
"cw_files_search_cancel": "Mégse",
"cw_files_search_capped": "Megtelt (200 000 fájl), ezért néhány fájl hiányzik.",
"cw_files_search_field": "Név vagy minta",
"cw_files_search_found": "{n} találat.",
"cw_files_search_go": "Keresés",
"cw_files_search_here": "Ez a mappa",
"cw_files_search_hint": "Fájlokat keres név alapján. Egy szó a név bármely részén egyezhet; mintához használd a * és ? jelet (*.pkg).",
"cw_files_search_more": "Az első {n} látszik. Van több is: próbálj hosszabb nevet.",
"cw_files_search_no_index": "Még nincs index. Az első keresés létrehozza: minden mappát beolvas, és néhány percig tarthat.",
"cw_files_search_none": "Semmi sem egyezik ezzel: „{q}”.",
"cw_files_search_partial": "Idő előtt leállt, ezért néhány fájl hiányzik.",
"cw_files_search_ready": "Index: {n} fájl.",
"cw_files_search_rebuild": "Újraépítés",
"cw_files_search_results": "Keresési eredmények",
"cw_files_search_scope": "Hol keressen",
"cw_files_search_searching": "Keresés…",
"cw_files_search_stopping": "Index leállítása…",
"cw_files_search_unsupported": "A konzolon lévő PS5Upload túl régi a kereséshez. Frissítsd, hogy kereshess.",
"cw_files_search_waiting": "Rákeres erre: „{q}”, amint kész az index.",
"cw_files_select_all": "Összes kijelölése",
"cw_files_select_invert": "Megfordítás",
"cw_files_select_none": "Kijelölés törlése",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Kitűzés megszüntetése",
"cw_files_unpin_done": "{name} kitűzése megszüntetve.",
"cw_files_up": "Fel",
"cw_files_usage": "Lemezhasználat",
"cw_files_usage_again": "Mérés újra",
"cw_files_usage_busy": "Most egy másik mappa mérete mérődik. Próbáld újra, ha végzett.",
"cw_files_usage_cancelled": "Megszakítva: {size} megszámolva, a többit nem mértük.",
"cw_files_usage_capped": "{items} elemnél leállt: {size} megszámolva, a mappában több van.",
"cw_files_usage_done": "{size}, {items} elemben.",
"cw_files_usage_files_here": "A mappa fájljai",
"cw_files_usage_inside": "Belenézés: {name}",
"cw_files_usage_open": "Megnyitás a Fájlokban",
"cw_files_usage_open_row": "{name} megnyitása a Fájlokban",
"cw_files_usage_running": "Mérés… {at}/{n} mappa, {items} elem.",
"cw_files_usage_skipped": "nincs mérve",
"cw_files_usage_timeout": "3 perc után leállt: {size} megszámolva, a többit nem mértük.",
"cw_files_usage_unreadable": "nem olvasható",
"cw_files_verify": "Ellenőrzés (ellenőrzőösszeg)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Megszakítva.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Item yang akan diganti tidak bisa dihapus, jadi tidak ditempel.",
"cw_files_retry": "Coba lagi",
"cw_files_rows_more": "Tampilkan baris lainnya (sisa {n})",
"cw_files_search": "Cari",
"cw_files_search_all": "Semua drive",
"cw_files_search_bad": "Tanda kutip dan garis miring terbalik tidak dapat dicari.",
"cw_files_search_building": "Membuat indeks… {n} file sejauh ini.",
"cw_files_search_cancel": "Batal",
"cw_files_search_capped": "Indeks penuh (200.000 file), jadi beberapa file tidak ada.",
"cw_files_search_field": "Nama atau pola",
"cw_files_search_found": "{n} ditemukan.",
"cw_files_search_go": "Cari",
"cw_files_search_here": "Folder ini",
"cw_files_search_hint": "Mencari file menurut nama. Sebuah kata cocok di bagian mana pun dari nama; gunakan * dan ? untuk pola (*.pkg).",
"cw_files_search_more": "Menampilkan {n} pertama. Masih ada lagi: coba nama yang lebih panjang.",
"cw_files_search_no_index": "Belum ada indeks. Pencarian pertama membuatnya: membaca setiap folder dan bisa memakan beberapa menit.",
"cw_files_search_none": "Tidak ada yang cocok dengan “{q}”.",
"cw_files_search_partial": "Indeks dihentikan lebih awal, jadi beberapa file tidak ada.",
"cw_files_search_ready": "Indeks: {n} file.",
"cw_files_search_rebuild": "Buat ulang",
"cw_files_search_results": "Hasil pencarian",
"cw_files_search_scope": "Tempat mencari",
"cw_files_search_searching": "Mencari…",
"cw_files_search_stopping": "Menghentikan indeks…",
"cw_files_search_unsupported": "PS5Upload di konsol ini terlalu lama untuk mencari. Perbarui untuk memakai pencarian.",
"cw_files_search_waiting": "Akan mencari “{q}” saat indeks siap.",
"cw_files_select_all": "Pilih semua",
"cw_files_select_invert": "Balikkan",
"cw_files_select_none": "Batalkan pilihan",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Lepas sematan",
"cw_files_unpin_done": "Sematan {name} dilepas.",
"cw_files_up": "Naik",
"cw_files_usage": "Penggunaan disk",
"cw_files_usage_again": "Ukur lagi",
"cw_files_usage_busy": "Ukuran folder lain sedang diukur. Coba lagi setelah selesai.",
"cw_files_usage_cancelled": "Dibatalkan: {size} terhitung, sisanya tidak diukur.",
"cw_files_usage_capped": "Berhenti di {items} item: {size} terhitung, folder berisi lebih banyak.",
"cw_files_usage_done": "{size} dalam {items} item.",
"cw_files_usage_files_here": "File di folder ini",
"cw_files_usage_inside": "Lihat isi {name}",
"cw_files_usage_open": "Buka di File",
"cw_files_usage_open_row": "Buka {name} di File",
"cw_files_usage_running": "Mengukur… {at} dari {n} folder, {items} item.",
"cw_files_usage_skipped": "tidak diukur",
"cw_files_usage_timeout": "Berhenti setelah 3 menit: {size} terhitung, sisanya tidak diukur.",
"cw_files_usage_unreadable": "tidak terbaca",
"cw_files_verify": "Verifikasi (checksum)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Dibatalkan.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Impossibile eliminare l’elemento da sostituire, quindi non è stato incollato.",
"cw_files_retry": "Riprova",
"cw_files_rows_more": "Mostra altre righe ({n} rimaste)",
"cw_files_search": "Cerca",
"cw_files_search_all": "Tutte le unità",
"cw_files_search_bad": "Non si possono cercare virgolette o barre rovesciate.",
"cw_files_search_building": "Creazione dell’indice… {n} file finora.",
"cw_files_search_cancel": "Annulla",
"cw_files_search_capped": "È pieno (200.000 file), quindi mancano alcuni file.",
"cw_files_search_field": "Nome o modello",
"cw_files_search_found": "{n} trovati.",
"cw_files_search_go": "Cerca",
"cw_files_search_here": "Questa cartella",
"cw_files_search_hint": "Trova i file per nome. Una parola corrisponde in qualsiasi punto del nome; usa * e ? per un modello (*.pkg).",
"cw_files_search_more": "Vengono mostrati i primi {n}. Ce ne sono altri: prova un nome più lungo.",
"cw_files_search_no_index": "Nessun indice ancora. La prima ricerca ne crea uno: legge ogni cartella e può richiedere alcuni minuti.",
"cw_files_search_none": "Nessun risultato per «{q}».",
"cw_files_search_partial": "È stato fermato prima della fine, quindi mancano alcuni file.",
"cw_files_search_ready": "Indice: {n} file.",
"cw_files_search_rebuild": "Ricrea",
"cw_files_search_results": "Risultati della ricerca",
"cw_files_search_scope": "Dove cercare",
"cw_files_search_searching": "Ricerca…",
"cw_files_search_stopping": "Arresto dell’indice…",
"cw_files_search_unsupported": "PS5Upload su questa console è troppo vecchio per cercare. Aggiornalo per usare la ricerca.",
"cw_files_search_waiting": "Cercherà «{q}» quando l’indice sarà pronto.",
"cw_files_select_all": "Seleziona tutto",
"cw_files_select_invert": "Inverti",
"cw_files_select_none": "Deseleziona tutto",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Rimuovi",
"cw_files_unpin_done": "{name} rimossa dalle fissate.",
"cw_files_up": "Su",
"cw_files_usage": "Utilizzo del disco",
"cw_files_usage_again": "Misura di nuovo",
"cw_files_usage_busy": "È in corso la misura di un’altra cartella. Riprova quando è finita.",
"cw_files_usage_cancelled": "Annullato: {size} contati, il resto non è stato misurato.",
"cw_files_usage_capped": "Fermato a {items} elementi: {size} contati, la cartella ne contiene di più.",
"cw_files_usage_done": "{size} in {items} elementi.",
"cw_files_usage_files_here": "File in questa cartella",
"cw_files_usage_inside": "Guarda dentro {name}",
"cw_files_usage_open": "Apri in File",
"cw_files_usage_open_row": "Apri {name} in File",
"cw_files_usage_running": "Misurazione… {at} di {n} cartelle, {items} elementi.",
"cw_files_usage_skipped": "non misurato",
"cw_files_usage_timeout": "Fermato dopo 3 minuti: {size} contati, il resto non è stato misurato.",
"cw_files_usage_unreadable": "illeggibile",
"cw_files_verify": "Verifica (checksum)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Annullato.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "置き換える項目を削除できなかったため、貼り付けませんでした。",
"cw_files_retry": "再試行",
"cw_files_rows_more": "行をさらに表示 (残り {n})",
"cw_files_search": "検索",
"cw_files_search_all": "すべてのドライブ",
"cw_files_search_bad": "引用符とバックスラッシュは検索できません。",
"cw_files_search_building": "インデックスを作成中… これまでに {n} ファイル。",
"cw_files_search_cancel": "キャンセル",
"cw_files_search_capped": "上限(200,000 ファイル)に達したため、一部のファイルがありません。",
"cw_files_search_field": "名前またはパターン",
"cw_files_search_found": "{n} 件見つかりました。",
"cw_files_search_go": "検索",
"cw_files_search_here": "このフォルダー",
"cw_files_search_hint": "名前でファイルを探します。単語は名前のどこにあっても一致します。パターンには * と ? を使います(*.pkg)。",
"cw_files_search_more": "最初の {n} 件を表示しています。さらにあります。もっと長い名前で試してください。",
"cw_files_search_no_index": "まだインデックスがありません。最初の検索で作成します。すべてのフォルダーを読むため数分かかることがあります。",
"cw_files_search_none": "「{q}」に一致するものはありません。",
"cw_files_search_partial": "途中で停止したため、一部のファイルがありません。",
"cw_files_search_ready": "インデックス: {n} ファイル。",
"cw_files_search_rebuild": "作り直す",
"cw_files_search_results": "検索結果",
"cw_files_search_scope": "検索する場所",
"cw_files_search_searching": "検索中…",
"cw_files_search_stopping": "インデックスを停止中…",
"cw_files_search_unsupported": "この本体の PS5Upload は古いため検索できません。検索を使うには更新してください。",
"cw_files_search_waiting": "インデックスの準備ができたら「{q}」を検索します。",
"cw_files_select_all": "すべて選択",
"cw_files_select_invert": "選択を反転",
"cw_files_select_none": "選択を解除",
@@ -251,6 +274,20 @@
"cw_files_unpin": "ピン留めを外す",
"cw_files_unpin_done": "{name} のピン留めを外しました。",
"cw_files_up": "上へ",
"cw_files_usage": "ディスク使用量",
"cw_files_usage_again": "もう一度計測",
"cw_files_usage_busy": "別のフォルダーのサイズを計測中です。終わってから再試行してください。",
"cw_files_usage_cancelled": "キャンセルしました: {size} を計上、残りは計測していません。",
"cw_files_usage_capped": "{items} 項目で停止: {size} を計上、フォルダーにはさらにあります。",
"cw_files_usage_done": "{items} 項目で {size}。",
"cw_files_usage_files_here": "このフォルダー内のファイル",
"cw_files_usage_inside": "{name} の中を見る",
"cw_files_usage_open": "ファイルで開く",
"cw_files_usage_open_row": "{name} をファイルで開く",
"cw_files_usage_running": "計測中… {n} フォルダー中 {at}、{items} 項目。",
"cw_files_usage_skipped": "未計測",
"cw_files_usage_timeout": "3 分で停止: {size} を計上、残りは計測していません。",
"cw_files_usage_unreadable": "読み取れません",
"cw_files_verify": "検証(チェックサム)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "キャンセルしました。",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "바꿀 항목을 삭제하지 못해 붙여넣지 않았습니다.",
"cw_files_retry": "다시 시도",
"cw_files_rows_more": "행 더 보기 ({n}개 남음)",
"cw_files_search": "검색",
"cw_files_search_all": "모든 드라이브",
"cw_files_search_bad": "따옴표와 백슬래시는 검색할 수 없습니다.",
"cw_files_search_building": "색인 생성 중… 지금까지 {n}개 파일.",
"cw_files_search_cancel": "취소",
"cw_files_search_capped": "가득 차서(200,000개 파일) 일부 파일이 빠져 있습니다.",
"cw_files_search_field": "이름 또는 패턴",
"cw_files_search_found": "{n}개 찾음.",
"cw_files_search_go": "검색",
"cw_files_search_here": "이 폴더",
"cw_files_search_hint": "이름으로 파일을 찾습니다. 단어는 이름의 어느 부분과도 일치하며, 패턴에는 *와 ?를 사용합니다(*.pkg).",
"cw_files_search_more": "처음 {n}개를 표시합니다. 더 있습니다. 더 긴 이름으로 시도하세요.",
"cw_files_search_no_index": "아직 색인이 없습니다. 첫 검색에서 색인을 만듭니다. 모든 폴더를 읽으므로 몇 분 걸릴 수 있습니다.",
"cw_files_search_none": "“{q}”과(와) 일치하는 항목이 없습니다.",
"cw_files_search_partial": "일찍 중지되어 일부 파일이 빠져 있습니다.",
"cw_files_search_ready": "색인: {n}개 파일.",
"cw_files_search_rebuild": "다시 만들기",
"cw_files_search_results": "검색 결과",
"cw_files_search_scope": "검색 위치",
"cw_files_search_searching": "검색 중…",
"cw_files_search_stopping": "색인 중지 중…",
"cw_files_search_unsupported": "이 콘솔의 PS5Upload가 오래되어 검색할 수 없습니다. 검색을 사용하려면 업데이트하세요.",
"cw_files_search_waiting": "색인이 준비되면 “{q}”을(를) 검색합니다.",
"cw_files_select_all": "모두 선택",
"cw_files_select_invert": "선택 반전",
"cw_files_select_none": "선택 해제",
@@ -251,6 +274,20 @@
"cw_files_unpin": "고정 해제",
"cw_files_unpin_done": "{name} 고정을 해제했습니다.",
"cw_files_up": "위로",
"cw_files_usage": "디스크 사용량",
"cw_files_usage_again": "다시 측정",
"cw_files_usage_busy": "다른 폴더 크기를 측정하는 중입니다. 끝난 뒤 다시 시도하세요.",
"cw_files_usage_cancelled": "취소됨: {size} 집계, 나머지는 측정하지 않았습니다.",
"cw_files_usage_capped": "항목 {items}개에서 중지: {size} 집계, 폴더에 더 있습니다.",
"cw_files_usage_done": "항목 {items}개에 {size}.",
"cw_files_usage_files_here": "이 폴더의 파일",
"cw_files_usage_inside": "{name} 안 보기",
"cw_files_usage_open": "파일에서 열기",
"cw_files_usage_open_row": "{name}을(를) 파일에서 열기",
"cw_files_usage_running": "측정 중… 폴더 {n}개 중 {at}개, 항목 {items}개.",
"cw_files_usage_skipped": "측정 안 됨",
"cw_files_usage_timeout": "3분 후 중지: {size} 집계, 나머지는 측정하지 않았습니다.",
"cw_files_usage_unreadable": "읽을 수 없음",
"cw_files_verify": "확인(체크섬)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "취소했습니다.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Nie udało się usunąć zastępowanego elementu, więc nic nie wklejono.",
"cw_files_retry": "Spróbuj ponownie",
"cw_files_rows_more": "Pokaż więcej wierszy (zostało {n})",
"cw_files_search": "Szukaj",
"cw_files_search_all": "Wszystkie dyski",
"cw_files_search_bad": "Nie można szukać cudzysłowów ani ukośników wstecznych.",
"cw_files_search_building": "Budowanie indeksu… dotąd {n} plików.",
"cw_files_search_cancel": "Anuluj",
"cw_files_search_capped": "Jest pełny (200 000 plików), więc brakuje niektórych plików.",
"cw_files_search_field": "Nazwa lub wzorzec",
"cw_files_search_found": "Znaleziono: {n}.",
"cw_files_search_go": "Szukaj",
"cw_files_search_here": "Ten folder",
"cw_files_search_hint": "Znajduje pliki po nazwie. Słowo pasuje w dowolnym miejscu nazwy; użyj * i ? jako wzorca (*.pkg).",
"cw_files_search_more": "Pokazano pierwsze {n}. Jest ich więcej: spróbuj dłuższej nazwy.",
"cw_files_search_no_index": "Nie ma jeszcze indeksu. Pierwsze wyszukiwanie go zbuduje: odczyta każdy folder i może potrwać kilka minut.",
"cw_files_search_none": "Nic nie pasuje do „{q}”.",
"cw_files_search_partial": "Zatrzymano go przed końcem, więc brakuje niektórych plików.",
"cw_files_search_ready": "Indeks: {n} plików.",
"cw_files_search_rebuild": "Zbuduj ponownie",
"cw_files_search_results": "Wyniki wyszukiwania",
"cw_files_search_scope": "Gdzie szukać",
"cw_files_search_searching": "Wyszukiwanie…",
"cw_files_search_stopping": "Zatrzymywanie indeksu…",
"cw_files_search_unsupported": "PS5Upload na tej konsoli jest za stary, by szukać. Zaktualizuj go, aby korzystać z wyszukiwania.",
"cw_files_search_waiting": "Wyszuka „{q}”, gdy indeks będzie gotowy.",
"cw_files_select_all": "Zaznacz wszystko",
"cw_files_select_invert": "Odwróć",
"cw_files_select_none": "Odznacz wszystko",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Odepnij",
"cw_files_unpin_done": "Odpięto: {name}.",
"cw_files_up": "W górę",
"cw_files_usage": "Zajęte miejsce",
"cw_files_usage_again": "Zmierz ponownie",
"cw_files_usage_busy": "Trwa mierzenie rozmiaru innego folderu. Spróbuj ponownie, gdy się skończy.",
"cw_files_usage_cancelled": "Anulowano: policzono {size}, reszty nie zmierzono.",
"cw_files_usage_capped": "Zatrzymano przy {items} elementach: policzono {size}, folder zawiera więcej.",
"cw_files_usage_done": "{size} w {items} elementach.",
"cw_files_usage_files_here": "Pliki w tym folderze",
"cw_files_usage_inside": "Zajrzyj do {name}",
"cw_files_usage_open": "Otwórz w Plikach",
"cw_files_usage_open_row": "Otwórz {name} w Plikach",
"cw_files_usage_running": "Mierzenie… {at} z {n} folderów, {items} elementów.",
"cw_files_usage_skipped": "nie zmierzono",
"cw_files_usage_timeout": "Zatrzymano po 3 minutach: policzono {size}, reszty nie zmierzono.",
"cw_files_usage_unreadable": "nieczytelny",
"cw_files_verify": "Sprawdź (suma kontrolna)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Anulowano.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Não foi possível excluir o item a ser substituído, então ele não foi colado.",
"cw_files_retry": "Tentar de novo",
"cw_files_rows_more": "Mostrar mais linhas ({n} restantes)",
"cw_files_search": "Pesquisar",
"cw_files_search_all": "Todas as unidades",
"cw_files_search_bad": "Não é possível pesquisar aspas nem barras invertidas.",
"cw_files_search_building": "Criando o índice… {n} arquivos até agora.",
"cw_files_search_cancel": "Cancelar",
"cw_files_search_capped": "Ele está cheio (200.000 arquivos), então faltam alguns arquivos.",
"cw_files_search_field": "Nome ou padrão",
"cw_files_search_found": "{n} encontrados.",
"cw_files_search_go": "Pesquisar",
"cw_files_search_here": "Esta pasta",
"cw_files_search_hint": "Encontra arquivos pelo nome. Uma palavra corresponde em qualquer parte do nome; use * e ? para um padrão (*.pkg).",
"cw_files_search_more": "Mostrando os primeiros {n}. Há mais: tente um nome mais longo.",
"cw_files_search_no_index": "Ainda não há índice. A primeira pesquisa cria um: ela lê todas as pastas e pode levar alguns minutos.",
"cw_files_search_none": "Nada corresponde a “{q}”.",
"cw_files_search_partial": "Ele foi interrompido antes do fim, então faltam alguns arquivos.",
"cw_files_search_ready": "Índice: {n} arquivos.",
"cw_files_search_rebuild": "Criar de novo",
"cw_files_search_results": "Resultados da pesquisa",
"cw_files_search_scope": "Onde pesquisar",
"cw_files_search_searching": "Pesquisando…",
"cw_files_search_stopping": "Parando o índice…",
"cw_files_search_unsupported": "O PS5Upload deste console é antigo demais para pesquisar. Atualize-o para usar a pesquisa.",
"cw_files_search_waiting": "Vai pesquisar “{q}” quando o índice estiver pronto.",
"cw_files_select_all": "Selecionar tudo",
"cw_files_select_invert": "Inverter",
"cw_files_select_none": "Limpar seleção",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Desafixar",
"cw_files_unpin_done": "{name} desafixada.",
"cw_files_up": "Acima",
"cw_files_usage": "Uso do disco",
"cw_files_usage_again": "Medir de novo",
"cw_files_usage_busy": "O tamanho de outra pasta está sendo medido. Tente de novo quando terminar.",
"cw_files_usage_cancelled": "Cancelado: {size} contados, o resto não foi medido.",
"cw_files_usage_capped": "Parou em {items} itens: {size} contados, a pasta tem mais.",
"cw_files_usage_done": "{size} em {items} itens.",
"cw_files_usage_files_here": "Arquivos nesta pasta",
"cw_files_usage_inside": "Ver dentro de {name}",
"cw_files_usage_open": "Abrir em Arquivos",
"cw_files_usage_open_row": "Abrir {name} em Arquivos",
"cw_files_usage_running": "Medindo… {at} de {n} pastas, {items} itens.",
"cw_files_usage_skipped": "não medido",
"cw_files_usage_timeout": "Parou após 3 minutos: {size} contados, o resto não foi medido.",
"cw_files_usage_unreadable": "ilegível",
"cw_files_verify": "Verificar (checksum)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Cancelado.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Не удалось удалить заменяемый элемент, поэтому вставка не выполнена.",
"cw_files_retry": "Повторить",
"cw_files_rows_more": "Показать ещё строки (осталось {n})",
"cw_files_search": "Поиск",
"cw_files_search_all": "Все диски",
"cw_files_search_bad": "Кавычки и обратную косую черту искать нельзя.",
"cw_files_search_building": "Создание индекса… пока {n} файлов.",
"cw_files_search_cancel": "Отмена",
"cw_files_search_capped": "Он заполнен (200 000 файлов), поэтому некоторых файлов нет.",
"cw_files_search_field": "Имя или шаблон",
"cw_files_search_found": "Найдено: {n}.",
"cw_files_search_go": "Найти",
"cw_files_search_here": "Эта папка",
"cw_files_search_hint": "Ищет файлы по имени. Слово совпадает в любом месте имени; для шаблона используйте * и ? (*.pkg).",
"cw_files_search_more": "Показаны первые {n}. Есть ещё: попробуйте имя подлиннее.",
"cw_files_search_no_index": "Индекса пока нет. Первый поиск создаст его: он читает каждую папку и может занять несколько минут.",
"cw_files_search_none": "Ничего не найдено по «{q}».",
"cw_files_search_partial": "Он был остановлен раньше времени, поэтому некоторых файлов нет.",
"cw_files_search_ready": "Индекс: {n} файлов.",
"cw_files_search_rebuild": "Создать заново",
"cw_files_search_results": "Результаты поиска",
"cw_files_search_scope": "Где искать",
"cw_files_search_searching": "Поиск…",
"cw_files_search_stopping": "Остановка индекса…",
"cw_files_search_unsupported": "PS5Upload на этой консоли слишком старый для поиска. Обновите его, чтобы искать.",
"cw_files_search_waiting": "Найдёт «{q}», когда индекс будет готов.",
"cw_files_select_all": "Выбрать все",
"cw_files_select_invert": "Инвертировать",
"cw_files_select_none": "Снять выбор",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Открепить",
"cw_files_unpin_done": "{name} откреплена.",
"cw_files_up": "Вверх",
"cw_files_usage": "Занятое место",
"cw_files_usage_again": "Измерить заново",
"cw_files_usage_busy": "Сейчас измеряется размер другой папки. Повторите, когда это закончится.",
"cw_files_usage_cancelled": "Отменено: учтено {size}, остальное не измерено.",
"cw_files_usage_capped": "Остановлено на {items} элементах: учтено {size}, в папке есть ещё.",
"cw_files_usage_done": "{size} в {items} элементах.",
"cw_files_usage_files_here": "Файлы в этой папке",
"cw_files_usage_inside": "Заглянуть в {name}",
"cw_files_usage_open": "Открыть в «Файлах»",
"cw_files_usage_open_row": "Открыть {name} в «Файлах»",
"cw_files_usage_running": "Измерение… {at} из {n} папок, {items} элементов.",
"cw_files_usage_skipped": "не измерено",
"cw_files_usage_timeout": "Остановлено через 3 минуты: учтено {size}, остальное не измерено.",
"cw_files_usage_unreadable": "не читается",
"cw_files_verify": "Проверить (контрольная сумма)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Отменено.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "ลบรายการที่จะแทนที่ไม่ได้ จึงไม่ได้วาง",
"cw_files_retry": "ลองอีกครั้ง",
"cw_files_rows_more": "แสดงแถวเพิ่ม (เหลือ {n})",
"cw_files_search": "ค้นหา",
"cw_files_search_all": "ทุกไดรฟ์",
"cw_files_search_bad": "ค้นหาเครื่องหมายคำพูดและแบ็กสแลชไม่ได้",
"cw_files_search_building": "กำลังสร้างดัชนี… ตอนนี้ {n} ไฟล์",
"cw_files_search_cancel": "ยกเลิก",
"cw_files_search_capped": "ดัชนีเต็ม (200,000 ไฟล์) จึงขาดบางไฟล์",
"cw_files_search_field": "ชื่อหรือรูปแบบ",
"cw_files_search_found": "พบ {n} รายการ",
"cw_files_search_go": "ค้นหา",
"cw_files_search_here": "โฟลเดอร์นี้",
"cw_files_search_hint": "ค้นหาไฟล์ตามชื่อ คำจะตรงกับส่วนใดก็ได้ของชื่อ ใช้ * และ ? เป็นรูปแบบ (*.pkg)",
"cw_files_search_more": "แสดง {n} รายการแรก ยังมีอีก: ลองใช้ชื่อที่ยาวขึ้น",
"cw_files_search_no_index": "ยังไม่มีดัชนี การค้นหาครั้งแรกจะสร้างดัชนี โดยอ่านทุกโฟลเดอร์และอาจใช้เวลาสองสามนาที",
"cw_files_search_none": "ไม่มีสิ่งที่ตรงกับ “{q}”",
"cw_files_search_partial": "ดัชนีถูกหยุดก่อนเสร็จ จึงขาดบางไฟล์",
"cw_files_search_ready": "ดัชนี: {n} ไฟล์",
"cw_files_search_rebuild": "สร้างใหม่",
"cw_files_search_results": "ผลการค้นหา",
"cw_files_search_scope": "ค้นหาที่ใด",
"cw_files_search_searching": "กำลังค้นหา…",
"cw_files_search_stopping": "กำลังหยุดดัชนี…",
"cw_files_search_unsupported": "PS5Upload บนเครื่องนี้เก่าเกินกว่าจะค้นหาได้ อัปเดตเพื่อใช้การค้นหา",
"cw_files_search_waiting": "จะค้นหา “{q}” เมื่อดัชนีพร้อม",
"cw_files_select_all": "เลือกทั้งหมด",
"cw_files_select_invert": "กลับการเลือก",
"cw_files_select_none": "ไม่เลือกเลย",
@@ -251,6 +274,20 @@
"cw_files_unpin": "เลิกปักหมุด",
"cw_files_unpin_done": "เลิกปักหมุด {name} แล้ว",
"cw_files_up": "ขึ้น",
"cw_files_usage": "การใช้ดิสก์",
"cw_files_usage_again": "วัดอีกครั้ง",
"cw_files_usage_busy": "กำลังวัดขนาดโฟลเดอร์อื่นอยู่ ลองอีกครั้งเมื่อเสร็จ",
"cw_files_usage_cancelled": "ยกเลิกแล้ว: นับได้ {size} ส่วนที่เหลือไม่ได้วัด",
"cw_files_usage_capped": "หยุดที่ {items} รายการ: นับได้ {size} โฟลเดอร์ยังมีอีก",
"cw_files_usage_done": "{size} ใน {items} รายการ",
"cw_files_usage_files_here": "ไฟล์ในโฟลเดอร์นี้",
"cw_files_usage_inside": "ดูข้างใน {name}",
"cw_files_usage_open": "เปิดในไฟล์",
"cw_files_usage_open_row": "เปิด {name} ในไฟล์",
"cw_files_usage_running": "กำลังวัด… {at} จาก {n} โฟลเดอร์, {items} รายการ",
"cw_files_usage_skipped": "ไม่ได้วัด",
"cw_files_usage_timeout": "หยุดหลัง 3 นาที: นับได้ {size} ส่วนที่เหลือไม่ได้วัด",
"cw_files_usage_unreadable": "อ่านไม่ได้",
"cw_files_verify": "ตรวจสอบ (เช็กซัม)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "ยกเลิกแล้ว",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Değiştirilecek öğe silinemedi, bu yüzden yapıştırılmadı.",
"cw_files_retry": "Tekrar dene",
"cw_files_rows_more": "Daha fazla satır göster ({n} kaldı)",
"cw_files_search": "Ara",
"cw_files_search_all": "Tüm sürücüler",
"cw_files_search_bad": "Tırnak işaretleri ve ters eğik çizgiler aranamaz.",
"cw_files_search_building": "Dizin oluşturuluyor… şimdiye dek {n} dosya.",
"cw_files_search_cancel": "İptal",
"cw_files_search_capped": "Dolu (200.000 dosya), bu yüzden bazı dosyalar eksik.",
"cw_files_search_field": "Ad veya desen",
"cw_files_search_found": "{n} bulundu.",
"cw_files_search_go": "Ara",
"cw_files_search_here": "Bu klasör",
"cw_files_search_hint": "Dosyaları adına göre bulur. Bir sözcük adın herhangi bir yerinde eşleşir; desen için * ve ? kullanın (*.pkg).",
"cw_files_search_more": "İlk {n} gösteriliyor. Daha fazlası var: daha uzun bir ad deneyin.",
"cw_files_search_no_index": "Henüz dizin yok. İlk arama bir dizin oluşturur: her klasörü okur ve birkaç dakika sürebilir.",
"cw_files_search_none": "“{q}” ile eşleşen bir şey yok.",
"cw_files_search_partial": "Erken durduruldu, bu yüzden bazı dosyalar eksik.",
"cw_files_search_ready": "Dizin: {n} dosya.",
"cw_files_search_rebuild": "Yeniden oluştur",
"cw_files_search_results": "Arama sonuçları",
"cw_files_search_scope": "Nerede aranacak",
"cw_files_search_searching": "Aranıyor…",
"cw_files_search_stopping": "Dizin durduruluyor…",
"cw_files_search_unsupported": "Bu konsoldaki PS5Upload arama için çok eski. Aramayı kullanmak için güncelleyin.",
"cw_files_search_waiting": "Dizin hazır olunca “{q}” aranacak.",
"cw_files_select_all": "Tümünü seç",
"cw_files_select_invert": "Tersine çevir",
"cw_files_select_none": "Seçimi kaldır",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Sabitlemeyi kaldır",
"cw_files_unpin_done": "{name} sabitlemesi kaldırıldı.",
"cw_files_up": "Yukarı",
"cw_files_usage": "Disk kullanımı",
"cw_files_usage_again": "Yeniden ölç",
"cw_files_usage_busy": "Başka bir klasörün boyutu ölçülüyor. Bitince yeniden deneyin.",
"cw_files_usage_cancelled": "İptal edildi: {size} sayıldı, kalanı ölçülmedi.",
"cw_files_usage_capped": "{items} öğede durdu: {size} sayıldı, klasörde daha fazlası var.",
"cw_files_usage_done": "{items} öğede {size}.",
"cw_files_usage_files_here": "Bu klasördeki dosyalar",
"cw_files_usage_inside": "{name} içine bak",
"cw_files_usage_open": "Dosyalar’da aç",
"cw_files_usage_open_row": "{name} öğesini Dosyalar’da aç",
"cw_files_usage_running": "Ölçülüyor… {n} klasörden {at}, {items} öğe.",
"cw_files_usage_skipped": "ölçülmedi",
"cw_files_usage_timeout": "3 dakika sonra durdu: {size} sayıldı, kalanı ölçülmedi.",
"cw_files_usage_unreadable": "okunamıyor",
"cw_files_verify": "Doğrula (sağlama toplamı)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "İptal edildi.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "Không xóa được mục cần thay thế nên chưa dán.",
"cw_files_retry": "Thử lại",
"cw_files_rows_more": "Hiện thêm dòng (còn {n})",
"cw_files_search": "Tìm kiếm",
"cw_files_search_all": "Tất cả ổ đĩa",
"cw_files_search_bad": "Không thể tìm dấu ngoặc kép và dấu gạch chéo ngược.",
"cw_files_search_building": "Đang tạo chỉ mục… đến giờ {n} tệp.",
"cw_files_search_cancel": "Hủy",
"cw_files_search_capped": "Chỉ mục đã đầy (200.000 tệp) nên thiếu một số tệp.",
"cw_files_search_field": "Tên hoặc mẫu",
"cw_files_search_found": "Tìm thấy {n}.",
"cw_files_search_go": "Tìm",
"cw_files_search_here": "Thư mục này",
"cw_files_search_hint": "Tìm tệp theo tên. Một từ khớp ở bất kỳ đâu trong tên; dùng * và ? cho mẫu (*.pkg).",
"cw_files_search_more": "Đang hiện {n} kết quả đầu. Còn nữa: hãy thử tên dài hơn.",
"cw_files_search_no_index": "Chưa có chỉ mục. Lần tìm đầu tiên sẽ tạo: nó đọc mọi thư mục và có thể mất vài phút.",
"cw_files_search_none": "Không có gì khớp với “{q}”.",
"cw_files_search_partial": "Chỉ mục bị dừng sớm nên thiếu một số tệp.",
"cw_files_search_ready": "Chỉ mục: {n} tệp.",
"cw_files_search_rebuild": "Tạo lại",
"cw_files_search_results": "Kết quả tìm kiếm",
"cw_files_search_scope": "Tìm ở đâu",
"cw_files_search_searching": "Đang tìm…",
"cw_files_search_stopping": "Đang dừng chỉ mục…",
"cw_files_search_unsupported": "PS5Upload trên máy này quá cũ để tìm kiếm. Hãy cập nhật để dùng tìm kiếm.",
"cw_files_search_waiting": "Sẽ tìm “{q}” khi chỉ mục sẵn sàng.",
"cw_files_select_all": "Chọn tất cả",
"cw_files_select_invert": "Đảo chọn",
"cw_files_select_none": "Bỏ chọn",
@@ -251,6 +274,20 @@
"cw_files_unpin": "Bỏ ghim",
"cw_files_unpin_done": "Đã bỏ ghim {name}.",
"cw_files_up": "Lên",
"cw_files_usage": "Dung lượng đã dùng",
"cw_files_usage_again": "Đo lại",
"cw_files_usage_busy": "Đang đo kích thước một thư mục khác. Hãy thử lại khi xong.",
"cw_files_usage_cancelled": "Đã hủy: đã đếm {size}, phần còn lại chưa đo.",
"cw_files_usage_capped": "Dừng ở {items} mục: đã đếm {size}, thư mục còn nhiều hơn.",
"cw_files_usage_done": "{size} trong {items} mục.",
"cw_files_usage_files_here": "Tệp trong thư mục này",
"cw_files_usage_inside": "Xem bên trong {name}",
"cw_files_usage_open": "Mở trong Tệp",
"cw_files_usage_open_row": "Mở {name} trong Tệp",
"cw_files_usage_running": "Đang đo… {at}/{n} thư mục, {items} mục.",
"cw_files_usage_skipped": "chưa đo",
"cw_files_usage_timeout": "Dừng sau 3 phút: đã đếm {size}, phần còn lại chưa đo.",
"cw_files_usage_unreadable": "không đọc được",
"cw_files_verify": "Kiểm tra (checksum)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "Đã hủy.",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "无法删除要替换的项目,因此未粘贴。",
"cw_files_retry": "重试",
"cw_files_rows_more": "显示更多行(还剩 {n})",
"cw_files_search": "搜索",
"cw_files_search_all": "所有驱动器",
"cw_files_search_bad": "无法搜索引号和反斜杠。",
"cw_files_search_building": "正在建立索引… 目前 {n} 个文件。",
"cw_files_search_cancel": "取消",
"cw_files_search_capped": "索引已满(200,000 个文件),因此缺少部分文件。",
"cw_files_search_field": "名称或模式",
"cw_files_search_found": "找到 {n} 个。",
"cw_files_search_go": "搜索",
"cw_files_search_here": "此文件夹",
"cw_files_search_hint": "按名称查找文件。单词可匹配名称中的任意位置;模式可使用 * 和 ?(*.pkg)。",
"cw_files_search_more": "显示前 {n} 个。还有更多:请尝试更长的名称。",
"cw_files_search_no_index": "还没有索引。首次搜索会建立索引:需要读取每个文件夹,可能需要几分钟。",
"cw_files_search_none": "没有与“{q}”匹配的内容。",
"cw_files_search_partial": "索引提前停止,因此缺少部分文件。",
"cw_files_search_ready": "索引:{n} 个文件。",
"cw_files_search_rebuild": "重新建立",
"cw_files_search_results": "搜索结果",
"cw_files_search_scope": "搜索范围",
"cw_files_search_searching": "正在搜索…",
"cw_files_search_stopping": "正在停止索引…",
"cw_files_search_unsupported": "此主机上的 PS5Upload 版本过旧,无法搜索。请更新后使用搜索。",
"cw_files_search_waiting": "索引就绪后将搜索“{q}”。",
"cw_files_select_all": "全选",
"cw_files_select_invert": "反选",
"cw_files_select_none": "取消全选",
@@ -251,6 +274,20 @@
"cw_files_unpin": "取消固定",
"cw_files_unpin_done": "已取消固定 {name}。",
"cw_files_up": "上一级",
"cw_files_usage": "磁盘用量",
"cw_files_usage_again": "重新测量",
"cw_files_usage_busy": "正在测量另一个文件夹的大小。请在其结束后重试。",
"cw_files_usage_cancelled": "已取消:已计 {size},其余未测量。",
"cw_files_usage_capped": "在 {items} 项处停止:已计 {size},文件夹中还有更多。",
"cw_files_usage_done": "{items} 项共 {size}。",
"cw_files_usage_files_here": "此文件夹中的文件",
"cw_files_usage_inside": "查看 {name} 内部",
"cw_files_usage_open": "在“文件”中打开",
"cw_files_usage_open_row": "在“文件”中打开 {name}",
"cw_files_usage_running": "正在测量… {n} 个文件夹中的第 {at} 个,{items} 项。",
"cw_files_usage_skipped": "未测量",
"cw_files_usage_timeout": "3 分钟后停止:已计 {size},其余未测量。",
"cw_files_usage_unreadable": "无法读取",
"cw_files_verify": "校验(校验和)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "已取消。",
+37
View File
@@ -188,6 +188,29 @@
"cw_files_replace_failed": "無法刪除要取代的項目,因此未貼上。",
"cw_files_retry": "重試",
"cw_files_rows_more": "顯示更多列(還剩 {n})",
"cw_files_search": "搜尋",
"cw_files_search_all": "所有磁碟機",
"cw_files_search_bad": "無法搜尋引號和反斜線。",
"cw_files_search_building": "正在建立索引… 目前 {n} 個檔案。",
"cw_files_search_cancel": "取消",
"cw_files_search_capped": "索引已滿(200,000 個檔案),因此缺少部分檔案。",
"cw_files_search_field": "名稱或樣式",
"cw_files_search_found": "找到 {n} 個。",
"cw_files_search_go": "搜尋",
"cw_files_search_here": "此資料夾",
"cw_files_search_hint": "依名稱尋找檔案。單字可符合名稱中的任何位置;樣式可使用 * 和 ?(*.pkg)。",
"cw_files_search_more": "顯示前 {n} 個。還有更多:請嘗試較長的名稱。",
"cw_files_search_no_index": "尚無索引。第一次搜尋會建立索引:需要讀取每個資料夾,可能需要幾分鐘。",
"cw_files_search_none": "沒有符合「{q}」的項目。",
"cw_files_search_partial": "索引提早停止,因此缺少部分檔案。",
"cw_files_search_ready": "索引:{n} 個檔案。",
"cw_files_search_rebuild": "重新建立",
"cw_files_search_results": "搜尋結果",
"cw_files_search_scope": "搜尋範圍",
"cw_files_search_searching": "正在搜尋…",
"cw_files_search_stopping": "正在停止索引…",
"cw_files_search_unsupported": "此主機上的 PS5Upload 版本太舊,無法搜尋。請更新後使用搜尋。",
"cw_files_search_waiting": "索引就緒後將搜尋「{q}」。",
"cw_files_select_all": "全選",
"cw_files_select_invert": "反選",
"cw_files_select_none": "取消全選",
@@ -251,6 +274,20 @@
"cw_files_unpin": "取消釘選",
"cw_files_unpin_done": "已取消釘選 {name}。",
"cw_files_up": "上一層",
"cw_files_usage": "磁碟用量",
"cw_files_usage_again": "重新測量",
"cw_files_usage_busy": "正在測量另一個資料夾的大小。請在結束後重試。",
"cw_files_usage_cancelled": "已取消:已計 {size},其餘未測量。",
"cw_files_usage_capped": "在 {items} 項處停止:已計 {size},資料夾中還有更多。",
"cw_files_usage_done": "{items} 項共 {size}。",
"cw_files_usage_files_here": "此資料夾中的檔案",
"cw_files_usage_inside": "查看 {name} 內部",
"cw_files_usage_open": "在「檔案」中開啟",
"cw_files_usage_open_row": "在「檔案」中開啟 {name}",
"cw_files_usage_running": "正在測量… {n} 個資料夾中的第 {at} 個,{items} 項。",
"cw_files_usage_skipped": "未測量",
"cw_files_usage_timeout": "3 分鐘後停止:已計 {size},其餘未測量。",
"cw_files_usage_unreadable": "無法讀取",
"cw_files_verify": "驗證(檢查碼)",
"cw_files_verify_blake3": "BLAKE3",
"cw_files_verify_cancelled": "已取消。",
+37
View File
@@ -4450,6 +4450,43 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
"cw_files_game_ps4": "مجلد لعبة PS4",
"cw_files_game_version": "الإصدار {v}",
"cw_files_game_fw": "يتطلب برنامج النظام {v}",
"cw_files_search": "بحث",
"cw_files_search_field": "الاسم أو النمط",
"cw_files_search_go": "بحث",
"cw_files_search_scope": "مكان البحث",
"cw_files_search_all": "كل الأقراص",
"cw_files_search_here": "هذا المجلد",
"cw_files_search_hint": "يبحث عن الملفات بالاسم. تطابق الكلمة أي جزء من الاسم؛ استخدم * و ? للنمط (*.pkg).",
"cw_files_search_bad": "لا يمكن البحث عن علامات الاقتباس والشرطات المائلة العكسية.",
"cw_files_search_unsupported": "إصدار PS5Upload على هذا الجهاز قديم جدًا للبحث. حدّثه لاستخدام البحث.",
"cw_files_search_no_index": "لا يوجد فهرس بعد. أول بحث ينشئ فهرسًا: يقرأ كل مجلد وقد يستغرق بضع دقائق.",
"cw_files_search_building": "جارٍ إنشاء الفهرس… {n} ملف حتى الآن.",
"cw_files_search_stopping": "جارٍ إيقاف الفهرس…",
"cw_files_search_ready": "الفهرس: {n} ملف.",
"cw_files_search_partial": "أُوقف مبكرًا، لذا تنقصه بعض الملفات.",
"cw_files_search_capped": "امتلأ (200,000 ملف)، لذا تنقصه بعض الملفات.",
"cw_files_search_rebuild": "إنشاء من جديد",
"cw_files_search_cancel": "إلغاء",
"cw_files_search_waiting": "سيبحث عن «{q}» عندما يجهز الفهرس.",
"cw_files_search_searching": "جارٍ البحث…",
"cw_files_search_none": "لا شيء يطابق «{q}».",
"cw_files_search_found": "عُثر على {n}.",
"cw_files_search_more": "تُعرض أول {n} نتيجة. هناك المزيد: جرّب اسمًا أطول.",
"cw_files_search_results": "نتائج البحث",
"cw_files_usage": "استخدام القرص",
"cw_files_usage_open": "فتح في الملفات",
"cw_files_usage_open_row": "فتح {name} في الملفات",
"cw_files_usage_inside": "عرض محتوى {name}",
"cw_files_usage_files_here": "الملفات في هذا المجلد",
"cw_files_usage_running": "جارٍ القياس… {at} من {n} مجلد، {items} عنصر.",
"cw_files_usage_done": "{size} في {items} عنصر.",
"cw_files_usage_capped": "توقف عند {items} عنصر: حُسب {size}، والمجلد يحوي المزيد.",
"cw_files_usage_timeout": "توقف بعد 3 دقائق: حُسب {size}، ولم يُقس الباقي.",
"cw_files_usage_cancelled": "أُلغي: حُسب {size}، ولم يُقس الباقي.",
"cw_files_usage_again": "القياس مجددًا",
"cw_files_usage_busy": "يجري قياس حجم مجلد آخر. أعد المحاولة عند انتهائه.",
"cw_files_usage_skipped": "لم يُقس",
"cw_files_usage_unreadable": "غير قابل للقراءة",
};
export default ar;
+37
View File
@@ -4450,6 +4450,43 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
"cw_files_game_ps4": "PS4 গেম ফোল্ডার",
"cw_files_game_version": "সংস্করণ {v}",
"cw_files_game_fw": "সিস্টেম সফটওয়্যার {v} দরকার",
"cw_files_search": "খুঁজুন",
"cw_files_search_field": "নাম বা প্যাটার্ন",
"cw_files_search_go": "খুঁজুন",
"cw_files_search_scope": "কোথায় খুঁজবেন",
"cw_files_search_all": "সব ড্রাইভ",
"cw_files_search_here": "এই ফোল্ডার",
"cw_files_search_hint": "নাম ধরে ফাইল খোঁজে। একটি শব্দ নামের যেকোনো জায়গায় মিলে যায়; প্যাটার্নের জন্য * ও ? ব্যবহার করুন (*.pkg)।",
"cw_files_search_bad": "উদ্ধৃতিচিহ্ন ও ব্যাকস্ল্যাশ খোঁজা যায় না।",
"cw_files_search_unsupported": "এই কনসোলের PS5Upload খোঁজার জন্য খুব পুরনো। খোঁজ ব্যবহার করতে এটি আপডেট করুন।",
"cw_files_search_no_index": "এখনও কোনো ইনডেক্স নেই। প্রথম খোঁজ একটি তৈরি করে: এটি প্রতিটি ফোল্ডার পড়ে এবং কয়েক মিনিট লাগতে পারে।",
"cw_files_search_building": "ইনডেক্স তৈরি হচ্ছে… এখন পর্যন্ত {n}টি ফাইল।",
"cw_files_search_stopping": "ইনডেক্স থামানো হচ্ছে…",
"cw_files_search_ready": "ইনডেক্স: {n}টি ফাইল।",
"cw_files_search_partial": "এটি আগেই থামানো হয়েছিল, তাই কিছু ফাইল নেই।",
"cw_files_search_capped": "এটি পূর্ণ (200,000টি ফাইল), তাই কিছু ফাইল নেই।",
"cw_files_search_rebuild": "আবার তৈরি করুন",
"cw_files_search_cancel": "বাতিল",
"cw_files_search_waiting": "ইনডেক্স তৈরি হলে “{q}” খুঁজবে।",
"cw_files_search_searching": "খোঁজা হচ্ছে…",
"cw_files_search_none": "“{q}”-এর সাথে কিছু মেলে না।",
"cw_files_search_found": "{n}টি পাওয়া গেছে।",
"cw_files_search_more": "প্রথম {n}টি দেখানো হচ্ছে। আরও আছে: লম্বা নাম দিয়ে চেষ্টা করুন।",
"cw_files_search_results": "খোঁজের ফলাফল",
"cw_files_usage": "ডিস্ক ব্যবহার",
"cw_files_usage_open": "ফাইল-এ খুলুন",
"cw_files_usage_open_row": "{name} ফাইল-এ খুলুন",
"cw_files_usage_inside": "{name}-এর ভেতরে দেখুন",
"cw_files_usage_files_here": "এই ফোল্ডারের ফাইল",
"cw_files_usage_running": "মাপা হচ্ছে… {n}টি ফোল্ডারের মধ্যে {at}, {items}টি আইটেম।",
"cw_files_usage_done": "{items}টি আইটেমে {size}।",
"cw_files_usage_capped": "{items}টি আইটেমে থেমেছে: {size} গোনা হয়েছে, ফোল্ডারে আরও আছে।",
"cw_files_usage_timeout": "৩ মিনিট পর থেমেছে: {size} গোনা হয়েছে, বাকিটা মাপা হয়নি।",
"cw_files_usage_cancelled": "বাতিল হয়েছে: {size} গোনা হয়েছে, বাকিটা মাপা হয়নি।",
"cw_files_usage_again": "আবার মাপুন",
"cw_files_usage_busy": "অন্য একটি ফোল্ডারের আকার মাপা হচ্ছে। সেটি শেষ হলে আবার চেষ্টা করুন।",
"cw_files_usage_skipped": "মাপা হয়নি",
"cw_files_usage_unreadable": "পড়া যায় না",
};
export default bn;
+37
View File
@@ -4458,6 +4458,43 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}",
"cw_files_game_ps4": "PS4-Spielordner",
"cw_files_game_version": "Version {v}",
"cw_files_game_fw": "benötigt Systemsoftware {v}",
"cw_files_search": "Suchen",
"cw_files_search_field": "Name oder Muster",
"cw_files_search_go": "Suchen",
"cw_files_search_scope": "Wo suchen",
"cw_files_search_all": "Alle Laufwerke",
"cw_files_search_here": "Dieser Ordner",
"cw_files_search_hint": "Findet Dateien nach Namen. Ein Wort passt überall im Namen; mit * und ? als Muster (*.pkg).",
"cw_files_search_bad": "Nach Anführungszeichen und Backslashes kann nicht gesucht werden.",
"cw_files_search_unsupported": "PS5Upload auf dieser Konsole ist zu alt für die Suche. Aktualisiere es, um zu suchen.",
"cw_files_search_no_index": "Noch kein Index. Die erste Suche erstellt einen: Sie liest jeden Ordner und kann einige Minuten dauern.",
"cw_files_search_building": "Index wird erstellt… bisher {n} Dateien.",
"cw_files_search_stopping": "Index wird gestoppt…",
"cw_files_search_ready": "Index: {n} Dateien.",
"cw_files_search_partial": "Er wurde vorzeitig gestoppt, daher fehlen einige Dateien.",
"cw_files_search_capped": "Er ist voll (200.000 Dateien), daher fehlen einige Dateien.",
"cw_files_search_rebuild": "Neu erstellen",
"cw_files_search_cancel": "Abbrechen",
"cw_files_search_waiting": "Sucht nach „{q}“, sobald der Index fertig ist.",
"cw_files_search_searching": "Suche…",
"cw_files_search_none": "Nichts passt zu „{q}“.",
"cw_files_search_found": "{n} gefunden.",
"cw_files_search_more": "Die ersten {n} werden angezeigt. Es gibt mehr: Versuche einen längeren Namen.",
"cw_files_search_results": "Suchergebnisse",
"cw_files_usage": "Speicherbelegung",
"cw_files_usage_open": "In Dateien öffnen",
"cw_files_usage_open_row": "{name} in Dateien öffnen",
"cw_files_usage_inside": "In {name} hineinsehen",
"cw_files_usage_files_here": "Dateien in diesem Ordner",
"cw_files_usage_running": "Messe… {at} von {n} Ordnern, {items} Elemente.",
"cw_files_usage_done": "{size} in {items} Elementen.",
"cw_files_usage_capped": "Bei {items} Elementen gestoppt: {size} gezählt, der Ordner enthält mehr.",
"cw_files_usage_timeout": "Nach 3 Minuten gestoppt: {size} gezählt, der Rest wurde nicht gemessen.",
"cw_files_usage_cancelled": "Abgebrochen: {size} gezählt, der Rest wurde nicht gemessen.",
"cw_files_usage_again": "Erneut messen",
"cw_files_usage_busy": "Gerade wird eine andere Ordnergröße gemessen. Versuche es danach erneut.",
"cw_files_usage_skipped": "nicht gemessen",
"cw_files_usage_unreadable": "nicht lesbar",
};
export default de;
+37
View File
@@ -4773,6 +4773,43 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}",
"cw_files_game_ps4": "PS4 game folder",
"cw_files_game_version": "version {v}",
"cw_files_game_fw": "needs system software {v}",
"cw_files_search": "Search",
"cw_files_search_field": "Name or pattern",
"cw_files_search_go": "Search",
"cw_files_search_scope": "Where to search",
"cw_files_search_all": "All drives",
"cw_files_search_here": "This folder",
"cw_files_search_hint": "Finds files by name. A word matches anywhere in the name; use * and ? for a pattern (*.pkg).",
"cw_files_search_bad": "Quotes and backslashes cannot be searched for.",
"cw_files_search_unsupported": "PS5Upload on this console is too old to search. Update it to use search.",
"cw_files_search_no_index": "No index yet. The first search builds one: it reads every folder and can take a few minutes.",
"cw_files_search_building": "Building the index… {n} files so far.",
"cw_files_search_stopping": "Stopping the index…",
"cw_files_search_ready": "Index: {n} files.",
"cw_files_search_partial": "It was stopped early, so some files are missing.",
"cw_files_search_capped": "It is full (200,000 files), so some files are missing.",
"cw_files_search_rebuild": "Build again",
"cw_files_search_cancel": "Cancel",
"cw_files_search_waiting": "Searches for “{q}” when the index is ready.",
"cw_files_search_searching": "Searching…",
"cw_files_search_none": "Nothing matches “{q}”.",
"cw_files_search_found": "{n} found.",
"cw_files_search_more": "Showing the first {n}. There are more: try a longer name.",
"cw_files_search_results": "Search results",
"cw_files_usage": "Disk usage",
"cw_files_usage_open": "Open in Files",
"cw_files_usage_open_row": "Open {name} in Files",
"cw_files_usage_inside": "Look inside {name}",
"cw_files_usage_files_here": "Files in this folder",
"cw_files_usage_running": "Measuring… {at} of {n} folders, {items} items.",
"cw_files_usage_done": "{size} in {items} items.",
"cw_files_usage_capped": "Stopped at {items} items: {size} counted, the folder holds more.",
"cw_files_usage_timeout": "Stopped after 3 minutes: {size} counted, the rest was not measured.",
"cw_files_usage_cancelled": "Cancelled: {size} counted, the rest was not measured.",
"cw_files_usage_again": "Measure again",
"cw_files_usage_busy": "Another folder size is being measured. Try again when it ends.",
"cw_files_usage_skipped": "not measured",
"cw_files_usage_unreadable": "unreadable",
};
export default en;
+37
View File
@@ -4400,6 +4400,43 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}",
"cw_files_game_ps4": "Carpeta de juego de PS4",
"cw_files_game_version": "versión {v}",
"cw_files_game_fw": "requiere el software del sistema {v}",
"cw_files_search": "Buscar",
"cw_files_search_field": "Nombre o patrón",
"cw_files_search_go": "Buscar",
"cw_files_search_scope": "Dónde buscar",
"cw_files_search_all": "Todas las unidades",
"cw_files_search_here": "Esta carpeta",
"cw_files_search_hint": "Busca archivos por nombre. Una palabra coincide en cualquier parte del nombre; usa * y ? para un patrón (*.pkg).",
"cw_files_search_bad": "No se pueden buscar comillas ni barras invertidas.",
"cw_files_search_unsupported": "PS5Upload en esta consola es demasiado antiguo para buscar. Actualízalo para usar la búsqueda.",
"cw_files_search_no_index": "Aún no hay índice. La primera búsqueda crea uno: lee todas las carpetas y puede tardar unos minutos.",
"cw_files_search_building": "Creando el índice… {n} archivos hasta ahora.",
"cw_files_search_stopping": "Deteniendo el índice…",
"cw_files_search_ready": "Índice: {n} archivos.",
"cw_files_search_partial": "Se detuvo antes de tiempo, así que faltan algunos archivos.",
"cw_files_search_capped": "Está lleno (200.000 archivos), así que faltan algunos archivos.",
"cw_files_search_rebuild": "Crear de nuevo",
"cw_files_search_cancel": "Cancelar",
"cw_files_search_waiting": "Buscará «{q}» cuando el índice esté listo.",
"cw_files_search_searching": "Buscando…",
"cw_files_search_none": "Nada coincide con «{q}».",
"cw_files_search_found": "{n} encontrados.",
"cw_files_search_more": "Se muestran los primeros {n}. Hay más: prueba con un nombre más largo.",
"cw_files_search_results": "Resultados de la búsqueda",
"cw_files_usage": "Uso del disco",
"cw_files_usage_open": "Abrir en Archivos",
"cw_files_usage_open_row": "Abrir {name} en Archivos",
"cw_files_usage_inside": "Ver dentro de {name}",
"cw_files_usage_files_here": "Archivos de esta carpeta",
"cw_files_usage_running": "Midiendo… {at} de {n} carpetas, {items} elementos.",
"cw_files_usage_done": "{size} en {items} elementos.",
"cw_files_usage_capped": "Detenido en {items} elementos: {size} contados, la carpeta contiene más.",
"cw_files_usage_timeout": "Detenido tras 3 minutos: {size} contados, el resto no se midió.",
"cw_files_usage_cancelled": "Cancelado: {size} contados, el resto no se midió.",
"cw_files_usage_again": "Medir de nuevo",
"cw_files_usage_busy": "Se está midiendo el tamaño de otra carpeta. Vuelve a intentarlo cuando termine.",
"cw_files_usage_skipped": "sin medir",
"cw_files_usage_unreadable": "ilegible",
};
export default es;
+37
View File
@@ -4751,6 +4751,43 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
"cw_files_game_ps4": "پوشهٔ بازی PS4",
"cw_files_game_version": "نسخهٔ {v}",
"cw_files_game_fw": "به نرم‌افزار سیستم {v} نیاز دارد",
"cw_files_search": "جستجو",
"cw_files_search_field": "نام یا الگو",
"cw_files_search_go": "جستجو",
"cw_files_search_scope": "محل جستجو",
"cw_files_search_all": "همهٔ درایوها",
"cw_files_search_here": "همین پوشه",
"cw_files_search_hint": "فایل‌ها را بر اساس نام پیدا می‌کند. یک واژه با هر بخشی از نام جور می‌شود؛ برای الگو از * و ? استفاده کنید (*.pkg).",
"cw_files_search_bad": "نمی‌توان نقل‌قول و بک‌اسلش را جستجو کرد.",
"cw_files_search_unsupported": "PS5Upload روی این کنسول برای جستجو قدیمی است. برای جستجو آن را به‌روز کنید.",
"cw_files_search_no_index": "هنوز نمایه‌ای نیست. نخستین جستجو آن را می‌سازد: همهٔ پوشه‌ها را می‌خواند و ممکن است چند دقیقه طول بکشد.",
"cw_files_search_building": "در حال ساخت نمایه… تاکنون {n} فایل.",
"cw_files_search_stopping": "در حال توقف نمایه…",
"cw_files_search_ready": "نمایه: {n} فایل.",
"cw_files_search_partial": "زودتر متوقف شد، پس برخی فایل‌ها در آن نیستند.",
"cw_files_search_capped": "پر شده است (۲۰۰٬۰۰۰ فایل)، پس برخی فایل‌ها در آن نیستند.",
"cw_files_search_rebuild": "ساخت دوباره",
"cw_files_search_cancel": "لغو",
"cw_files_search_waiting": "وقتی نمایه آماده شد «{q}» را جستجو می‌کند.",
"cw_files_search_searching": "در حال جستجو…",
"cw_files_search_none": "چیزی با «{q}» جور نیست.",
"cw_files_search_found": "{n} مورد پیدا شد.",
"cw_files_search_more": "{n} مورد نخست نمایش داده می‌شود. موارد بیشتری هست: نام بلندتری امتحان کنید.",
"cw_files_search_results": "نتایج جستجو",
"cw_files_usage": "فضای اشغال‌شده",
"cw_files_usage_open": "باز کردن در فایل‌ها",
"cw_files_usage_open_row": "باز کردن {name} در فایل‌ها",
"cw_files_usage_inside": "دیدن درون {name}",
"cw_files_usage_files_here": "فایل‌های این پوشه",
"cw_files_usage_running": "در حال اندازه‌گیری… {at} از {n} پوشه، {items} مورد.",
"cw_files_usage_done": "{size} در {items} مورد.",
"cw_files_usage_capped": "در {items} مورد متوقف شد: {size} شمرده شد، پوشه بیشتر دارد.",
"cw_files_usage_timeout": "پس از ۳ دقیقه متوقف شد: {size} شمرده شد، بقیه اندازه‌گیری نشد.",
"cw_files_usage_cancelled": "لغو شد: {size} شمرده شد، بقیه اندازه‌گیری نشد.",
"cw_files_usage_again": "اندازه‌گیری دوباره",
"cw_files_usage_busy": "اندازهٔ پوشهٔ دیگری در حال اندازه‌گیری است. پس از پایان آن دوباره امتحان کنید.",
"cw_files_usage_skipped": "اندازه‌گیری نشده",
"cw_files_usage_unreadable": "خواندنی نیست",
};
export default fa;
+37
View File
@@ -4468,6 +4468,43 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}",
"cw_files_game_ps4": "Dossier de jeu PS4",
"cw_files_game_version": "version {v}",
"cw_files_game_fw": "nécessite le logiciel système {v}",
"cw_files_search": "Rechercher",
"cw_files_search_field": "Nom ou motif",
"cw_files_search_go": "Rechercher",
"cw_files_search_scope": "Où chercher",
"cw_files_search_all": "Tous les disques",
"cw_files_search_here": "Ce dossier",
"cw_files_search_hint": "Trouve des fichiers par leur nom. Un mot correspond n’importe où dans le nom ; utilisez * et ? pour un motif (*.pkg).",
"cw_files_search_bad": "Impossible de rechercher des guillemets ou des barres obliques inverses.",
"cw_files_search_unsupported": "PS5Upload sur cette console est trop ancien pour rechercher. Mettez-le à jour pour utiliser la recherche.",
"cw_files_search_no_index": "Pas encore d’index. La première recherche en crée un : elle lit chaque dossier et peut prendre quelques minutes.",
"cw_files_search_building": "Création de l’index… {n} fichiers jusqu’ici.",
"cw_files_search_stopping": "Arrêt de l’index…",
"cw_files_search_ready": "Index : {n} fichiers.",
"cw_files_search_partial": "Il a été arrêté avant la fin ; il manque donc des fichiers.",
"cw_files_search_capped": "Il est plein (200 000 fichiers) ; il manque donc des fichiers.",
"cw_files_search_rebuild": "Recréer",
"cw_files_search_cancel": "Annuler",
"cw_files_search_waiting": "Recherchera « {q} » quand l’index sera prêt.",
"cw_files_search_searching": "Recherche…",
"cw_files_search_none": "Rien ne correspond à « {q} ».",
"cw_files_search_found": "{n} trouvés.",
"cw_files_search_more": "Les {n} premiers sont affichés. Il y en a d’autres : essayez un nom plus long.",
"cw_files_search_results": "Résultats de la recherche",
"cw_files_usage": "Utilisation du disque",
"cw_files_usage_open": "Ouvrir dans Fichiers",
"cw_files_usage_open_row": "Ouvrir {name} dans Fichiers",
"cw_files_usage_inside": "Voir le contenu de {name}",
"cw_files_usage_files_here": "Fichiers de ce dossier",
"cw_files_usage_running": "Mesure… {at} sur {n} dossiers, {items} éléments.",
"cw_files_usage_done": "{size} en {items} éléments.",
"cw_files_usage_capped": "Arrêté à {items} éléments : {size} comptés, le dossier en contient davantage.",
"cw_files_usage_timeout": "Arrêté après 3 minutes : {size} comptés, le reste n’a pas été mesuré.",
"cw_files_usage_cancelled": "Annulé : {size} comptés, le reste n’a pas été mesuré.",
"cw_files_usage_again": "Mesurer à nouveau",
"cw_files_usage_busy": "La taille d’un autre dossier est en cours de mesure. Réessayez quand elle sera terminée.",
"cw_files_usage_skipped": "non mesuré",
"cw_files_usage_unreadable": "illisible",
};
export default fr;
+37
View File
@@ -4466,6 +4466,43 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
"cw_files_game_ps4": "PS4 गेम फ़ोल्डर",
"cw_files_game_version": "संस्करण {v}",
"cw_files_game_fw": "सिस्टम सॉफ़्टवेयर {v} चाहिए",
"cw_files_search": "खोजें",
"cw_files_search_field": "नाम या पैटर्न",
"cw_files_search_go": "खोजें",
"cw_files_search_scope": "कहाँ खोजें",
"cw_files_search_all": "सभी ड्राइव",
"cw_files_search_here": "यह फ़ोल्डर",
"cw_files_search_hint": "नाम से फ़ाइलें ढूँढता है। कोई शब्द नाम में कहीं भी मेल खाता है; पैटर्न के लिए * और ? का उपयोग करें (*.pkg)।",
"cw_files_search_bad": "उद्धरण चिह्न और बैकस्लैश नहीं खोजे जा सकते।",
"cw_files_search_unsupported": "इस कंसोल पर PS5Upload खोज के लिए बहुत पुराना है। खोज इस्तेमाल करने के लिए इसे अपडेट करें।",
"cw_files_search_no_index": "अभी कोई इंडेक्स नहीं है। पहली खोज इसे बनाती है: यह हर फ़ोल्डर पढ़ती है और कुछ मिनट लग सकते हैं।",
"cw_files_search_building": "इंडेक्स बन रहा है… अब तक {n} फ़ाइलें।",
"cw_files_search_stopping": "इंडेक्स रोका जा रहा है…",
"cw_files_search_ready": "इंडेक्स: {n} फ़ाइलें।",
"cw_files_search_partial": "इसे पहले ही रोक दिया गया, इसलिए कुछ फ़ाइलें नहीं हैं।",
"cw_files_search_capped": "यह भर गया है (200,000 फ़ाइलें), इसलिए कुछ फ़ाइलें नहीं हैं।",
"cw_files_search_rebuild": "फिर से बनाएँ",
"cw_files_search_cancel": "रद्द करें",
"cw_files_search_waiting": "इंडेक्स तैयार होने पर “{q}” खोजेगा।",
"cw_files_search_searching": "खोज रहा है…",
"cw_files_search_none": "“{q}” से कुछ मेल नहीं खाता।",
"cw_files_search_found": "{n} मिले।",
"cw_files_search_more": "पहले {n} दिखाए जा रहे हैं। और भी हैं: लंबा नाम आज़माएँ।",
"cw_files_search_results": "खोज परिणाम",
"cw_files_usage": "डिस्क उपयोग",
"cw_files_usage_open": "फ़ाइलें सेक्शन में खोलें",
"cw_files_usage_open_row": "{name} को फ़ाइलें सेक्शन में खोलें",
"cw_files_usage_inside": "{name} के अंदर देखें",
"cw_files_usage_files_here": "इस फ़ोल्डर की फ़ाइलें",
"cw_files_usage_running": "माप रहा है… {n} में से {at} फ़ोल्डर, {items} आइटम।",
"cw_files_usage_done": "{items} आइटम में {size}।",
"cw_files_usage_capped": "{items} आइटम पर रुका: {size} गिना गया, फ़ोल्डर में और है।",
"cw_files_usage_timeout": "3 मिनट बाद रुका: {size} गिना गया, बाकी नहीं मापा गया।",
"cw_files_usage_cancelled": "रद्द किया गया: {size} गिना गया, बाकी नहीं मापा गया।",
"cw_files_usage_again": "फिर से मापें",
"cw_files_usage_busy": "किसी दूसरे फ़ोल्डर का आकार मापा जा रहा है। उसके खत्म होने पर फिर कोशिश करें।",
"cw_files_usage_skipped": "नहीं मापा गया",
"cw_files_usage_unreadable": "पढ़ा नहीं जा सकता",
};
export default hi;
+37
View File
@@ -4765,6 +4765,43 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}",
"cw_files_game_ps4": "PS4-játékmappa",
"cw_files_game_version": "verzió: {v}",
"cw_files_game_fw": "rendszerszoftver szükséges: {v}",
"cw_files_search": "Keresés",
"cw_files_search_field": "Név vagy minta",
"cw_files_search_go": "Keresés",
"cw_files_search_scope": "Hol keressen",
"cw_files_search_all": "Minden meghajtó",
"cw_files_search_here": "Ez a mappa",
"cw_files_search_hint": "Fájlokat keres név alapján. Egy szó a név bármely részén egyezhet; mintához használd a * és ? jelet (*.pkg).",
"cw_files_search_bad": "Idézőjelre és fordított perjelre nem lehet keresni.",
"cw_files_search_unsupported": "A konzolon lévő PS5Upload túl régi a kereséshez. Frissítsd, hogy kereshess.",
"cw_files_search_no_index": "Még nincs index. Az első keresés létrehozza: minden mappát beolvas, és néhány percig tarthat.",
"cw_files_search_building": "Index készítése… eddig {n} fájl.",
"cw_files_search_stopping": "Index leállítása…",
"cw_files_search_ready": "Index: {n} fájl.",
"cw_files_search_partial": "Idő előtt leállt, ezért néhány fájl hiányzik.",
"cw_files_search_capped": "Megtelt (200 000 fájl), ezért néhány fájl hiányzik.",
"cw_files_search_rebuild": "Újraépítés",
"cw_files_search_cancel": "Mégse",
"cw_files_search_waiting": "Rákeres erre: „{q}”, amint kész az index.",
"cw_files_search_searching": "Keresés…",
"cw_files_search_none": "Semmi sem egyezik ezzel: „{q}”.",
"cw_files_search_found": "{n} találat.",
"cw_files_search_more": "Az első {n} látszik. Van több is: próbálj hosszabb nevet.",
"cw_files_search_results": "Keresési eredmények",
"cw_files_usage": "Lemezhasználat",
"cw_files_usage_open": "Megnyitás a Fájlokban",
"cw_files_usage_open_row": "{name} megnyitása a Fájlokban",
"cw_files_usage_inside": "Belenézés: {name}",
"cw_files_usage_files_here": "A mappa fájljai",
"cw_files_usage_running": "Mérés… {at}/{n} mappa, {items} elem.",
"cw_files_usage_done": "{size}, {items} elemben.",
"cw_files_usage_capped": "{items} elemnél leállt: {size} megszámolva, a mappában több van.",
"cw_files_usage_timeout": "3 perc után leállt: {size} megszámolva, a többit nem mértük.",
"cw_files_usage_cancelled": "Megszakítva: {size} megszámolva, a többit nem mértük.",
"cw_files_usage_again": "Mérés újra",
"cw_files_usage_busy": "Most egy másik mappa mérete mérődik. Próbáld újra, ha végzett.",
"cw_files_usage_skipped": "nincs mérve",
"cw_files_usage_unreadable": "nem olvasható",
};
export default hu;
+37
View File
@@ -4459,6 +4459,43 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}",
"cw_files_game_ps4": "Folder game PS4",
"cw_files_game_version": "versi {v}",
"cw_files_game_fw": "perlu perangkat lunak sistem {v}",
"cw_files_search": "Cari",
"cw_files_search_field": "Nama atau pola",
"cw_files_search_go": "Cari",
"cw_files_search_scope": "Tempat mencari",
"cw_files_search_all": "Semua drive",
"cw_files_search_here": "Folder ini",
"cw_files_search_hint": "Mencari file menurut nama. Sebuah kata cocok di bagian mana pun dari nama; gunakan * dan ? untuk pola (*.pkg).",
"cw_files_search_bad": "Tanda kutip dan garis miring terbalik tidak dapat dicari.",
"cw_files_search_unsupported": "PS5Upload di konsol ini terlalu lama untuk mencari. Perbarui untuk memakai pencarian.",
"cw_files_search_no_index": "Belum ada indeks. Pencarian pertama membuatnya: membaca setiap folder dan bisa memakan beberapa menit.",
"cw_files_search_building": "Membuat indeks… {n} file sejauh ini.",
"cw_files_search_stopping": "Menghentikan indeks…",
"cw_files_search_ready": "Indeks: {n} file.",
"cw_files_search_partial": "Indeks dihentikan lebih awal, jadi beberapa file tidak ada.",
"cw_files_search_capped": "Indeks penuh (200.000 file), jadi beberapa file tidak ada.",
"cw_files_search_rebuild": "Buat ulang",
"cw_files_search_cancel": "Batal",
"cw_files_search_waiting": "Akan mencari “{q}” saat indeks siap.",
"cw_files_search_searching": "Mencari…",
"cw_files_search_none": "Tidak ada yang cocok dengan “{q}”.",
"cw_files_search_found": "{n} ditemukan.",
"cw_files_search_more": "Menampilkan {n} pertama. Masih ada lagi: coba nama yang lebih panjang.",
"cw_files_search_results": "Hasil pencarian",
"cw_files_usage": "Penggunaan disk",
"cw_files_usage_open": "Buka di File",
"cw_files_usage_open_row": "Buka {name} di File",
"cw_files_usage_inside": "Lihat isi {name}",
"cw_files_usage_files_here": "File di folder ini",
"cw_files_usage_running": "Mengukur… {at} dari {n} folder, {items} item.",
"cw_files_usage_done": "{size} dalam {items} item.",
"cw_files_usage_capped": "Berhenti di {items} item: {size} terhitung, folder berisi lebih banyak.",
"cw_files_usage_timeout": "Berhenti setelah 3 menit: {size} terhitung, sisanya tidak diukur.",
"cw_files_usage_cancelled": "Dibatalkan: {size} terhitung, sisanya tidak diukur.",
"cw_files_usage_again": "Ukur lagi",
"cw_files_usage_busy": "Ukuran folder lain sedang diukur. Coba lagi setelah selesai.",
"cw_files_usage_skipped": "tidak diukur",
"cw_files_usage_unreadable": "tidak terbaca",
};
export default id;
+37
View File
@@ -4400,6 +4400,43 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}",
"cw_files_game_ps4": "Cartella di gioco PS4",
"cw_files_game_version": "versione {v}",
"cw_files_game_fw": "richiede il software di sistema {v}",
"cw_files_search": "Cerca",
"cw_files_search_field": "Nome o modello",
"cw_files_search_go": "Cerca",
"cw_files_search_scope": "Dove cercare",
"cw_files_search_all": "Tutte le unità",
"cw_files_search_here": "Questa cartella",
"cw_files_search_hint": "Trova i file per nome. Una parola corrisponde in qualsiasi punto del nome; usa * e ? per un modello (*.pkg).",
"cw_files_search_bad": "Non si possono cercare virgolette o barre rovesciate.",
"cw_files_search_unsupported": "PS5Upload su questa console è troppo vecchio per cercare. Aggiornalo per usare la ricerca.",
"cw_files_search_no_index": "Nessun indice ancora. La prima ricerca ne crea uno: legge ogni cartella e può richiedere alcuni minuti.",
"cw_files_search_building": "Creazione dell’indice… {n} file finora.",
"cw_files_search_stopping": "Arresto dell’indice…",
"cw_files_search_ready": "Indice: {n} file.",
"cw_files_search_partial": "È stato fermato prima della fine, quindi mancano alcuni file.",
"cw_files_search_capped": "È pieno (200.000 file), quindi mancano alcuni file.",
"cw_files_search_rebuild": "Ricrea",
"cw_files_search_cancel": "Annulla",
"cw_files_search_waiting": "Cercherà «{q}» quando l’indice sarà pronto.",
"cw_files_search_searching": "Ricerca…",
"cw_files_search_none": "Nessun risultato per «{q}».",
"cw_files_search_found": "{n} trovati.",
"cw_files_search_more": "Vengono mostrati i primi {n}. Ce ne sono altri: prova un nome più lungo.",
"cw_files_search_results": "Risultati della ricerca",
"cw_files_usage": "Utilizzo del disco",
"cw_files_usage_open": "Apri in File",
"cw_files_usage_open_row": "Apri {name} in File",
"cw_files_usage_inside": "Guarda dentro {name}",
"cw_files_usage_files_here": "File in questa cartella",
"cw_files_usage_running": "Misurazione… {at} di {n} cartelle, {items} elementi.",
"cw_files_usage_done": "{size} in {items} elementi.",
"cw_files_usage_capped": "Fermato a {items} elementi: {size} contati, la cartella ne contiene di più.",
"cw_files_usage_timeout": "Fermato dopo 3 minuti: {size} contati, il resto non è stato misurato.",
"cw_files_usage_cancelled": "Annullato: {size} contati, il resto non è stato misurato.",
"cw_files_usage_again": "Misura di nuovo",
"cw_files_usage_busy": "È in corso la misura di un’altra cartella. Riprova quando è finita.",
"cw_files_usage_skipped": "non misurato",
"cw_files_usage_unreadable": "illeggibile",
};
export default it;
+37
View File
@@ -4471,6 +4471,43 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
"cw_files_game_ps4": "PS4 ゲームフォルダー",
"cw_files_game_version": "バージョン {v}",
"cw_files_game_fw": "システムソフトウェア {v} が必要",
"cw_files_search": "検索",
"cw_files_search_field": "名前またはパターン",
"cw_files_search_go": "検索",
"cw_files_search_scope": "検索する場所",
"cw_files_search_all": "すべてのドライブ",
"cw_files_search_here": "このフォルダー",
"cw_files_search_hint": "名前でファイルを探します。単語は名前のどこにあっても一致します。パターンには * と ? を使います(*.pkg)。",
"cw_files_search_bad": "引用符とバックスラッシュは検索できません。",
"cw_files_search_unsupported": "この本体の PS5Upload は古いため検索できません。検索を使うには更新してください。",
"cw_files_search_no_index": "まだインデックスがありません。最初の検索で作成します。すべてのフォルダーを読むため数分かかることがあります。",
"cw_files_search_building": "インデックスを作成中… これまでに {n} ファイル。",
"cw_files_search_stopping": "インデックスを停止中…",
"cw_files_search_ready": "インデックス: {n} ファイル。",
"cw_files_search_partial": "途中で停止したため、一部のファイルがありません。",
"cw_files_search_capped": "上限(200,000 ファイル)に達したため、一部のファイルがありません。",
"cw_files_search_rebuild": "作り直す",
"cw_files_search_cancel": "キャンセル",
"cw_files_search_waiting": "インデックスの準備ができたら「{q}」を検索します。",
"cw_files_search_searching": "検索中…",
"cw_files_search_none": "「{q}」に一致するものはありません。",
"cw_files_search_found": "{n} 件見つかりました。",
"cw_files_search_more": "最初の {n} 件を表示しています。さらにあります。もっと長い名前で試してください。",
"cw_files_search_results": "検索結果",
"cw_files_usage": "ディスク使用量",
"cw_files_usage_open": "ファイルで開く",
"cw_files_usage_open_row": "{name} をファイルで開く",
"cw_files_usage_inside": "{name} の中を見る",
"cw_files_usage_files_here": "このフォルダー内のファイル",
"cw_files_usage_running": "計測中… {n} フォルダー中 {at}、{items} 項目。",
"cw_files_usage_done": "{items} 項目で {size}。",
"cw_files_usage_capped": "{items} 項目で停止: {size} を計上、フォルダーにはさらにあります。",
"cw_files_usage_timeout": "3 分で停止: {size} を計上、残りは計測していません。",
"cw_files_usage_cancelled": "キャンセルしました: {size} を計上、残りは計測していません。",
"cw_files_usage_again": "もう一度計測",
"cw_files_usage_busy": "別のフォルダーのサイズを計測中です。終わってから再試行してください。",
"cw_files_usage_skipped": "未計測",
"cw_files_usage_unreadable": "読み取れません",
};
export default ja;
+37
View File
@@ -4469,6 +4469,43 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
"cw_files_game_ps4": "PS4 게임 폴더",
"cw_files_game_version": "버전 {v}",
"cw_files_game_fw": "시스템 소프트웨어 {v} 필요",
"cw_files_search": "검색",
"cw_files_search_field": "이름 또는 패턴",
"cw_files_search_go": "검색",
"cw_files_search_scope": "검색 위치",
"cw_files_search_all": "모든 드라이브",
"cw_files_search_here": "이 폴더",
"cw_files_search_hint": "이름으로 파일을 찾습니다. 단어는 이름의 어느 부분과도 일치하며, 패턴에는 *와 ?를 사용합니다(*.pkg).",
"cw_files_search_bad": "따옴표와 백슬래시는 검색할 수 없습니다.",
"cw_files_search_unsupported": "이 콘솔의 PS5Upload가 오래되어 검색할 수 없습니다. 검색을 사용하려면 업데이트하세요.",
"cw_files_search_no_index": "아직 색인이 없습니다. 첫 검색에서 색인을 만듭니다. 모든 폴더를 읽으므로 몇 분 걸릴 수 있습니다.",
"cw_files_search_building": "색인 생성 중… 지금까지 {n}개 파일.",
"cw_files_search_stopping": "색인 중지 중…",
"cw_files_search_ready": "색인: {n}개 파일.",
"cw_files_search_partial": "일찍 중지되어 일부 파일이 빠져 있습니다.",
"cw_files_search_capped": "가득 차서(200,000개 파일) 일부 파일이 빠져 있습니다.",
"cw_files_search_rebuild": "다시 만들기",
"cw_files_search_cancel": "취소",
"cw_files_search_waiting": "색인이 준비되면 “{q}”을(를) 검색합니다.",
"cw_files_search_searching": "검색 중…",
"cw_files_search_none": "“{q}”과(와) 일치하는 항목이 없습니다.",
"cw_files_search_found": "{n}개 찾음.",
"cw_files_search_more": "처음 {n}개를 표시합니다. 더 있습니다. 더 긴 이름으로 시도하세요.",
"cw_files_search_results": "검색 결과",
"cw_files_usage": "디스크 사용량",
"cw_files_usage_open": "파일에서 열기",
"cw_files_usage_open_row": "{name}을(를) 파일에서 열기",
"cw_files_usage_inside": "{name} 안 보기",
"cw_files_usage_files_here": "이 폴더의 파일",
"cw_files_usage_running": "측정 중… 폴더 {n}개 중 {at}개, 항목 {items}개.",
"cw_files_usage_done": "항목 {items}개에 {size}.",
"cw_files_usage_capped": "항목 {items}개에서 중지: {size} 집계, 폴더에 더 있습니다.",
"cw_files_usage_timeout": "3분 후 중지: {size} 집계, 나머지는 측정하지 않았습니다.",
"cw_files_usage_cancelled": "취소됨: {size} 집계, 나머지는 측정하지 않았습니다.",
"cw_files_usage_again": "다시 측정",
"cw_files_usage_busy": "다른 폴더 크기를 측정하는 중입니다. 끝난 뒤 다시 시도하세요.",
"cw_files_usage_skipped": "측정 안 됨",
"cw_files_usage_unreadable": "읽을 수 없음",
};
export default ko;
+37
View File
@@ -4705,6 +4705,43 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}",
"cw_files_game_ps4": "Folder gry PS4",
"cw_files_game_version": "wersja {v}",
"cw_files_game_fw": "wymaga oprogramowania systemowego {v}",
"cw_files_search": "Szukaj",
"cw_files_search_field": "Nazwa lub wzorzec",
"cw_files_search_go": "Szukaj",
"cw_files_search_scope": "Gdzie szukać",
"cw_files_search_all": "Wszystkie dyski",
"cw_files_search_here": "Ten folder",
"cw_files_search_hint": "Znajduje pliki po nazwie. Słowo pasuje w dowolnym miejscu nazwy; użyj * i ? jako wzorca (*.pkg).",
"cw_files_search_bad": "Nie można szukać cudzysłowów ani ukośników wstecznych.",
"cw_files_search_unsupported": "PS5Upload na tej konsoli jest za stary, by szukać. Zaktualizuj go, aby korzystać z wyszukiwania.",
"cw_files_search_no_index": "Nie ma jeszcze indeksu. Pierwsze wyszukiwanie go zbuduje: odczyta każdy folder i może potrwać kilka minut.",
"cw_files_search_building": "Budowanie indeksu… dotąd {n} plików.",
"cw_files_search_stopping": "Zatrzymywanie indeksu…",
"cw_files_search_ready": "Indeks: {n} plików.",
"cw_files_search_partial": "Zatrzymano go przed końcem, więc brakuje niektórych plików.",
"cw_files_search_capped": "Jest pełny (200 000 plików), więc brakuje niektórych plików.",
"cw_files_search_rebuild": "Zbuduj ponownie",
"cw_files_search_cancel": "Anuluj",
"cw_files_search_waiting": "Wyszuka „{q}”, gdy indeks będzie gotowy.",
"cw_files_search_searching": "Wyszukiwanie…",
"cw_files_search_none": "Nic nie pasuje do „{q}”.",
"cw_files_search_found": "Znaleziono: {n}.",
"cw_files_search_more": "Pokazano pierwsze {n}. Jest ich więcej: spróbuj dłuższej nazwy.",
"cw_files_search_results": "Wyniki wyszukiwania",
"cw_files_usage": "Zajęte miejsce",
"cw_files_usage_open": "Otwórz w Plikach",
"cw_files_usage_open_row": "Otwórz {name} w Plikach",
"cw_files_usage_inside": "Zajrzyj do {name}",
"cw_files_usage_files_here": "Pliki w tym folderze",
"cw_files_usage_running": "Mierzenie… {at} z {n} folderów, {items} elementów.",
"cw_files_usage_done": "{size} w {items} elementach.",
"cw_files_usage_capped": "Zatrzymano przy {items} elementach: policzono {size}, folder zawiera więcej.",
"cw_files_usage_timeout": "Zatrzymano po 3 minutach: policzono {size}, reszty nie zmierzono.",
"cw_files_usage_cancelled": "Anulowano: policzono {size}, reszty nie zmierzono.",
"cw_files_usage_again": "Zmierz ponownie",
"cw_files_usage_busy": "Trwa mierzenie rozmiaru innego folderu. Spróbuj ponownie, gdy się skończy.",
"cw_files_usage_skipped": "nie zmierzono",
"cw_files_usage_unreadable": "nieczytelny",
};
export default pl;
+37
View File
@@ -4467,6 +4467,43 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}",
"cw_files_game_ps4": "Pasta de jogo de PS4",
"cw_files_game_version": "versão {v}",
"cw_files_game_fw": "requer o software do sistema {v}",
"cw_files_search": "Pesquisar",
"cw_files_search_field": "Nome ou padrão",
"cw_files_search_go": "Pesquisar",
"cw_files_search_scope": "Onde pesquisar",
"cw_files_search_all": "Todas as unidades",
"cw_files_search_here": "Esta pasta",
"cw_files_search_hint": "Encontra arquivos pelo nome. Uma palavra corresponde em qualquer parte do nome; use * e ? para um padrão (*.pkg).",
"cw_files_search_bad": "Não é possível pesquisar aspas nem barras invertidas.",
"cw_files_search_unsupported": "O PS5Upload deste console é antigo demais para pesquisar. Atualize-o para usar a pesquisa.",
"cw_files_search_no_index": "Ainda não há índice. A primeira pesquisa cria um: ela lê todas as pastas e pode levar alguns minutos.",
"cw_files_search_building": "Criando o índice… {n} arquivos até agora.",
"cw_files_search_stopping": "Parando o índice…",
"cw_files_search_ready": "Índice: {n} arquivos.",
"cw_files_search_partial": "Ele foi interrompido antes do fim, então faltam alguns arquivos.",
"cw_files_search_capped": "Ele está cheio (200.000 arquivos), então faltam alguns arquivos.",
"cw_files_search_rebuild": "Criar de novo",
"cw_files_search_cancel": "Cancelar",
"cw_files_search_waiting": "Vai pesquisar “{q}” quando o índice estiver pronto.",
"cw_files_search_searching": "Pesquisando…",
"cw_files_search_none": "Nada corresponde a “{q}”.",
"cw_files_search_found": "{n} encontrados.",
"cw_files_search_more": "Mostrando os primeiros {n}. Há mais: tente um nome mais longo.",
"cw_files_search_results": "Resultados da pesquisa",
"cw_files_usage": "Uso do disco",
"cw_files_usage_open": "Abrir em Arquivos",
"cw_files_usage_open_row": "Abrir {name} em Arquivos",
"cw_files_usage_inside": "Ver dentro de {name}",
"cw_files_usage_files_here": "Arquivos nesta pasta",
"cw_files_usage_running": "Medindo… {at} de {n} pastas, {items} itens.",
"cw_files_usage_done": "{size} em {items} itens.",
"cw_files_usage_capped": "Parou em {items} itens: {size} contados, a pasta tem mais.",
"cw_files_usage_timeout": "Parou após 3 minutos: {size} contados, o resto não foi medido.",
"cw_files_usage_cancelled": "Cancelado: {size} contados, o resto não foi medido.",
"cw_files_usage_again": "Medir de novo",
"cw_files_usage_busy": "O tamanho de outra pasta está sendo medido. Tente de novo quando terminar.",
"cw_files_usage_skipped": "não medido",
"cw_files_usage_unreadable": "ilegível",
};
export default pt_BR;
+37
View File
@@ -4400,6 +4400,43 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
"cw_files_game_ps4": "Папка игры PS4",
"cw_files_game_version": "версия {v}",
"cw_files_game_fw": "требуется системное ПО {v}",
"cw_files_search": "Поиск",
"cw_files_search_field": "Имя или шаблон",
"cw_files_search_go": "Найти",
"cw_files_search_scope": "Где искать",
"cw_files_search_all": "Все диски",
"cw_files_search_here": "Эта папка",
"cw_files_search_hint": "Ищет файлы по имени. Слово совпадает в любом месте имени; для шаблона используйте * и ? (*.pkg).",
"cw_files_search_bad": "Кавычки и обратную косую черту искать нельзя.",
"cw_files_search_unsupported": "PS5Upload на этой консоли слишком старый для поиска. Обновите его, чтобы искать.",
"cw_files_search_no_index": "Индекса пока нет. Первый поиск создаст его: он читает каждую папку и может занять несколько минут.",
"cw_files_search_building": "Создание индекса… пока {n} файлов.",
"cw_files_search_stopping": "Остановка индекса…",
"cw_files_search_ready": "Индекс: {n} файлов.",
"cw_files_search_partial": "Он был остановлен раньше времени, поэтому некоторых файлов нет.",
"cw_files_search_capped": "Он заполнен (200 000 файлов), поэтому некоторых файлов нет.",
"cw_files_search_rebuild": "Создать заново",
"cw_files_search_cancel": "Отмена",
"cw_files_search_waiting": "Найдёт «{q}», когда индекс будет готов.",
"cw_files_search_searching": "Поиск…",
"cw_files_search_none": "Ничего не найдено по «{q}».",
"cw_files_search_found": "Найдено: {n}.",
"cw_files_search_more": "Показаны первые {n}. Есть ещё: попробуйте имя подлиннее.",
"cw_files_search_results": "Результаты поиска",
"cw_files_usage": "Занятое место",
"cw_files_usage_open": "Открыть в «Файлах»",
"cw_files_usage_open_row": "Открыть {name} в «Файлах»",
"cw_files_usage_inside": "Заглянуть в {name}",
"cw_files_usage_files_here": "Файлы в этой папке",
"cw_files_usage_running": "Измерение… {at} из {n} папок, {items} элементов.",
"cw_files_usage_done": "{size} в {items} элементах.",
"cw_files_usage_capped": "Остановлено на {items} элементах: учтено {size}, в папке есть ещё.",
"cw_files_usage_timeout": "Остановлено через 3 минуты: учтено {size}, остальное не измерено.",
"cw_files_usage_cancelled": "Отменено: учтено {size}, остальное не измерено.",
"cw_files_usage_again": "Измерить заново",
"cw_files_usage_busy": "Сейчас измеряется размер другой папки. Повторите, когда это закончится.",
"cw_files_usage_skipped": "не измерено",
"cw_files_usage_unreadable": "не читается",
};
export default ru;
+37
View File
@@ -4459,6 +4459,43 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
"cw_files_game_ps4": "โฟลเดอร์เกม PS4",
"cw_files_game_version": "เวอร์ชัน {v}",
"cw_files_game_fw": "ต้องใช้ซอฟต์แวร์ระบบ {v}",
"cw_files_search": "ค้นหา",
"cw_files_search_field": "ชื่อหรือรูปแบบ",
"cw_files_search_go": "ค้นหา",
"cw_files_search_scope": "ค้นหาที่ใด",
"cw_files_search_all": "ทุกไดรฟ์",
"cw_files_search_here": "โฟลเดอร์นี้",
"cw_files_search_hint": "ค้นหาไฟล์ตามชื่อ คำจะตรงกับส่วนใดก็ได้ของชื่อ ใช้ * และ ? เป็นรูปแบบ (*.pkg)",
"cw_files_search_bad": "ค้นหาเครื่องหมายคำพูดและแบ็กสแลชไม่ได้",
"cw_files_search_unsupported": "PS5Upload บนเครื่องนี้เก่าเกินกว่าจะค้นหาได้ อัปเดตเพื่อใช้การค้นหา",
"cw_files_search_no_index": "ยังไม่มีดัชนี การค้นหาครั้งแรกจะสร้างดัชนี โดยอ่านทุกโฟลเดอร์และอาจใช้เวลาสองสามนาที",
"cw_files_search_building": "กำลังสร้างดัชนี… ตอนนี้ {n} ไฟล์",
"cw_files_search_stopping": "กำลังหยุดดัชนี…",
"cw_files_search_ready": "ดัชนี: {n} ไฟล์",
"cw_files_search_partial": "ดัชนีถูกหยุดก่อนเสร็จ จึงขาดบางไฟล์",
"cw_files_search_capped": "ดัชนีเต็ม (200,000 ไฟล์) จึงขาดบางไฟล์",
"cw_files_search_rebuild": "สร้างใหม่",
"cw_files_search_cancel": "ยกเลิก",
"cw_files_search_waiting": "จะค้นหา “{q}” เมื่อดัชนีพร้อม",
"cw_files_search_searching": "กำลังค้นหา…",
"cw_files_search_none": "ไม่มีสิ่งที่ตรงกับ “{q}”",
"cw_files_search_found": "พบ {n} รายการ",
"cw_files_search_more": "แสดง {n} รายการแรก ยังมีอีก: ลองใช้ชื่อที่ยาวขึ้น",
"cw_files_search_results": "ผลการค้นหา",
"cw_files_usage": "การใช้ดิสก์",
"cw_files_usage_open": "เปิดในไฟล์",
"cw_files_usage_open_row": "เปิด {name} ในไฟล์",
"cw_files_usage_inside": "ดูข้างใน {name}",
"cw_files_usage_files_here": "ไฟล์ในโฟลเดอร์นี้",
"cw_files_usage_running": "กำลังวัด… {at} จาก {n} โฟลเดอร์, {items} รายการ",
"cw_files_usage_done": "{size} ใน {items} รายการ",
"cw_files_usage_capped": "หยุดที่ {items} รายการ: นับได้ {size} โฟลเดอร์ยังมีอีก",
"cw_files_usage_timeout": "หยุดหลัง 3 นาที: นับได้ {size} ส่วนที่เหลือไม่ได้วัด",
"cw_files_usage_cancelled": "ยกเลิกแล้ว: นับได้ {size} ส่วนที่เหลือไม่ได้วัด",
"cw_files_usage_again": "วัดอีกครั้ง",
"cw_files_usage_busy": "กำลังวัดขนาดโฟลเดอร์อื่นอยู่ ลองอีกครั้งเมื่อเสร็จ",
"cw_files_usage_skipped": "ไม่ได้วัด",
"cw_files_usage_unreadable": "อ่านไม่ได้",
};
export default th;
+37
View File
@@ -4457,6 +4457,43 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}",
"cw_files_game_ps4": "PS4 oyun klasörü",
"cw_files_game_version": "sürüm {v}",
"cw_files_game_fw": "sistem yazılımı {v} gerekir",
"cw_files_search": "Ara",
"cw_files_search_field": "Ad veya desen",
"cw_files_search_go": "Ara",
"cw_files_search_scope": "Nerede aranacak",
"cw_files_search_all": "Tüm sürücüler",
"cw_files_search_here": "Bu klasör",
"cw_files_search_hint": "Dosyaları adına göre bulur. Bir sözcük adın herhangi bir yerinde eşleşir; desen için * ve ? kullanın (*.pkg).",
"cw_files_search_bad": "Tırnak işaretleri ve ters eğik çizgiler aranamaz.",
"cw_files_search_unsupported": "Bu konsoldaki PS5Upload arama için çok eski. Aramayı kullanmak için güncelleyin.",
"cw_files_search_no_index": "Henüz dizin yok. İlk arama bir dizin oluşturur: her klasörü okur ve birkaç dakika sürebilir.",
"cw_files_search_building": "Dizin oluşturuluyor… şimdiye dek {n} dosya.",
"cw_files_search_stopping": "Dizin durduruluyor…",
"cw_files_search_ready": "Dizin: {n} dosya.",
"cw_files_search_partial": "Erken durduruldu, bu yüzden bazı dosyalar eksik.",
"cw_files_search_capped": "Dolu (200.000 dosya), bu yüzden bazı dosyalar eksik.",
"cw_files_search_rebuild": "Yeniden oluştur",
"cw_files_search_cancel": "İptal",
"cw_files_search_waiting": "Dizin hazır olunca “{q}” aranacak.",
"cw_files_search_searching": "Aranıyor…",
"cw_files_search_none": "“{q}” ile eşleşen bir şey yok.",
"cw_files_search_found": "{n} bulundu.",
"cw_files_search_more": "İlk {n} gösteriliyor. Daha fazlası var: daha uzun bir ad deneyin.",
"cw_files_search_results": "Arama sonuçları",
"cw_files_usage": "Disk kullanımı",
"cw_files_usage_open": "Dosyalar’da aç",
"cw_files_usage_open_row": "{name} öğesini Dosyalar’da aç",
"cw_files_usage_inside": "{name} içine bak",
"cw_files_usage_files_here": "Bu klasördeki dosyalar",
"cw_files_usage_running": "Ölçülüyor… {n} klasörden {at}, {items} öğe.",
"cw_files_usage_done": "{items} öğede {size}.",
"cw_files_usage_capped": "{items} öğede durdu: {size} sayıldı, klasörde daha fazlası var.",
"cw_files_usage_timeout": "3 dakika sonra durdu: {size} sayıldı, kalanı ölçülmedi.",
"cw_files_usage_cancelled": "İptal edildi: {size} sayıldı, kalanı ölçülmedi.",
"cw_files_usage_again": "Yeniden ölç",
"cw_files_usage_busy": "Başka bir klasörün boyutu ölçülüyor. Bitince yeniden deneyin.",
"cw_files_usage_skipped": "ölçülmedi",
"cw_files_usage_unreadable": "okunamıyor",
};
export default tr;
+37
View File
@@ -4452,6 +4452,43 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}",
"cw_files_game_ps4": "Thư mục game PS4",
"cw_files_game_version": "phiên bản {v}",
"cw_files_game_fw": "cần phần mềm hệ thống {v}",
"cw_files_search": "Tìm kiếm",
"cw_files_search_field": "Tên hoặc mẫu",
"cw_files_search_go": "Tìm",
"cw_files_search_scope": "Tìm ở đâu",
"cw_files_search_all": "Tất cả ổ đĩa",
"cw_files_search_here": "Thư mục này",
"cw_files_search_hint": "Tìm tệp theo tên. Một từ khớp ở bất kỳ đâu trong tên; dùng * và ? cho mẫu (*.pkg).",
"cw_files_search_bad": "Không thể tìm dấu ngoặc kép và dấu gạch chéo ngược.",
"cw_files_search_unsupported": "PS5Upload trên máy này quá cũ để tìm kiếm. Hãy cập nhật để dùng tìm kiếm.",
"cw_files_search_no_index": "Chưa có chỉ mục. Lần tìm đầu tiên sẽ tạo: nó đọc mọi thư mục và có thể mất vài phút.",
"cw_files_search_building": "Đang tạo chỉ mục… đến giờ {n} tệp.",
"cw_files_search_stopping": "Đang dừng chỉ mục…",
"cw_files_search_ready": "Chỉ mục: {n} tệp.",
"cw_files_search_partial": "Chỉ mục bị dừng sớm nên thiếu một số tệp.",
"cw_files_search_capped": "Chỉ mục đã đầy (200.000 tệp) nên thiếu một số tệp.",
"cw_files_search_rebuild": "Tạo lại",
"cw_files_search_cancel": "Hủy",
"cw_files_search_waiting": "Sẽ tìm “{q}” khi chỉ mục sẵn sàng.",
"cw_files_search_searching": "Đang tìm…",
"cw_files_search_none": "Không có gì khớp với “{q}”.",
"cw_files_search_found": "Tìm thấy {n}.",
"cw_files_search_more": "Đang hiện {n} kết quả đầu. Còn nữa: hãy thử tên dài hơn.",
"cw_files_search_results": "Kết quả tìm kiếm",
"cw_files_usage": "Dung lượng đã dùng",
"cw_files_usage_open": "Mở trong Tệp",
"cw_files_usage_open_row": "Mở {name} trong Tệp",
"cw_files_usage_inside": "Xem bên trong {name}",
"cw_files_usage_files_here": "Tệp trong thư mục này",
"cw_files_usage_running": "Đang đo… {at}/{n} thư mục, {items} mục.",
"cw_files_usage_done": "{size} trong {items} mục.",
"cw_files_usage_capped": "Dừng ở {items} mục: đã đếm {size}, thư mục còn nhiều hơn.",
"cw_files_usage_timeout": "Dừng sau 3 phút: đã đếm {size}, phần còn lại chưa đo.",
"cw_files_usage_cancelled": "Đã hủy: đã đếm {size}, phần còn lại chưa đo.",
"cw_files_usage_again": "Đo lại",
"cw_files_usage_busy": "Đang đo kích thước một thư mục khác. Hãy thử lại khi xong.",
"cw_files_usage_skipped": "chưa đo",
"cw_files_usage_unreadable": "không đọc được",
};
export default vi;
+37
View File
@@ -4406,6 +4406,43 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
"cw_files_game_ps4": "PS4 游戏文件夹",
"cw_files_game_version": "版本 {v}",
"cw_files_game_fw": "需要系统软件 {v}",
"cw_files_search": "搜索",
"cw_files_search_field": "名称或模式",
"cw_files_search_go": "搜索",
"cw_files_search_scope": "搜索范围",
"cw_files_search_all": "所有驱动器",
"cw_files_search_here": "此文件夹",
"cw_files_search_hint": "按名称查找文件。单词可匹配名称中的任意位置;模式可使用 * 和 ?(*.pkg)。",
"cw_files_search_bad": "无法搜索引号和反斜杠。",
"cw_files_search_unsupported": "此主机上的 PS5Upload 版本过旧,无法搜索。请更新后使用搜索。",
"cw_files_search_no_index": "还没有索引。首次搜索会建立索引:需要读取每个文件夹,可能需要几分钟。",
"cw_files_search_building": "正在建立索引… 目前 {n} 个文件。",
"cw_files_search_stopping": "正在停止索引…",
"cw_files_search_ready": "索引:{n} 个文件。",
"cw_files_search_partial": "索引提前停止,因此缺少部分文件。",
"cw_files_search_capped": "索引已满(200,000 个文件),因此缺少部分文件。",
"cw_files_search_rebuild": "重新建立",
"cw_files_search_cancel": "取消",
"cw_files_search_waiting": "索引就绪后将搜索“{q}”。",
"cw_files_search_searching": "正在搜索…",
"cw_files_search_none": "没有与“{q}”匹配的内容。",
"cw_files_search_found": "找到 {n} 个。",
"cw_files_search_more": "显示前 {n} 个。还有更多:请尝试更长的名称。",
"cw_files_search_results": "搜索结果",
"cw_files_usage": "磁盘用量",
"cw_files_usage_open": "在“文件”中打开",
"cw_files_usage_open_row": "在“文件”中打开 {name}",
"cw_files_usage_inside": "查看 {name} 内部",
"cw_files_usage_files_here": "此文件夹中的文件",
"cw_files_usage_running": "正在测量… {n} 个文件夹中的第 {at} 个,{items} 项。",
"cw_files_usage_done": "{items} 项共 {size}。",
"cw_files_usage_capped": "在 {items} 项处停止:已计 {size},文件夹中还有更多。",
"cw_files_usage_timeout": "3 分钟后停止:已计 {size},其余未测量。",
"cw_files_usage_cancelled": "已取消:已计 {size},其余未测量。",
"cw_files_usage_again": "重新测量",
"cw_files_usage_busy": "正在测量另一个文件夹的大小。请在其结束后重试。",
"cw_files_usage_skipped": "未测量",
"cw_files_usage_unreadable": "无法读取",
};
export default zh_CN;
+37
View File
@@ -4400,6 +4400,43 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
"cw_files_game_ps4": "PS4 遊戲資料夾",
"cw_files_game_version": "版本 {v}",
"cw_files_game_fw": "需要系統軟體 {v}",
"cw_files_search": "搜尋",
"cw_files_search_field": "名稱或樣式",
"cw_files_search_go": "搜尋",
"cw_files_search_scope": "搜尋範圍",
"cw_files_search_all": "所有磁碟機",
"cw_files_search_here": "此資料夾",
"cw_files_search_hint": "依名稱尋找檔案。單字可符合名稱中的任何位置;樣式可使用 * 和 ?(*.pkg)。",
"cw_files_search_bad": "無法搜尋引號和反斜線。",
"cw_files_search_unsupported": "此主機上的 PS5Upload 版本太舊,無法搜尋。請更新後使用搜尋。",
"cw_files_search_no_index": "尚無索引。第一次搜尋會建立索引:需要讀取每個資料夾,可能需要幾分鐘。",
"cw_files_search_building": "正在建立索引… 目前 {n} 個檔案。",
"cw_files_search_stopping": "正在停止索引…",
"cw_files_search_ready": "索引:{n} 個檔案。",
"cw_files_search_partial": "索引提早停止,因此缺少部分檔案。",
"cw_files_search_capped": "索引已滿(200,000 個檔案),因此缺少部分檔案。",
"cw_files_search_rebuild": "重新建立",
"cw_files_search_cancel": "取消",
"cw_files_search_waiting": "索引就緒後將搜尋「{q}」。",
"cw_files_search_searching": "正在搜尋…",
"cw_files_search_none": "沒有符合「{q}」的項目。",
"cw_files_search_found": "找到 {n} 個。",
"cw_files_search_more": "顯示前 {n} 個。還有更多:請嘗試較長的名稱。",
"cw_files_search_results": "搜尋結果",
"cw_files_usage": "磁碟用量",
"cw_files_usage_open": "在「檔案」中開啟",
"cw_files_usage_open_row": "在「檔案」中開啟 {name}",
"cw_files_usage_inside": "查看 {name} 內部",
"cw_files_usage_files_here": "此資料夾中的檔案",
"cw_files_usage_running": "正在測量… {n} 個資料夾中的第 {at} 個,{items} 項。",
"cw_files_usage_done": "{items} 項共 {size}。",
"cw_files_usage_capped": "在 {items} 項處停止:已計 {size},資料夾中還有更多。",
"cw_files_usage_timeout": "3 分鐘後停止:已計 {size},其餘未測量。",
"cw_files_usage_cancelled": "已取消:已計 {size},其餘未測量。",
"cw_files_usage_again": "重新測量",
"cw_files_usage_busy": "正在測量另一個資料夾的大小。請在結束後重試。",
"cw_files_usage_skipped": "未測量",
"cw_files_usage_unreadable": "無法讀取",
};
export default zh_TW;
+34
View File
@@ -145,4 +145,38 @@ const char *webui_cheats_toggle_error(const char *cause, size_t n);
* unmount, so a running game is never asked about: it would lose its image (ruling R20). */
const char *webui_unmount_refusal(const char *p, int game_running);
/* ---- search index (INDEX_START / INDEX_STATUS / INDEX_SEARCH / INDEX_CANCEL, runtime.c) ----
* The handlers read their bodies with a quote-to-quote scan (no JSON unescaping), so every string the
* page sends is checked to hold no '"', no '\\' and no control character before it is put in one. */
/* The roots INDEX_START takes: at most 8, each under 256 bytes (the handler's own limits). */
#define WEBUI_INDEX_ROOTS_MAX 8
#define WEBUI_INDEX_ROOT_CAP 256
/* 1 when `p` can be an index root: starts with '/', 1-255 bytes, no '"', '\\' or control character. */
int webui_index_root_ok(const char *p);
/* 1 when `q` can be a name pattern: 1-255 bytes, no '"', '\\' or control character. */
int webui_index_query_ok(const char *q);
/* INDEX_START's body {"roots":["/a","/b"]} for `n` roots (each webui_index_root_ok, 1 <= n <= 8).
* Returns the length, or -1 (a bad root, a bad count, or it does not fit; out is then ""). */
int webui_index_start_body(char *out, size_t cap, const char *const *roots, int n);
/* An INDEX_STATUS reply ({"phase":"idle"|"building"|"ready","files":N,"truncated":bool,"started_at":N,
* "completed_at":N}) as
* {"phase":"..","files":N,"truncated":bool,"cancelled":bool,"roots":["..",..]|null}
* `roots` (NULL: null) are the roots the page started this index with, `cancelled` that the page stopped
* it (the helper itself does not say). 0, or -1 (no usable phase, or it does not fit). */
int webui_index_status_to_json(const char *text, size_t n, const char *const *roots, int nroots, int cancelled,
char *out, size_t cap);
/* An INDEX_SEARCH reply ({"results":[{"path":"..","size":N},..](,"truncated":true)}) as
* {"results":[{"path":"..","size":N},..],"truncated":bool}
* keeping only paths under `under` (NULL: all; "/" all) that `hide` (may be NULL) does not hide, at
* most `max` of them. "truncated" is set when the helper said so, when it answered `asked` results
* (its limit: there may be more), when a result was dropped for `max`, or when `out` is full. Returns
* the number written, or -1 (no results array, or not even the empty list fits). */
int webui_index_results_to_json(const char *text, size_t n, const char *under, size_t max, size_t asked,
int (*hide)(const char *path, void *ctx), void *ctx, char *out, size_t cap);
#endif
+196
View File
@@ -1081,6 +1081,198 @@ static void r_cheats_toggle(const char *b, size_t n, uint8_t *out, int fd) {
}
}
/* ---- search: the helper's file index (INDEX_START / STATUS / SEARCH / CANCEL, runtime.c) ----
*
* One index for the whole helper (its own thread walks the roots: at most 200,000 files and 32 MiB of
* paths, ten folders deep, dot names skipped; INDEX_CANCEL stops it between folders). The helper does
* not say which roots an index covers or that it was stopped: the page's last start is remembered here
* (by the started_at INDEX_STATUS gives it) so /api/index/status can tell the page whether the index
* it finds is the one it asked for. The AVA1 trust store's files are never answered. */
#define API_INDEX_RESULTS_MAX 500u /* results one search answers */
#define API_INDEX_ASK_ALL 500u /* INDEX_SEARCH limit for a search of every root */
#define API_INDEX_ASK_UNDER 5000u /* for one folder: the helper's own maximum, filtered here */
#define API_INDEX_JSON (256u * 1024u) /* 500 results with long, escaped paths fit easily */
#define API_INDEX_STATUS_JSON (WEBUI_INDEX_ROOTS_MAX * (6u * WEBUI_INDEX_ROOT_CAP + 3u) + 160u) /* 8 roots, escaped */
static pthread_mutex_t g_idx_mu = PTHREAD_MUTEX_INITIALIZER;
static char g_idx_roots[WEBUI_INDEX_ROOTS_MAX][WEBUI_INDEX_ROOT_CAP];
static int g_idx_nroots = 0;
static uint64_t g_idx_started_at = 0; /* 0: no index the page started */
static int g_idx_cancelled = 0;
static int hide_protected(const char *path, void *ctx) {
(void)ctx;
return path_in_protected(path);
}
/* INDEX_STATUS's reply text, and its started_at (0 when absent). */
static int index_status_text(uint8_t *out, const uint8_t **t, size_t *tl, uint64_t *started_at) {
size_t ol;
int st = call_text(AVA1_METHOD_INDEX_STATUS, NULL, 0, out, &ol, t, tl);
if (st != AVA1_STATUS_OK) return st;
if (webui_json_u64((const char *)*t, *tl, "started_at", started_at) != 1) *started_at = 0;
return AVA1_STATUS_OK;
}
/* {roots:["/data",..]} (1-8 roots): a new index, replacing the one there. 409 busy while one is built. */
static void r_index_start(const char *b, size_t n, uint8_t *out, int fd) {
char roots[WEBUI_INDEX_ROOTS_MAX][WEBUI_INDEX_ROOT_CAP], body[WEBUI_INDEX_ROOTS_MAX * (WEBUI_INDEX_ROOT_CAP + 3) + 16];
char err[64];
const char *ptrs[WEBUI_INDEX_ROOTS_MAX];
const char *arr;
const uint8_t *t;
size_t alen, pos = 0, ol, tl;
uint64_t started_at = 0;
int nroots = 0, rc, len, st, started = 0;
if (webui_json_array(b, n, "roots", &arr, &alen) != 1) {
send_bad(fd, "bad_roots");
return;
}
for (;;) {
char root[WEBUI_INDEX_ROOT_CAP + 1];
rc = webui_json_array_str(arr, alen, &pos, root, sizeof root);
if (rc == 0) break;
if (rc < 0 || nroots >= WEBUI_INDEX_ROOTS_MAX || !webui_index_root_ok(root)) {
send_bad(fd, "bad_roots");
return;
}
if (path_in_protected(root)) {
send_error(fd, 403, "path_not_allowed", 16);
return;
}
memcpy(roots[nroots], root, strlen(root) + 1);
ptrs[nroots] = roots[nroots];
nroots++;
}
len = nroots ? webui_index_start_body(body, sizeof body, ptrs, nroots) : -1;
if (len <= 0) {
send_bad(fd, "bad_roots");
return;
}
st = call_text(AVA1_METHOD_INDEX_START, body, (size_t)len, out, &ol, &t, &tl);
if (st != AVA1_STATUS_OK) {
send_status_error(fd, st, out, ol);
return;
}
if (webui_json_bool((const char *)t, tl, "started", &started) != 1 || !started) {
if (webui_json_str((const char *)t, tl, "err", err, sizeof err) != 1 || !err[0]) snprintf(err, sizeof err, "index_failed");
if (strcmp(err, "already_building") == 0) send_error(fd, 409, "busy", 4);
else send_error(fd, 500, err, strlen(err));
return;
}
/* the index just started is this one: remember what it covers */
if (index_status_text(out, &t, &tl, &started_at) != AVA1_STATUS_OK) started_at = 0;
pthread_mutex_lock(&g_idx_mu);
memcpy(g_idx_roots, roots, sizeof roots);
g_idx_nroots = nroots;
g_idx_started_at = started_at;
g_idx_cancelled = 0;
pthread_mutex_unlock(&g_idx_mu);
send_json(fd, 200, "{\"started\":true}", 16);
}
/* {} -> {"phase","files","truncated","cancelled","roots"|null}: roots null when the index there is not
* one the page started (never built, or another client's). */
static void r_index_status(const char *b, size_t n, uint8_t *out, int fd) {
const char *ptrs[WEBUI_INDEX_ROOTS_MAX];
char roots[WEBUI_INDEX_ROOTS_MAX][WEBUI_INDEX_ROOT_CAP];
char *json;
const uint8_t *t;
size_t tl;
uint64_t started_at = 0;
int st, i, nroots = 0, cancelled = 0, ours = 0;
(void)b;
(void)n;
st = index_status_text(out, &t, &tl, &started_at);
if (st != AVA1_STATUS_OK) {
send_status_error(fd, st, NULL, 0);
return;
}
pthread_mutex_lock(&g_idx_mu);
if (g_idx_started_at != 0 && g_idx_started_at == started_at) {
ours = 1;
nroots = g_idx_nroots;
cancelled = g_idx_cancelled;
memcpy(roots, g_idx_roots, sizeof roots);
}
pthread_mutex_unlock(&g_idx_mu);
for (i = 0; i < nroots; i++) ptrs[i] = roots[i];
json = malloc(API_INDEX_STATUS_JSON);
if (!json) {
send_error(fd, 500, "out_of_memory", 13);
return;
}
if (webui_index_status_to_json((const char *)t, tl, ours ? ptrs : NULL, nroots, cancelled, json, API_INDEX_STATUS_JSON) != 0)
send_error(fd, 500, "bad_index_reply", 15);
else
send_json(fd, 200, json, strlen(json));
free(json);
}
/* {query, under?}: files whose NAME matches `query` (the helper's glob: * and ?, ASCII case-insensitive,
* the whole name), under the folder `under` when given. -> {"results":[{"path","size"}],"truncated"}. */
static void r_index_search(const char *b, size_t n, uint8_t *out, int fd) {
char query[WEBUI_INDEX_ROOT_CAP + 1], under[API_PATH], body[WEBUI_INDEX_ROOT_CAP + 64], *json;
const uint8_t *t;
size_t ol, tl;
unsigned ask;
int rc, st, k;
if (webui_json_str(b, n, "query", query, sizeof query) != 1 || !webui_index_query_ok(query)) {
send_bad(fd, "bad_query");
return;
}
rc = webui_json_str(b, n, "under", under, sizeof under);
if (rc < 0 || (rc == 1 && under[0] != '/')) {
send_bad(fd, "bad_under");
return;
}
if (rc == 0) under[0] = '\0';
ask = under[0] ? API_INDEX_ASK_UNDER : API_INDEX_ASK_ALL;
k = snprintf(body, sizeof body, "{\"query\":\"%s\",\"limit\":%u}", query, ask); /* checked: no quote, no backslash */
if (k <= 0 || (size_t)k >= sizeof body) {
send_bad(fd, "bad_query");
return;
}
st = call_text(AVA1_METHOD_INDEX_SEARCH, body, (size_t)k, out, &ol, &t, &tl);
if (st != AVA1_STATUS_OK) {
send_status_error(fd, st, out, ol);
return;
}
json = malloc(API_INDEX_JSON);
if (!json) {
send_error(fd, 500, "out_of_memory", 13);
return;
}
if (webui_index_results_to_json((const char *)t, tl, under[0] ? under : NULL, API_INDEX_RESULTS_MAX, ask,
hide_protected, NULL, json, API_INDEX_JSON) < 0)
send_error(fd, 500, "bad_index_reply", 15);
else
send_json(fd, 200, json, strlen(json));
free(json);
}
/* {} -> {}: the build stops before its next folder (what it found stays searchable, marked cancelled). */
static void r_index_cancel(const char *b, size_t n, uint8_t *out, int fd) {
const uint8_t *t;
size_t ol, tl;
uint64_t started_at = 0;
int st;
(void)b;
(void)n;
st = call_text(AVA1_METHOD_INDEX_CANCEL, NULL, 0, out, &ol, &t, &tl);
if (st != AVA1_STATUS_OK) {
send_status_error(fd, st, out, ol);
return;
}
if (index_status_text(out, &t, &tl, &started_at) == AVA1_STATUS_OK) {
pthread_mutex_lock(&g_idx_mu);
if (g_idx_started_at != 0 && g_idx_started_at == started_at) g_idx_cancelled = 1;
pthread_mutex_unlock(&g_idx_mu);
}
send_json(fd, 200, "{}", 2);
}
/* ---- input log ---- */
/* One append at a time (the file is a ring rewritten in place), and a rate gate: a page that
@@ -1166,6 +1358,10 @@ static const struct {
{ "/api/cheats/status", r_cheats_status },
{ "/api/cheats/list", r_cheats_list },
{ "/api/cheats/toggle", r_cheats_toggle },
{ "/api/index/start", r_index_start },
{ "/api/index/status", r_index_status },
{ "/api/index/search", r_index_search },
{ "/api/index/cancel", r_index_cancel },
};
/* The page's own headers: only same-origin fetches, inline script/style (the page is one file),
+165
View File
@@ -868,3 +868,168 @@ const char *webui_unmount_refusal(const char *p, int game_running) {
return "system_volume";
return game_running ? "game_running" : NULL;
}
/* ---- search index ---- */
/* 1-255 bytes with no '"', '\\' or control character (the index handlers scan quote to quote). */
static int index_text_ok(const char *s) {
size_t i, n;
if (!s) return 0;
n = strlen(s);
if (n == 0 || n >= WEBUI_INDEX_ROOT_CAP) return 0;
for (i = 0; i < n; i++) {
unsigned char c = (unsigned char)s[i];
if (c < 0x20 || c == 0x7f || c == '"' || c == '\\') return 0;
}
return 1;
}
int webui_index_root_ok(const char *p) { return index_text_ok(p) && p[0] == '/'; }
int webui_index_query_ok(const char *q) { return index_text_ok(q); }
int webui_index_start_body(char *out, size_t cap, const char *const *roots, int n) {
size_t at = 0;
int i;
if (!out || !cap) return -1;
out[0] = '\0';
if (!roots || n < 1 || n > WEBUI_INDEX_ROOTS_MAX) return -1;
for (i = 0; i < n; i++)
if (!webui_index_root_ok(roots[i])) return -1;
if (cap < 12) return -1;
memcpy(out, "{\"roots\":[", 10);
at = 10;
for (i = 0; i < n; i++) {
size_t l = strlen(roots[i]);
/* `,"<root>"` then `]}` and the NUL */
if (at + l + 3 + 3 > cap) {
out[0] = '\0';
return -1;
}
if (i) out[at++] = ',';
out[at++] = '"';
memcpy(out + at, roots[i], l);
at += l;
out[at++] = '"';
}
out[at++] = ']';
out[at++] = '}';
out[at] = '\0';
return (int)at;
}
int webui_index_status_to_json(const char *text, size_t n, const char *const *roots, int nroots, int cancelled,
char *out, size_t cap) {
char phase[16];
uint64_t files = 0;
int truncated = 0, i;
jw_t w;
char num[48];
if (cap) out[0] = '\0';
if (!text || webui_json_str(text, n, "phase", phase, sizeof phase) != 1) return -1;
if (strcmp(phase, "idle") != 0 && strcmp(phase, "building") != 0 && strcmp(phase, "ready") != 0) return -1;
if (webui_json_u64(text, n, "files", &files) != 1) files = 0;
if (webui_json_bool(text, n, "truncated", &truncated) != 1) truncated = 0;
memset(&w, 0, sizeof w);
w.out = out;
w.cap = cap;
put(&w, "{\"phase\":\"", 10);
put(&w, phase, strlen(phase));
snprintf(num, sizeof num, "\",\"files\":%llu", (unsigned long long)files);
put(&w, num, strlen(num));
put(&w, truncated ? ",\"truncated\":true" : ",\"truncated\":false", truncated ? 17 : 18);
put(&w, cancelled ? ",\"cancelled\":true" : ",\"cancelled\":false", cancelled ? 17 : 18);
if (!roots) {
put(&w, ",\"roots\":null}", 14);
} else {
put(&w, ",\"roots\":[", 10);
for (i = 0; i < nroots; i++) {
if (i) put(&w, ",", 1);
put(&w, "\"", 1);
esc(&w, roots[i], strlen(roots[i]));
put(&w, "\"", 1);
}
put(&w, "]}", 2);
}
if (w.full) {
if (cap) out[0] = '\0';
return -1;
}
out[w.wrote] = '\0';
return 0;
}
/* `path` is `under` or below it ("/" and NULL: everything). */
static int path_under(const char *path, const char *under) {
size_t l;
if (!under || !under[0] || strcmp(under, "/") == 0) return 1;
l = strlen(under);
while (l > 1 && under[l - 1] == '/') l--;
return strncmp(path, under, l) == 0 && (path[l] == '\0' || path[l] == '/');
}
#define INDEX_RESULTS_TAIL 24 /* `],"truncated":false}` + NUL */
int webui_index_results_to_json(const char *text, size_t n, const char *under, size_t max, size_t asked,
int (*hide)(const char *path, void *ctx), void *ctx, char *out, size_t cap) {
const char *e = text + n, *p, *q;
char path[1024 + 8];
size_t pos, seen = 0;
int written = 0, truncated = 0, said = 0;
if (cap) out[0] = '\0';
if (!text) return -1;
p = top_value(text, n, "results");
if (!p || *p != '[') return -1;
if (cap < 12 + INDEX_RESULTS_TAIL) return -1;
if (webui_json_bool(text, n, "truncated", &said) == 1 && said) truncated = 1;
memcpy(out, "{\"results\":[", 12);
pos = 12;
q = p + 1;
for (;;) {
const char *el, *end;
size_t l;
uint64_t size;
q = ws(q, e);
if (q >= e) {
truncated = 1;
break;
}
if (*q == ']') break;
el = q;
end = skip_val(q, e);
if (!end) {
truncated = 1;
break;
}
l = (size_t)(end - el);
seen++;
if (*el == '{' && webui_json_str(el, l, "path", path, sizeof path) == 1 && path[0] == '/' &&
webui_json_u64(el, l, "size", &size) == 1 && path_under(path, under) && !(hide && hide(path, ctx))) {
jw_t w;
char tail[40];
int tl;
if ((size_t)written >= max) {
truncated = 1;
break;
}
memset(&w, 0, sizeof w);
w.out = out + pos;
w.cap = cap - pos - INDEX_RESULTS_TAIL + 1;
put(&w, written ? ",{\"path\":\"" : "{\"path\":\"", written ? 10 : 9);
esc(&w, path, strlen(path));
tl = snprintf(tail, sizeof tail, "\",\"size\":%llu}", (unsigned long long)size);
put(&w, tail, (size_t)tl);
if (w.full) {
truncated = 1;
break;
}
pos += w.wrote;
written++;
}
q = ws(end, e);
if (q < e && *q == ',') q++;
}
if (asked && seen >= asked) truncated = 1;
snprintf(out + pos, cap - pos, "],\"truncated\":%s}", truncated ? "true" : "false");
return written;
}
+92
View File
@@ -769,6 +769,97 @@ static void cheats_toggle_errors(void) {
CHECK(strcmp(webui_cheats_toggle_error("busy", 4), "toggle_failed") == 0);
}
static int hide_secret(const char *path, void *ctx) {
(void)ctx;
return strncmp(path, "/data/secret", 12) == 0;
}
static void index_codec(void) {
static char out[4096];
const char *roots[] = { "/data", "/mnt/ext0", "/mnt/usb0" };
const char *bad[] = { "/data", "/a\"b" };
char long_root[300];
const char *j;
/* roots and queries: nothing the handlers' quote-to-quote scan would misread */
CHECK(webui_index_root_ok("/data"));
CHECK(webui_index_root_ok("/mnt/usb0/Spiele \xc3\xa4"));
CHECK(!webui_index_root_ok("data"));
CHECK(!webui_index_root_ok(""));
CHECK(!webui_index_root_ok("/a\"b"));
CHECK(!webui_index_root_ok("/a\\b"));
CHECK(!webui_index_root_ok("/a\nb"));
memset(long_root, 'a', sizeof long_root);
long_root[0] = '/';
long_root[256] = '\0';
CHECK(!webui_index_root_ok(long_root)); /* 256 bytes: the handler keeps < 256 */
long_root[255] = '\0';
CHECK(webui_index_root_ok(long_root));
CHECK(webui_index_query_ok("*.pkg"));
CHECK(webui_index_query_ok("eboot"));
CHECK(!webui_index_query_ok(""));
CHECK(!webui_index_query_ok("a\"b"));
CHECK(!webui_index_query_ok("a\\b"));
CHECK(!webui_index_query_ok("a\tb"));
/* INDEX_START's body */
CHECK(webui_index_start_body(out, sizeof out, roots, 3) > 0);
CHECK(strcmp(out, "{\"roots\":[\"/data\",\"/mnt/ext0\",\"/mnt/usb0\"]}") == 0);
CHECK(json_valid(out));
CHECK(webui_index_start_body(out, sizeof out, bad, 2) == -1 && out[0] == '\0');
CHECK(webui_index_start_body(out, sizeof out, roots, 0) == -1);
CHECK(webui_index_start_body(out, sizeof out, roots, 9) == -1);
CHECK(webui_index_start_body(out, 20, roots, 3) == -1 && out[0] == '\0');
/* status: the helper's reply plus what the page knows */
j = "{\"phase\":\"building\",\"files\":1234,\"truncated\":false,\"started_at\":1700000000,\"completed_at\":0}";
CHECK(webui_index_status_to_json(j, strlen(j), roots, 2, 0, out, sizeof out) == 0);
CHECK(strcmp(out, "{\"phase\":\"building\",\"files\":1234,\"truncated\":false,\"cancelled\":false,"
"\"roots\":[\"/data\",\"/mnt/ext0\"]}") == 0);
j = "{\"phase\":\"ready\",\"files\":200000,\"truncated\":true,\"started_at\":1,\"completed_at\":2}";
CHECK(webui_index_status_to_json(j, strlen(j), NULL, 0, 1, out, sizeof out) == 0);
CHECK(strcmp(out, "{\"phase\":\"ready\",\"files\":200000,\"truncated\":true,\"cancelled\":true,\"roots\":null}") == 0);
j = "{\"phase\":\"weird\",\"files\":1}";
CHECK(webui_index_status_to_json(j, strlen(j), NULL, 0, 0, out, sizeof out) == -1);
CHECK(webui_index_status_to_json("nope", 4, NULL, 0, 0, out, sizeof out) == -1);
j = "{\"phase\":\"idle\",\"files\":0,\"truncated\":false,\"started_at\":0,\"completed_at\":0}";
CHECK(webui_index_status_to_json(j, strlen(j), roots, 3, 0, out, 30) == -1 && out[0] == '\0');
/* search results: escaped paths, scope, hidden paths, the helper's limit */
j = "{\"results\":[{\"path\":\"/data/games/A \\\"x\\\".pkg\",\"size\":10},"
"{\"path\":\"/data/secret/key.pkg\",\"size\":2},"
"{\"path\":\"/data/gamesX/b.pkg\",\"size\":3},"
"{\"path\":\"/mnt/usb0/c.pkg\",\"size\":4}]}";
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 200, hide_secret, NULL, out, sizeof out) == 3);
CHECK(json_valid(out));
CHECK(strcmp(out, "{\"results\":[{\"path\":\"/data/games/A \\\"x\\\".pkg\",\"size\":10},"
"{\"path\":\"/data/gamesX/b.pkg\",\"size\":3},{\"path\":\"/mnt/usb0/c.pkg\",\"size\":4}],"
"\"truncated\":false}") == 0);
/* under a folder: "/data/games" is not "/data/gamesX" */
CHECK(webui_index_results_to_json(j, strlen(j), "/data/games/", 100, 200, hide_secret, NULL, out, sizeof out) == 1);
CHECK(strstr(out, "A \\\"x\\\".pkg") && !strstr(out, "gamesX"));
CHECK(webui_index_results_to_json(j, strlen(j), "/", 100, 200, NULL, NULL, out, sizeof out) == 4);
/* the page's own maximum, and the helper's limit reached: both say there may be more */
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 2, 200, NULL, NULL, out, sizeof out) == 2);
CHECK(strstr(out, "\"truncated\":true}") != NULL);
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 4, NULL, NULL, out, sizeof out) == 4);
CHECK(strstr(out, "\"truncated\":true}") != NULL);
/* the helper's own truncated flag */
j = "{\"results\":[{\"path\":\"/data/a\",\"size\":1}],\"truncated\":true}";
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 200, NULL, NULL, out, sizeof out) == 1);
CHECK(strstr(out, "\"truncated\":true}") != NULL);
/* a cut reply keeps the whole entries */
j = "{\"results\":[{\"path\":\"/data/a\",\"size\":1},{\"path\":\"/data/b";
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 200, NULL, NULL, out, sizeof out) == 1);
CHECK(json_valid(out) && strstr(out, "\"truncated\":true}"));
/* an entry without a usable path or size is left out; an empty list is fine */
j = "{\"results\":[{\"path\":\"rel\",\"size\":1},{\"path\":\"/x\"},{\"size\":3}]}";
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 200, NULL, NULL, out, sizeof out) == 0);
CHECK(strcmp(out, "{\"results\":[],\"truncated\":false}") == 0);
CHECK(webui_index_results_to_json("{\"err\":\"oom\"}", 13, NULL, 100, 200, NULL, NULL, out, sizeof out) == -1);
/* a small output keeps what fits and says so */
j = "{\"results\":[{\"path\":\"/data/aaaaaaaaaaaaaaaaaaaa\",\"size\":1},{\"path\":\"/data/bbbbbbbbbbbbbbbbbbbb\",\"size\":2}]}";
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 200, NULL, NULL, out, 90) == 1);
CHECK(json_valid(out) && strstr(out, "aaaa") && !strstr(out, "bbbb") && strstr(out, "\"truncated\":true}"));
CHECK(webui_index_results_to_json(j, strlen(j), NULL, 100, 200, NULL, NULL, out, 20) == -1);
}
int main(void) {
unicode_path_round_trips();
array_span();
@@ -792,6 +883,7 @@ int main(void) {
cheats_status();
cheats_mods();
cheats_toggle_errors();
index_codec();
if (failures) {
fprintf(stderr, "webui_codec_selftest: %d failure(s)\n", failures);
return 1;