refactor(panel): standardize interface copy in documentation style

This commit is contained in:
Lemon-miaow committed 2026-10-06 00:02:20 +08:00
1 parent fa1b8a905d
commit df19878c2a
50 files changed
+869 -869

No files matched your search

@@ -154,7 +154,7 @@ describe("CreateServerDialog names", () => {
["survival-", "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end."], ["survival-", "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end."],
["my_world", "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end."], ["my_world", "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end."],
["a".repeat(33), "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end."], ["a".repeat(33), "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end."],
["lobby", "“lobby” is kept for the platform. Pick another."], ["lobby", "“lobby” is reserved by the platform. Use a different name."],
])("refuses the name %j before sending, and says why", async (name, why) => { ])("refuses the name %j before sending, and says why", async (name, why) => {
const user = await openDialog(); const user = await openDialog();
await fillValid(user); await fillValid(user);
@@ -176,7 +176,7 @@ describe("CreateServerDialog names", () => {
await user.type(subdomainBox(), "api"); await user.type(subdomainBox(), "api");
expect(create().disabled).toBe(true); expect(create().disabled).toBe(true);
expect(screen.getByText("“api” is kept for the platform. Pick another.")).toBeTruthy(); expect(screen.getByText("“api” is reserved by the platform. Use a different name.")).toBeTruthy();
expect(screen.queryByText(/Will be reachable at/)).toBeNull(); expect(screen.queryByText(/Will be reachable at/)).toBeNull();
}); });
@@ -249,10 +249,10 @@ describe("CreateServerDialog on reopening", () => {
const alert = await screen.findByRole("alert"); const alert = await screen.findByRole("alert");
expect(alert.textContent).toContain(humanizeError(outage)); expect(alert.textContent).toContain(humanizeError(outage));
expect(screen.getByText("Image list not loaded")).toBeTruthy(); expect(screen.getByText("Image list unavailable")).toBeTruthy();
expect(screen.queryByText("No whitelisted images")).toBeNull(); expect(screen.queryByText("No whitelisted images")).toBeNull();
await user.click(within(alert).getByRole("button", { name: "Try again" })); await user.click(within(alert).getByRole("button", { name: "Retry" }));
expect(calls.listImages).toHaveBeenCalledTimes(2); expect(calls.listImages).toHaveBeenCalledTimes(2);
await pick(user, "Image", PAPER); await pick(user, "Image", PAPER);
expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByRole("alert")).toBeNull();
@@ -26,7 +26,7 @@ describe("durable migration", () => {
await screen.findByText("source node is offline"); await screen.findByText("source node is offline");
expect(calls.migration).toHaveBeenCalledWith("survival"); expect(calls.migration).toHaveBeenCalledWith("survival");
expect(screen.getByText("persisted-op")).toBeTruthy(); expect(screen.getByText("persisted-op")).toBeTruthy();
await user.click(screen.getByRole("button", { name: "Try again" })); await user.click(screen.getByRole("button", { name: "Retry" }));
await waitFor(() => expect(calls.retryMigration).toHaveBeenCalledWith("survival", "persisted-op")); await waitFor(() => expect(calls.retryMigration).toHaveBeenCalledWith("survival", "persisted-op"));
expect(changed).toHaveBeenCalled(); expect(changed).toHaveBeenCalled();
expect(calls.migrateServer).not.toHaveBeenCalled(); expect(calls.migrateServer).not.toHaveBeenCalled();
@@ -36,7 +36,7 @@ describe("durable migration", () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<MigrationDialog name="survival" nodeName="b" stopped={false} onChanged={() => {}} />); render(<MigrationDialog name="survival" nodeName="b" stopped={false} onChanged={() => {}} />);
await user.click(screen.getByRole("button", { name: "Migrate world" })); await user.click(screen.getByRole("button", { name: "Migrate world" }));
const retry = await screen.findByRole("button", { name: "Try again" }); const retry = await screen.findByRole("button", { name: "Retry" });
expect((retry as HTMLButtonElement).disabled).toBe(true); expect((retry as HTMLButtonElement).disabled).toBe(true);
await user.click(retry); await user.click(retry);
expect(calls.retryMigration).not.toHaveBeenCalled(); expect(calls.retryMigration).not.toHaveBeenCalled();
@@ -109,7 +109,7 @@ describe("EditServerDialog request body", () => {
for (const bad of ["two", "0", "1.5m", "-1"]) { for (const bad of ["two", "0", "1.5m", "-1"]) {
await user.clear(cpuInput()); await user.clear(cpuInput());
await user.type(cpuInput(), bad); await user.type(cpuInput(), bad);
expect(screen.getByText("Enter cores (1, 1.5) or millicores (500m), above zero."), bad).toBeTruthy(); expect(screen.getByText("The CPU limit must be greater than zero, expressed as cores (for example, 1 or 1.5) or millicores (for example, 500m)."), bad).toBeTruthy();
expect(cpuInput().getAttribute("aria-invalid"), bad).toBe("true"); expect(cpuInput().getAttribute("aria-invalid"), bad).toBe("true");
expect(save().disabled, bad).toBe(true); expect(save().disabled, bad).toBe(true);
} }
+2 -2
View File
@@ -70,7 +70,7 @@ describe("NotRunning", () => {
] as const) { ] as const) {
const { unmount } = render(notRunning(phase, desired)); const { unmount } = render(notRunning(phase, desired));
expect(screen.getByText("Server is starting"), `${phase}/${desired}`).toBeTruthy(); expect(screen.getByText("Server is starting"), `${phase}/${desired}`).toBeTruthy();
expect(screen.getByText(/switches over by itself/), `${phase}/${desired}`).toBeTruthy(); expect(screen.getByText(/updates the startup status automatically/), `${phase}/${desired}`).toBeTruthy();
expect(screen.queryByRole("button"), `${phase}/${desired}`).toBeNull(); expect(screen.queryByRole("button"), `${phase}/${desired}`).toBeNull();
unmount(); unmount();
} }
@@ -122,7 +122,7 @@ describe("RefreshError", () => {
const err = { status: 429, code: "quota_exceeded", message: "raw" }; const err = { status: 429, code: "quota_exceeded", message: "raw" };
render(<RefreshError error={err} />); render(<RefreshError error={err} />);
const alert = screen.getByRole("alert"); const alert = screen.getByRole("alert");
expect(alert.textContent).toContain("Couldn't refresh; what you see is the last status read."); expect(alert.textContent).toContain("Refresh failed. The last successfully retrieved status is displayed.");
expect(alert.textContent).toContain(humanizeError(err)); expect(alert.textContent).toContain(humanizeError(err));
}); });
}); });
+14 -14
View File
@@ -7,50 +7,50 @@ const e = (code: string, over: Record<string, unknown> = {}) => ({ code, message
describe("opErrorText", () => { describe("opErrorText", () => {
it("tells an upload that found its file there from an extraction that would replace files", () => { it("tells an upload that found its file there from an extraction that would replace files", () => {
expect(opErrorText("upload", e("file_exists"))).toBe( expect(opErrorText("upload", e("file_exists"))).toBe(
"A file with this name is already here, so nothing was replaced. Upload it again and choose Replace.", "A file with this name already exists. The upload did not replace it. Select Replace when uploading again.",
); );
expect(opErrorText("unzip", e("file_exists", { conflicts: ["a", "b"], conflict_count: 250 }))).toBe( expect(opErrorText("unzip", e("file_exists", { conflicts: ["a", "b"], conflict_count: 250 }))).toBe(
"It would replace 250 files already here, so nothing was extracted yet.", "Extraction would overwrite 250 existing files. The operation has not been performed.",
); );
// Without the count, the list is counted. // Without the count, the list is counted.
expect(opErrorText("unzip", e("file_exists", { conflicts: ["a"] }))).toBe( expect(opErrorText("unzip", e("file_exists", { conflicts: ["a"] }))).toBe(
"It would replace 1 file already here, so nothing was extracted yet.", "Extraction would overwrite 1 existing file. The operation has not been performed.",
); );
}); });
it("names both sizes of a volume too small, and falls back when they are missing", () => { it("names both sizes of a volume too small, and falls back when they are missing", () => {
expect(opErrorText("unzip", e("volume_full", { need: 3 * 1024 * 1024, avail: 1024 }))).toBe( expect(opErrorText("unzip", e("volume_full", { need: 3 * 1024 * 1024, avail: 1024 }))).toBe(
"Not enough room on the world volume: 3.0 MiB needed, 1.0 KiB free. Nothing was changed.", "Insufficient world volume space: 3.0 MiB required, 1.0 KiB available. This operation did not modify files.",
); );
// A full volume: the Job leaves the zero out. // A full volume: the Job leaves the zero out.
expect(opErrorText("upload", e("volume_full", { need: 2048 }))).toBe( expect(opErrorText("upload", e("volume_full", { need: 2048 }))).toBe(
"Not enough room on the world volume: 2.0 KiB needed, 0 B free. Nothing was changed.", "Insufficient world volume space: 2.0 KiB required, 0 B available. This operation did not modify files.",
); );
expect(opErrorText("upload", e("volume_full"))).toBe(humanizeError({ status: 0, code: "volume_full", message: "raw words" })); expect(opErrorText("upload", e("volume_full"))).toBe(humanizeError({ status: 0, code: "volume_full", message: "raw words" }));
}); });
it("names the archive entry at fault", () => { it("names the archive entry at fault", () => {
expect(opErrorText("unzip", e("archive_invalid"))).toBe("The archive is damaged, or not a zip file. Nothing was changed."); expect(opErrorText("unzip", e("archive_invalid"))).toBe("The archive is damaged or is not in ZIP format. This operation did not modify files.");
expect(opErrorText("unzip", e("archive_invalid", { entry: "world/level.dat" }))).toBe( expect(opErrorText("unzip", e("archive_invalid", { entry: "world/level.dat" }))).toBe(
"world/level.dat in the archive is damaged (its size or checksum does not match). Nothing was changed.", "The size or checksum of archive entry world/level.dat does not match. This operation did not modify files.",
); );
expect(opErrorText("unzip", e("archive_unsafe", { entry: "../../etc/passwd" }))).toBe( expect(opErrorText("unzip", e("archive_unsafe", { entry: "../../etc/passwd" }))).toBe(
"../../etc/passwd in the archive would land outside this folder, or is a device file. Nothing in the archive was extracted.", "Archive entry ../../etc/passwd contains an out-of-directory path or a device file. The entire archive was rejected without extraction.",
); );
expect(opErrorText("unzip", e("archive_symlink", { entry: "world/link" }))).toBe( expect(opErrorText("unzip", e("archive_symlink", { entry: "world/link" }))).toBe(
"world/link in the archive is a symbolic link. Nothing in the archive was extracted.", "Archive entry world/link is a symbolic link. The entire archive was rejected without extraction.",
); );
expect(opErrorText("unzip", e("type_conflict", { entry: "plugins" }))).toBe( expect(opErrorText("unzip", e("type_conflict", { entry: "plugins" }))).toBe(
"plugins is a file on one side and a folder on the other, which replacing cannot resolve. Rename or delete plugins here, then extract again.", "Entry plugins conflicts with an existing entry of the same name because one is a file and the other is a folder. Rename or delete the existing plugins before extraction.",
); );
}); });
it("gives a Job that ran out of time its own words", () => { it("gives a Job that ran out of time its own words", () => {
expect(opErrorText("unzip", e("job_failed", { message: "job failed: DeadlineExceeded" }))).toBe( expect(opErrorText("unzip", e("job_failed", { message: "job failed: DeadlineExceeded" }))).toBe(
"The background task did not finish within 2 hours and was stopped. Refresh the list to check, then try again.", "The background task exceeded its two-hour execution limit and was terminated. Refresh the file list to verify the result before retrying.",
); );
expect(opErrorText("upload", e("job_failed", { message: "job failed: BackoffLimitExceeded" }))).toBe( expect(opErrorText("upload", e("job_failed", { message: "job failed: BackoffLimitExceeded" }))).toBe(
"The background task ended without saying why. Refresh the list to check, then try again.", "The background task ended without a failure reason. Refresh the file list to verify the result before retrying.",
); );
}); });
@@ -58,10 +58,10 @@ describe("opErrorText", () => {
// The message felis-api words a memory kill with. // The message felis-api words a memory kill with.
const oom = "the file operation ran out of memory (OOMKilled); an archive of this many files has to be split into smaller ones"; const oom = "the file operation ran out of memory (OOMKilled); an archive of this many files has to be split into smaller ones";
expect(opErrorText("unzip", e("job_failed", { message: oom }))).toBe( expect(opErrorText("unzip", e("job_failed", { message: oom }))).toBe(
"The archive holds more files than the extraction task has memory to list, so the system stopped it. The files on the server were not changed. Split it into several smaller zips and extract each one.", "The extraction task terminated because its file list exceeded available memory. Server files were not modified. Split the content into smaller ZIP archives and upload and extract them separately.",
); );
expect(opErrorText("upload", e("job_failed", { message: oom }))).toBe( expect(opErrorText("upload", e("job_failed", { message: oom }))).toBe(
"The background task ran out of memory and the system stopped it. The files on the server were not changed. Try again.", "The background task terminated due to insufficient memory. Server files were not modified. Check task resources before retrying.",
); );
}); });
@@ -59,12 +59,12 @@ describe("WakeListSection", () => {
it("says the list has no effect under the other two policies", async () => { it("says the list has no effect under the other two policies", async () => {
calls.serverAllowlist.mockResolvedValue([]); calls.serverAllowlist.mockResolvedValue([]);
const owner = render(<WakeListSection name="lobby" policy="ownerOnly" defaultOpen />); const owner = render(<WakeListSection name="lobby" policy="ownerOnly" defaultOpen />);
expect(await screen.findByText(/Owner only, so only you and admins/)).toBeTruthy(); expect(await screen.findByText(/Owner only policy permits only the server owner and administrators/)).toBeTruthy();
expect(screen.getByText("No player has joined this server yet.")).toBeTruthy(); expect(screen.getByText("No player join records available.")).toBeTruthy();
owner.unmount(); owner.unmount();
render(<WakeListSection name="lobby" policy="public" defaultOpen />); render(<WakeListSection name="lobby" policy="public" defaultOpen />);
expect(await screen.findByText(/Public, so any player's join starts it/)).toBeTruthy(); expect(await screen.findByText(/Public policy allows any player’s join to trigger startup/)).toBeTruthy();
}); });
it("revokes and restores in place, reading the list again after each change", async () => { it("revokes and restores in place, reading the list again after each change", async () => {
@@ -97,14 +97,14 @@ describe("WakeListSection", () => {
calls.setAllowlistWake.mockRejectedValue({ status: 403, code: "forbidden", message: "forbidden" }); calls.setAllowlistWake.mockRejectedValue({ status: 403, code: "forbidden", message: "forbidden" });
const first = render(<WakeListSection name="lobby" policy="allowlist" defaultOpen />); const first = render(<WakeListSection name="lobby" policy="allowlist" defaultOpen />);
await user.click(await screen.findByRole("button", { name: "Take away Steve's right to wake the server" })); await user.click(await screen.findByRole("button", { name: "Take away Steve's right to wake the server" }));
expect((await screen.findByRole("alert")).textContent).toBe("You are not allowed to do that."); expect((await screen.findByRole("alert")).textContent).toBe("The current account is not authorized to perform this operation.");
expect(within(rowOf("Steve")).queryByText("Revoked")).toBeNull(); expect(within(rowOf("Steve")).queryByText("Revoked")).toBeNull();
first.unmount(); first.unmount();
calls.serverAllowlist.mockRejectedValue({ status: 500, code: "internal", message: "db down" }); calls.serverAllowlist.mockRejectedValue({ status: 500, code: "internal", message: "db down" });
render(<WakeListSection name="lobby" policy="allowlist" defaultOpen />); render(<WakeListSection name="lobby" policy="allowlist" defaultOpen />);
expect((await screen.findByRole("alert")).textContent).toBe( expect((await screen.findByRole("alert")).textContent).toBe(
"Couldn't load the wake list. The operation failed because of an internal server error. Ask an admin to check the logs.", "Unable to load the wake list. The operation failed because of an internal server error. Ask an admin to check the logs.",
); );
}); });
+37 -37
View File
@@ -2,8 +2,8 @@
"title": "Account", "title": "Account",
"subtitle": "Identity and Minecraft linking.", "subtitle": "Identity and Minecraft linking.",
"sessions_title": "Signed-in devices", "sessions_title": "Signed-in devices",
"sessions_desc": "Every browser signed in to your account. Sign out any you don't recognize.", "sessions_desc": "Review browser sessions signed in to the current account. Revoke any unrecognized device session.",
"sessions_staff_idle": "Operator sessions sign out on their own after 30 minutes without activity.", "sessions_staff_idle": "Operations account sessions expire after 30 minutes of inactivity.",
"loading_sessions": "Loading devices…", "loading_sessions": "Loading devices…",
"sessions_empty": "No devices are signed in with a panel session.", "sessions_empty": "No devices are signed in with a panel session.",
"session_this_device": "This device", "session_this_device": "This device",
@@ -12,37 +12,37 @@
"session_active_now": "Active now", "session_active_now": "Active now",
"session_active": "Active {{when}}", "session_active": "Active {{when}}",
"session_signed_in": "Signed in {{when}}", "session_signed_in": "Signed in {{when}}",
"session_sign_out_aria": "Sign out {{device}}", "session_sign_out_aria": "Sign out the session on {{device}}",
"session_signed_out_device": "Signed out {{device}}.", "session_signed_out_device": "Signed out {{device}}.",
"sessions_sign_out_others": "Sign out other devices", "sessions_sign_out_others": "Sign out other devices",
"sessions_sign_out_others_title": "Sign out every other device?", "sessions_sign_out_others_title": "Sign out all other device sessions?",
"sessions_sign_out_others_desc": "Every device except this one is signed out and has to sign in again.", "sessions_sign_out_others_desc": "Sessions on all devices except the current device will be invalidated. Sign-in will be required again.",
"sessions_signed_out_others_one": "Signed out {{count}} other device.", "sessions_signed_out_others_one": "The session on {{count}} other device has been signed out.",
"sessions_signed_out_others_other": "Signed out {{count}} other devices.", "sessions_signed_out_others_other": "Sessions on {{count}} other devices have been signed out.",
"sign_out": "Sign out", "sign_out": "Sign out",
"signing_out": "Signing out…", "signing_out": "Signing out…",
"minecraft_link": "Minecraft link", "minecraft_link": "Minecraft link",
"checking_link": "Checking link status…", "checking_link": "Checking link status…",
"linked_title": "Your Minecraft account is linked.", "linked_title": "Your Minecraft account is linked.",
"linked_desc": "Linked sessions can claim and own servers — ownership operations (claim, wake, stop) are unblocked.", "linked_desc": "The Minecraft account is linked. Server claiming, starting, stopping, and other authorized management operations are available.",
"uuid_label": "UUID", "uuid_label": "UUID",
"step1_title": "Get a code in-game", "step1_title": "Get a code in-game",
"step1_desc_prefix": "Join any server and run ", "step1_desc_prefix": "Connect to a server and run ",
"step1_desc_suffix": " in chat. The server already knows your verified identity, so it hands you a one-time code (valid about 10 minutes).", "step1_desc_suffix": ". After identity verification, the server provides a one-time binding code valid for approximately 10 minutes.",
"step2_title": "Enter it here", "step2_title": "Enter the binding code",
"link_code": "Link code", "link_code": "Link code",
"link_code_placeholder": "ABCD2345", "link_code_placeholder": "ABCD2345",
"verify_btn": "Link", "verify_btn": "Link",
"verifying": "Verifying…", "verifying": "Verifying…",
"email_verification": "Email Verification", "email_verification": "Email Verification",
"email_desc": "Verify your email address to secure your account.", "email_desc": "Verify the registered email address for account authentication.",
"email_verified": "Verified", "email_verified": "Verified",
"send_code": "Send Code", "send_code": "Send Code",
"sending_code": "Sending…", "sending_code": "Sending…",
"email_step1": "Enter Email Address", "email_step1": "Enter Email Address",
"email_step1_desc": "Enter the email address you want to bind to request a verification code.", "email_step1_desc": "Enter the email address to register and request a verification code.",
"email_step2": "Enter Verification Code", "email_step2": "Enter Verification Code",
"email_step2_desc": "Enter the 6-digit verification code sent to your email (valid for 10 minutes).", "email_step2_desc": "Enter the six-digit verification code from the email. The code is valid for 10 minutes.",
"email_verify_btn": "Verify", "email_verify_btn": "Verify",
"email_verifying": "Verifying…", "email_verifying": "Verifying…",
"email_otp_sent": "Verification code sent.", "email_otp_sent": "Verification code sent.",
@@ -50,7 +50,7 @@
"change_email": "Change email", "change_email": "Change email",
"continue_btn": "Continue", "continue_btn": "Continue",
"passkeys": "Passkeys", "passkeys": "Passkeys",
"passkeys_desc": "Passkeys let you log in securely using your fingerprint, face, or screen lock PIN.", "passkeys_desc": "Passkeys provide panel sign-in using a fingerprint, facial recognition, or device PIN.",
"no_passkeys": "No registered passkeys.", "no_passkeys": "No registered passkeys.",
"loading_passkeys": "Loading passkeys…", "loading_passkeys": "Loading passkeys…",
"add_passkey": "Add Passkey", "add_passkey": "Add Passkey",
@@ -61,41 +61,41 @@
"last_used": "Last used: ", "last_used": "Last used: ",
"never": "Never", "never": "Never",
"passkey_delete_aria": "Delete passkey “{{name}}”", "passkey_delete_aria": "Delete passkey “{{name}}”",
"passkey_delete_title": "Delete this passkey?", "passkey_delete_title": "Delete this Passkey?",
"passkey_delete_desc": "“{{name}}” (registered {{created}}) will no longer sign you in. You can register it again later. Your other devices are signed out too.", "passkey_delete_desc": "Deleting “{{name}}” (registered {{created}}) prevents further sign-in with this key and signs out sessions on other devices. The key can be registered again if required.",
"passkey_delete_confirm": "Delete", "passkey_delete_confirm": "Delete",
"passkey_last_hint": "This is your only passkey and your email is not verified, so deleting it would lock you out. Verify an email or add another passkey first.", "passkey_last_hint": "The account has only one Passkey and no verified email address. Removing it would eliminate the available sign-in method. Verify an email address or register another Passkey first.",
"migration": "Account migration", "migration": "Account migration",
"migration_desc": "Move everything a retired account owns onto this one. Migration starts in-game and finishes here.", "migration_desc": "Transfer all servers owned by a retired account to the current account. Initiate migration in-game and complete it in the panel.",
"account_id": "Account ID", "account_id": "Account ID",
"migration_checking": "Checking migration status…", "migration_checking": "Checking migration status…",
"migration_none_prefix": "No migration in progress. To move this account's servers to another account, run ", "migration_none_prefix": "No migration in progress. To move this account's servers to another account, run ",
"migration_none_suffix": " in-game on the account being retired.", "migration_none_suffix": " in-game on the account being retired.",
"migration_confirm_title": "Confirm it's you", "migration_confirm_title": "Account identity verification",
"migration_confirm_desc": "Migration retires this account, so it needs a step-up check first.", "migration_confirm_desc": "Migration retires this account, so it needs a step-up check first.",
"migration_confirming": "Confirming…", "migration_confirming": "Confirming…",
"migration_confirm_passkey_btn": "Confirm with passkey", "migration_confirm_passkey_btn": "Confirm with passkey",
"migration_confirm_otp_btn": "Send a code to my email", "migration_confirm_otp_btn": "Send a code to my email",
"migration_issue_title": "Name the destination account", "migration_issue_title": "Name the destination account",
"migration_issue_desc": "Paste the Account ID shown on the destination account's own page here, then issue a one-time transfer code.", "migration_issue_desc": "Enter the Account ID displayed on the destination account page, then issue a one-time transfer code.",
"migration_issue_deadline": "Issue it before {{time}}. The confirmation counts only in this browser; after that, confirm again. A verified email on this account gets a notice once the code is issued.", "migration_issue_deadline": "Issue the code before {{time}}. Identity confirmation is valid only in the current browser and must be repeated after expiry. A notification will be sent to the verified email address after issuance.",
"migration_target_placeholder": "Destination Account ID", "migration_target_placeholder": "Destination Account ID",
"migration_issuing": "Issuing…", "migration_issuing": "Issuing…",
"migration_issue_btn": "Issue code", "migration_issue_btn": "Issue code",
"migration_code_title": "Transfer code (shown once)", "migration_code_title": "Transfer code (shown once)",
"migration_code_desc": "Sign in as the destination account and redeem this code there before it expires", "migration_code_desc": "Sign in to the destination account and redeem this code before it expires.",
"migration_code_pending": "A transfer code has been issued. Redeem it from the destination account before it expires", "migration_code_pending": "A transfer code has been issued. Redeem it from the destination account before it expires",
"migration_redeem_title": "Redeem a transfer code", "migration_redeem_title": "Redeem a transfer code",
"migration_redeem_desc": "Received a code from an account being retired? Redeem it here to take over its servers.", "migration_redeem_desc": "Redeem the transfer code issued by the source account to transfer its servers to the current account.",
"migration_redeem_placeholder": "Transfer code", "migration_redeem_placeholder": "Transfer code",
"migration_redeeming": "Redeeming…", "migration_redeeming": "Redeeming…",
"migration_redeem_btn": "Redeem", "migration_redeem_btn": "Redeem",
"migration_redeemed": "Migration complete — {{count}} server(s) moved to this account.", "migration_redeemed": "Migration complete — {{count}} server(s) moved to this account.",
"reauth_title": "Confirm it's you", "reauth_title": "Account identity verification",
"reauth_desc": "Adding or removing a passkey and changing the email need a fresh check with a way in you already have. The check lasts 5 minutes.", "reauth_desc": "This operation requires identity verification. Use a registered Passkey or verified email address. Verification remains valid for five minutes.",
"reauth_checking": "Checking how you can confirm…", "reauth_checking": "Checking available identity verification methods…",
"reauth_passkey_btn": "Use a passkey", "reauth_passkey_btn": "Use a passkey",
"reauth_passkey_waiting": "Waiting for your passkey…", "reauth_passkey_waiting": "Waiting for Passkey verification…",
"reauth_or": "or", "reauth_or": "or",
"reauth_email_btn": "Email a code to {{email}}", "reauth_email_btn": "Email a code to {{email}}",
"reauth_code_label": "Code sent to {{email}}", "reauth_code_label": "Code sent to {{email}}",
@@ -104,24 +104,24 @@
"reauth_resend": "Send another code", "reauth_resend": "Send another code",
"reauth_sign_in_desc": "Operator accounts can also sign out and sign in again. A fresh sign-in counts for 5 minutes.", "reauth_sign_in_desc": "Operator accounts can also sign out and sign in again. A fresh sign-in counts for 5 minutes.",
"reauth_sign_in_btn": "Sign out and sign in again", "reauth_sign_in_btn": "Sign out and sign in again",
"reauth_done_continue": "Confirmed. Press Continue to add the passkey.", "reauth_done_continue": "Identity verified. Select Continue to add a Passkey.",
"email_change_desc": "Enter the new address and we'll send it a code. Once it's verified, sign-in codes go there, the old address gets a notice, and your other devices are signed out.", "email_change_desc": "Enter the new email address to receive a verification code. After verification, sign-in codes will be sent to the new address, a change notification will be sent to the previous address, and sessions on other devices will be signed out.",
"staff_linked_desc": "This role is linked to your administrator account and identifies you in-game.", "staff_linked_desc": "This profile is linked to the current administrator account. Its management identity is recognized when joining the game.",
"staff_link_desc": "Link a game role to identify yourself in-game. Panel management is already available; you can do this later.", "staff_link_desc": "Link a Minecraft profile to identify its management role when joining the game. Panel administration does not require this link and it can be configured later.",
"staff_sources_loading": "Loading authentication sources…", "staff_sources_loading": "Loading authentication sources…",
"staff_source_mojang": "Minecraft official", "staff_source_mojang": "Minecraft official",
"staff_source": "Authentication source", "staff_source": "Authentication source",
"staff_profile": "Minecraft role name or UUID", "staff_profile": "Minecraft role name or UUID",
"staff_lookup": "Look up role", "staff_lookup": "Look up role",
"staff_working": "Working…", "staff_working": "Working…",
"staff_lookup_unsupported": "This source needs api_url configured before roles can be looked up. You can continue using the panel.", "staff_lookup_unsupported": "This authentication source requires api_url for profile lookup. Panel administration remains available.",
"staff_confirm_desc": "Confirm this role as your game identity. It will carry your administrator authority in-game.", "staff_confirm_desc": "Link this profile to the current administrator account to identify its management role when joining the game.",
"staff_confirm": "Confirm role link", "staff_confirm": "Confirm role link",
"staff_source_help": "Yggdrasil is Minecraft’s account authentication protocol. Official and third-party providers use it to supply role profiles. Select the source used by your launcher and enter a role name or UUID, not an email.", "staff_source_help": "Yggdrasil is Minecraft’s account authentication protocol. Official and third-party providers use it to supply role profiles. Select the source used by your launcher and enter a role name or UUID, not an email.",
"game_guide": "Prepare to enter Minecraft", "game_guide": "Prepare to enter Minecraft",
"game_version": "Use Minecraft Java Edition {{version}} for the login server and lobby. Target servers may require another version or mods; check their instructions.", "game_version": "Use Minecraft Java Edition {{version}} for the login server and lobby. Target servers may require another version or mods; check their instructions.",
"game_version_unknown": "The login server’s client version is not configured. Ask the operator to confirm it and set game_version; the panel build version does not determine it.", "game_version_unknown": "The login service client version is not configured. An operator must verify and set game_version. The panel version does not indicate the game version.",
"game_lobby": "The login server verifies your identity before sending you to the lobby. The lobby is a hub for choosing a target server: use /menu and wait for it to start. Check the server list if login or lobby is not ready.", "game_lobby": "Players enter the lobby after authentication through the login service. The lobby provides server selection. Run /menu to open the server menu and wait for startup. Check the service list if the login service or lobby is not ready.",
"game_thirdparty": "For a third-party source, configure the same provider in your launcher. Profile lookup reads an identity; joining still requires authentication with that provider.", "game_thirdparty": "For a third-party source, configure the same provider in your launcher. Profile lookup reads an identity; joining still requires authentication with that provider.",
"staff_code_alternative": "Provider does not support lookup? Use an in-game link code" "staff_code_alternative": "Use an in-game binding code when profile lookup is unavailable"
} }
+39 -39
View File
@@ -9,7 +9,7 @@
"add_image_btn": "Add Image", "add_image_btn": "Add Image",
"image_ref_placeholder": "e.g. registry.felis.svc:5000/image:tag", "image_ref_placeholder": "e.g. registry.felis.svc:5000/image:tag",
"delete_image_tooltip": "Delete Whitelist Image", "delete_image_tooltip": "Delete Whitelist Image",
"image_source_built": "Built here", "image_source_built": "Platform build",
"image_source_external": "External", "image_source_external": "External",
"image_source_recommended": "Recommended", "image_source_recommended": "Recommended",
"delete_image_title": "Remove this image from the whitelist?", "delete_image_title": "Remove this image from the whitelist?",
@@ -32,10 +32,10 @@
"download_context_btn": "Download context", "download_context_btn": "Download context",
"download_context_hint": "Downloads the uploaded context (.tar.gz) — it contains the Dockerfile that will actually be executed.", "download_context_hint": "Downloads the uploaded context (.tar.gz) — it contains the Dockerfile that will actually be executed.",
"context_sha256_label": "Context SHA-256", "context_sha256_label": "Context SHA-256",
"context_sha256_hint": "Approving binds this digest: if the submitter uploads again, the build refuses to run. When reviewing offline, compare it with sha256sum of your download.", "context_sha256_hint": "Approval pins this digest. Builds reject subsequent uploads that do not match it. Use sha256sum to verify downloaded files during offline review.",
"context_sha256_missing": "No build context uploaded yet, or it was uploaded before digests were recorded and must be uploaded again.", "context_sha256_missing": "No build context is available, or it predates digest recording. The submitter must upload it again.",
"context_sha256_downloaded": "Matches the file you just downloaded; approving binds exactly these bytes.", "context_sha256_downloaded": "The digest matches the downloaded file. Approval pins this content.",
"context_sha256_stale": "The copy you downloaded ({{digest}}…) was replaced by a newer upload; download and review it again.", "context_sha256_stale": "The downloaded version ({{digest}}…) was replaced by a new upload. Download and review the latest version.",
"approve_needs_context": "Nothing to approve: the context has not been uploaded, or must be uploaded again", "approve_needs_context": "Nothing to approve: the context has not been uploaded, or must be uploaded again",
"base_image_label": "Base Image", "base_image_label": "Base Image",
"base_image_placeholder": "e.g. library/postgres:15", "base_image_placeholder": "e.g. library/postgres:15",
@@ -44,11 +44,11 @@
"build_status_succeeded": "Succeeded", "build_status_succeeded": "Succeeded",
"build_status_failed": "Failed", "build_status_failed": "Failed",
"build_status_cancelled": "Cancelled", "build_status_cancelled": "Cancelled",
"build_queued_hint": "The concurrent build limit ([registry] max_concurrent_builds) is reached; this build starts on its own when an earlier one finishes.", "build_queued_hint": "The concurrent build limit ([registry] max_concurrent_builds) has been reached. The task starts automatically after a running build completes.",
"view_logs_btn": "Scan & logs", "view_logs_btn": "Scan & logs",
"cancel_build_btn": "Cancel", "cancel_build_btn": "Cancel",
"no_builds_title": "No Builds Found", "no_builds_title": "No Builds Found",
"no_builds_hint": "You can trigger your first image build task using the form on the top right.", "no_builds_hint": "Submit an image build task using the build form.",
"no_images_title": "No images whitelisted", "no_images_title": "No images whitelisted",
"no_images_hint": "The whitelist is empty — a platform admin must add one (CLI for now).", "no_images_hint": "The whitelist is empty — a platform admin must add one (CLI for now).",
"enabled": "enabled", "enabled": "enabled",
@@ -112,7 +112,7 @@
"updates_stat_end": "End Time", "updates_stat_end": "End Time",
"updates_stat_timezone": "Local Timezone", "updates_stat_timezone": "Local Timezone",
"dbbackup_title": "Control-plane database backup", "dbbackup_title": "Control-plane database backup",
"dbbackup_subtitle": "Users, passkeys, server ownership, quotas, audit logs and the world-archive index live in this database. felis-db-backup.timer on the host backs it up daily, and every upgrade snapshots it before migrating.", "dbbackup_subtitle": "The control-plane database stores users, Passkeys, server ownership, quotas, audit logs, and world archive indexes. felis-db-backup.timer performs daily backups, and a separate snapshot is created before upgrade migrations.",
"dbbackup_status_ok": "Healthy", "dbbackup_status_ok": "Healthy",
"dbbackup_status_stale": "Overdue", "dbbackup_status_stale": "Overdue",
"dbbackup_status_never": "Never backed up", "dbbackup_status_never": "Never backed up",
@@ -129,18 +129,18 @@
"dbbackup_label_pre_restore": "Pre-restore snapshot", "dbbackup_label_pre_restore": "Pre-restore snapshot",
"dbbackup_label_offsite": "Off-site copy snapshot", "dbbackup_label_offsite": "Off-site copy snapshot",
"dbbackup_label_manual": "Manual", "dbbackup_label_manual": "Manual",
"dbbackup_never_title": "No database backup has been recorded yet", "dbbackup_never_title": "No database backup records",
"dbbackup_stale_title": "The newest backup is more than {{hours}} hours old", "dbbackup_stale_title": "The newest backup is more than {{hours}} hours old",
"dbbackup_daily_stale_title": "The daily backup last completed {{when}}, past the {{hours}}-hour limit", "dbbackup_daily_stale_title": "The daily backup last completed {{when}}, past the {{hours}}-hour limit",
"dbbackup_daily_never_title": "The daily timer has not completed a backup yet", "dbbackup_daily_never_title": "The daily timer has not completed a backup",
"dbbackup_daily_fix_hint": "The newer backup above was taken by hand or by another job and can be restored from, but it grows older every day the timer stays broken. Run the daily backup on the host now, then read its log to find out why it stopped:", "dbbackup_daily_fix_hint": "The newer backup was created manually or by another task and is available for restoration. It does not confirm that the daily timer is healthy. Run a daily backup on the host and inspect the timer logs:",
"dbbackup_servers_title": "The newest backup lacks the server definitions", "dbbackup_servers_title": "The newest backup lacks the server definitions",
"dbbackup_servers_hint": "The cluster did not answer when the backup was taken. Restoring from it brings back accounts and the database, but no servers. Once the cluster answers, take a backup on the host again:", "dbbackup_servers_hint": "The cluster did not respond during backup, so server definitions are missing. Restoring the database alone cannot recreate servers. Perform a complete backup after cluster connectivity recovers:",
"dbbackup_fix_hint": "If the host failed now, accounts, server ownership and the archive index could not be recovered. Run the daily backup on the host now, then read its log to find out why the timer missed it:", "dbbackup_fix_hint": "Without a valid database backup, host failure may prevent recovery of accounts, server ownership, and archive indexes. Run a daily backup on the host and inspect the timer logs:",
"dbbackup_copy": "Copy command", "dbbackup_copy": "Copy command",
"dbbackup_offsite_note": "Backups are kept on this host only and are lost with its disk. Copy the backup directory to another machine regularly; restore and disaster-recovery steps are in the troubleshooting guide, §16.", "dbbackup_offsite_note": "Backups are stored only on the current host. Disk or host failure may affect both backups and original data. Copy backups regularly to an independent host. Restoration and disaster recovery procedures are documented in troubleshooting §16.",
"versions_title": "Component versions", "versions_title": "Component versions",
"versions_subtitle": "felis-update-check.timer compares what this host runs with the newest upstream releases every day. Felis applies nothing on its own.", "versions_subtitle": "felis-update-check.timer compares installed component versions with upstream releases daily. Felis does not apply updates automatically.",
"versions_status_available_one": "{{count}} update available", "versions_status_available_one": "{{count}} update available",
"versions_status_available_other": "{{count}} updates available", "versions_status_available_other": "{{count}} updates available",
"versions_status_current": "Up to date", "versions_status_current": "Up to date",
@@ -157,19 +157,19 @@
"versions_state_available": "Update available", "versions_state_available": "Update available",
"versions_state_current": "Up to date", "versions_state_current": "Up to date",
"versions_state_unknown": "Feed unreachable", "versions_state_unknown": "Feed unreachable",
"versions_state_unreadable": "Version unreadable", "versions_state_unreadable": "Version read failed",
"versions_state_pinned": "Pinned", "versions_state_pinned": "Pinned",
"versions_apply_hint": "Run this read-only check on the host. It shows the target, scope and an exact `--apply` command. Review it, then run that command during the maintenance window:", "versions_apply_hint": "Run this read-only check on the host. It shows the target, scope and an exact `--apply` command. Review it, then run that command during the maintenance window:",
"versions_never_title": "No version check has been recorded yet", "versions_never_title": "No version check records",
"versions_stale_title": "The newest version check is more than {{hours}} hours old", "versions_stale_title": "The newest version check is more than {{hours}} hours old",
"versions_fix_hint": "The versions below may be out of date. Run a check on the host now, then read the timer's log to find out why it did not run:", "versions_fix_hint": "The displayed version information may be outdated. Run a new check on the host and inspect the timer logs to determine why the scheduled check did not run:",
"build_import_submission_label": "Import parameters from submission", "build_import_submission_label": "Import parameters from submission",
"build_import_submission_placeholder": "Select a user submission...", "build_import_submission_placeholder": "Select a user submission...",
"build_import_submission_none": "No submissions to import yet", "build_import_submission_none": "No submissions available for import",
"build_import_submission_no_match": "No submissions match “{{query}}”", "build_import_submission_no_match": "No submissions match “{{query}}”",
"build_import_submission_unavailable": "Submission list not loaded", "build_import_submission_unavailable": "Submission list unavailable",
"build_import_submission_failed": "Couldn't load submissions: {{reason}}", "build_import_submission_failed": "Unable to load submissions: {{reason}}",
"build_import_submission_more": "Listing the newest {{shown}} of {{total}}; search above to find older ones.", "build_import_submission_more": "Displaying the latest {{shown}} of {{total}} submissions. Use search to locate older submissions.",
"build_import_search_placeholder": "Search submissions by name, ID or submitter", "build_import_search_placeholder": "Search submissions by name, ID or submitter",
"build_import_submission_hint": "Selecting a submission automatically populates the Image Reference, Context Reference, and the corresponding Dockerfile audit header.", "build_import_submission_hint": "Selecting a submission automatically populates the Image Reference, Context Reference, and the corresponding Dockerfile audit header.",
"build_import_submission_warning_title": "Warning: This submission is currently \"{{status}}\"", "build_import_submission_warning_title": "Warning: This submission is currently \"{{status}}\"",
@@ -201,7 +201,7 @@
"users_col_created": "Created", "users_col_created": "Created",
"users_view_detail": "Details", "users_view_detail": "Details",
"user_not_found_title": "No such user", "user_not_found_title": "No such user",
"user_not_found_body": "This account may have been deleted. Find it again from the user list.", "user_not_found_body": "The account does not exist or has been deleted. Return to the user list and search again.",
"users_empty_title": "No Users Found", "users_empty_title": "No Users Found",
"users_empty_hint": "No users match the current filters.", "users_empty_hint": "No users match the current filters.",
"users_back_to_list": "Back to user list", "users_back_to_list": "Back to user list",
@@ -209,12 +209,12 @@
"users_field_username": "Username", "users_field_username": "Username",
"users_field_email": "Email", "users_field_email": "Email",
"users_field_role": "Role", "users_field_role": "Role",
"users_role_locked_self": "You can't change your own role.", "users_role_locked_self": "The current account’s role cannot be changed.",
"users_role_locked_owner": "The owner's role is fixed. Only the host's break-glass console (sudo felis breakGlass) manages the owner.", "users_role_locked_owner": "The Owner role cannot be changed. Manage it through the host recovery console (sudo felis breakGlass).",
"users_save_btn": "Save Changes", "users_save_btn": "Save Changes",
"users_save_ok": "Changes saved successfully.", "users_save_ok": "Changes saved successfully.",
"users_linked_accounts": "Linked Minecraft Accounts", "users_linked_accounts": "Linked Minecraft Accounts",
"users_no_linked": "No Minecraft accounts linked yet.", "users_no_linked": "No Minecraft accounts linked.",
"users_link_add": "Link Account", "users_link_add": "Link Account",
"users_link_source": "Auth Source", "users_link_source": "Auth Source",
"users_link_source_mojang": "Mojang", "users_link_source_mojang": "Mojang",
@@ -230,7 +230,7 @@
"users_quota_memory": "Max Memory (MiB)", "users_quota_memory": "Max Memory (MiB)",
"users_quota_storage": "Max Storage (GiB)", "users_quota_storage": "Max Storage (GiB)",
"users_quota_unlimited": "Unlimited", "users_quota_unlimited": "Unlimited",
"users_quota_hint": "Leave a box empty for unlimited. 0 grants none of it, so every claim that needs it is refused. A cap below current use only stops new claims; servers the user already has stay.", "users_quota_hint": "Leave blank for no limit. Zero allocates no resources for that category. Claims exceeding the quota are rejected. Reducing a quota restricts subsequent claims without affecting existing servers.",
"users_sessions": "Sessions", "users_sessions": "Sessions",
"users_no_sessions": "No active sessions.", "users_no_sessions": "No active sessions.",
"users_session_expires": "Expires", "users_session_expires": "Expires",
@@ -243,8 +243,8 @@
"users_session_revoke_all_dlg_desc": "Are you sure you want to revoke all active sessions? The user will be logged out from every device.", "users_session_revoke_all_dlg_desc": "Are you sure you want to revoke all active sessions? The user will be logged out from every device.",
"users_session_revoke_confirm": "Revoke", "users_session_revoke_confirm": "Revoke",
"users_danger_zone": "Danger Zone", "users_danger_zone": "Danger Zone",
"users_protected_self": "You can't disable or delete the account you're signed in with.", "users_protected_self": "The signed-in account cannot be disabled or deleted.",
"users_protected_owner": "The owner account can't be disabled or deleted from the panel. Only the host's break-glass console (sudo felis breakGlass) manages it.", "users_protected_owner": "The Owner account cannot be disabled or deleted from the panel. Manage it through the host recovery console (sudo felis breakGlass).",
"users_danger_disable": "Disable User", "users_danger_disable": "Disable User",
"users_danger_disable_desc": "Prevent this user from logging in. All active sessions will be revoked immediately.", "users_danger_disable_desc": "Prevent this user from logging in. All active sessions will be revoked immediately.",
"users_danger_disable_btn": "Disable User", "users_danger_disable_btn": "Disable User",
@@ -260,12 +260,12 @@
"users_danger_delete": "Delete User", "users_danger_delete": "Delete User",
"users_danger_delete_desc": "Soft-delete this user. Owned servers are released, all sessions are revoked, and the account is permanently disabled. This action cannot be undone through the panel.", "users_danger_delete_desc": "Soft-delete this user. Owned servers are released, all sessions are revoked, and the account is permanently disabled. This action cannot be undone through the panel.",
"users_danger_delete_btn": "Delete User", "users_danger_delete_btn": "Delete User",
"users_danger_delete_yes": "Yes, Delete Permanently", "users_danger_delete_yes": "Confirm permanent deletion",
"users_danger_passkeys": "Unbind passkeys", "users_danger_passkeys": "Unbind passkeys",
"users_danger_passkeys_desc": "Sever every passkey this account holds — the remediation when an authenticator is lost or compromised. Other login doors (email code, in-game approval) keep working.", "users_danger_passkeys_desc": "Remove all Passkeys registered to this account to address lost or compromised authenticators. Other sign-in methods, including email codes and in-game approval, remain available.",
"users_danger_passkeys_btn": "Unbind passkeys", "users_danger_passkeys_btn": "Unbind passkeys",
"users_danger_passkeys_dlg_title": "Unbind passkeys", "users_danger_passkeys_dlg_title": "Unbind passkeys",
"users_danger_passkeys_dlg_desc": "Every passkey for this account will be permanently removed. This is not a lockout: the user can sign in another way and re-enroll from the account page.", "users_danger_passkeys_dlg_desc": "All Passkeys registered to this account will be permanently removed. Other sign-in methods remain available. Keys can be registered again from Account after sign-in.",
"users_danger_passkeys_yes": "Yes, unbind", "users_danger_passkeys_yes": "Yes, unbind",
"add_image_desc": "Register an external Docker image reference on the whitelist for later server creation.", "add_image_desc": "Register an external Docker image reference on the whitelist for later server creation.",
"images_search_placeholder": "Search image name or source...", "images_search_placeholder": "Search image name or source...",
@@ -274,14 +274,14 @@
"submissions_search_placeholder": "Search modpack name or submitter...", "submissions_search_placeholder": "Search modpack name or submitter...",
"trigger_build_desc": "Enter the build parameters to launch a Kaniko pipeline job in an isolated namespace.", "trigger_build_desc": "Enter the build parameters to launch a Kaniko pipeline job in an isolated namespace.",
"builds_search_placeholder": "Search build ID, image reference, or status...", "builds_search_placeholder": "Search build ID, image reference, or status...",
"builds_refresh_failed": "Couldn't refresh the build list: {{reason}}", "builds_refresh_failed": "Unable to refresh the build list: {{reason}}",
"build_requested_by_you": "You", "build_requested_by_you": "You",
"scan_title": "Security scan", "scan_title": "Security scan",
"scan_blocked": "Blocked", "scan_blocked": "Blocked",
"scan_passed": "Passed", "scan_passed": "Passed",
"scan_loading": "Loading the security scan…", "scan_loading": "Loading the security scan…",
"scan_none": "No security scan was kept for this build. It stopped before the scan step, or it ran before builds kept their scans.", "scan_none": "This build has no retained security scan record. It may have ended before scanning or been created before scan retention was enabled.",
"scan_load_failed": "Couldn't load the security scan: {{reason}}", "scan_load_failed": "Unable to load the security scan: {{reason}}",
"scan_meta_one": "Scanned {{when}} · {{count}} package", "scan_meta_one": "Scanned {{when}} · {{count}} package",
"scan_meta_other": "Scanned {{when}} · {{count}} packages", "scan_meta_other": "Scanned {{when}} · {{count}} packages",
"scan_policy": "Blocks on {{severities}}; vulnerabilities with no fixed release are listed only", "scan_policy": "Blocks on {{severities}}; vulnerabilities with no fixed release are listed only",
@@ -289,14 +289,14 @@
"scan_counts_label": "Findings by severity", "scan_counts_label": "Findings by severity",
"scan_blocking_count_one": "{{count}} blocks the image", "scan_blocking_count_one": "{{count}} blocks the image",
"scan_blocking_count_other": "{{count}} block the image", "scan_blocking_count_other": "{{count}} block the image",
"scan_clean": "Trivy found nothing to report in this image.", "scan_clean": "Trivy detected no findings in this image.",
"scan_showing": "Showing {{shown}} of {{total}} findings. The full report lists every one.", "scan_showing": "Showing {{shown}} of {{total}} findings. The full report lists every one.",
"scan_col_id": "ID", "scan_col_id": "ID",
"scan_col_severity": "Severity", "scan_col_severity": "Severity",
"scan_col_package": "Package", "scan_col_package": "Package",
"scan_col_version": "Installed → fixed", "scan_col_version": "Installed → fixed",
"scan_col_target": "Found in", "scan_col_target": "Found in",
"scan_no_fix": "no fix yet", "scan_no_fix": "No fix available",
"scan_blocks": "Blocks", "scan_blocks": "Blocks",
"scan_policy_accepts_one": "{{count}} ID accepted as a known risk", "scan_policy_accepts_one": "{{count}} ID accepted as a known risk",
"scan_policy_accepts_other": "{{count}} IDs accepted as known risks", "scan_policy_accepts_other": "{{count}} IDs accepted as known risks",
@@ -305,8 +305,8 @@
"scan_secret": "Secret: {{title}}", "scan_secret": "Secret: {{title}}",
"scan_download_report": "Trivy report", "scan_download_report": "Trivy report",
"scan_download_sbom": "SBOM", "scan_download_sbom": "SBOM",
"scan_not_kept": "Not kept with this build: the file was too large, or the step that writes it failed.", "scan_not_kept": "The file was not retained for this build. It may have exceeded the size limit or its generation step may have failed.",
"scan_download_failed": "Couldn't download: {{reason}}", "scan_download_failed": "Unable to download: {{reason}}",
"platform_title": "Platform settings", "platform_title": "Platform settings",
"platform_subtitle": "Global controls for server startup and wake requests.", "platform_subtitle": "Global controls for server startup and wake requests.",
"platform_reload": "Reload", "platform_reload": "Reload",
+22 -22
View File
@@ -13,56 +13,56 @@
"otp_placeholder": "Enter 6-digit code", "otp_placeholder": "Enter 6-digit code",
"send_otp": "Send Code", "send_otp": "Send Code",
"sending_otp": "Sending…", "sending_otp": "Sending…",
"otp_sent": "Verification code sent to your email. If you received several, any code from the last few minutes works.", "otp_sent": "A verification code has been sent to the registered email address. Codes received within their validity period remain usable.",
"otp_btn": "Verify & Sign In", "otp_btn": "Verify & Sign In",
"resend_in": "s", "resend_in": "s",
"passkey_btn": "Sign in with Passkey", "passkey_btn": "Sign in with Passkey",
"passkey_email_hint": "Enter your registered email above to use a passkey, or leave it empty to sign in with a discoverable passkey.", "passkey_email_hint": "To sign in with a Passkey, enter the registered email address or leave it blank for passwordless sign-in.",
"bind_code": "Bind Code", "bind_code": "Bind Code",
"bind_code_placeholder": "e.g., ABCD2345", "bind_code_placeholder": "e.g., ABCD2345",
"bind_hint": "In Minecraft (Java Edition), join the address below. The login server gives a one-time bind code on your first join; after that, type /link in-game for a new one.", "bind_hint": "Connect to the address below using Minecraft Java Edition. The login service provides a one-time binding code on first connection. Run /link in-game to obtain subsequent codes.",
"bind_hint_no_address": "In Minecraft (Java Edition), join this server. The login server gives a one-time bind code on your first join; after that, type /link in-game for a new one.", "bind_hint_no_address": "Connect to this server using Minecraft Java Edition. The login service provides a one-time binding code on first connection. Run /link in-game to obtain subsequent codes.",
"no_owner_title": "No Owner yet, so nobody can sign in", "no_owner_title": "Owner account not initialized; sign-in unavailable",
"no_owner_subtitle": "The Owner is the account that owns this server", "no_owner_subtitle": "Owner is the platform owner account",
"no_owner_intro": "The host administrator needs to initialize the first Owner login. Minecraft is not required.", "no_owner_intro": "The Owner sign-in method has not been initialized. The host administrator must complete the following setup. Minecraft startup is not required.",
"no_owner_step_setup": "Run this command on the host to finish deployment and configure panel access:", "no_owner_step_setup": "Run this command on the host to finish deployment and configure panel access:",
"no_owner_step_link": "Open the one-time setup link in your browser, record an email, and create a passkey. Link a Minecraft role later from Account.", "no_owner_step_link": "Open the one-time setup link provided by the terminal in a browser, register an email address, and create a Passkey. A Minecraft profile can be linked from Account.",
"no_owner_recheck": "Finished setup? Check again", "no_owner_recheck": "Finished setup? Check again",
"no_owner_rechecking": "Checking…", "no_owner_rechecking": "Checking…",
"no_owner_still_unbound": "There is still no Owner. Check that the terminal has shown the setup link.", "no_owner_still_unbound": "No initialized Owner account was detected. Confirm that the terminal generated a setup link and that account initialization is complete.",
"bind_btn": "Verify & Sign In", "bind_btn": "Verify & Sign In",
"binding": "Verifying…", "binding": "Verifying…",
"op_hint": "Staff only: a code is emailed to you, and an online operator must approve the request in-game before you can sign in.", "op_hint": "Staff only: a code is emailed to you, and an online operator must approve the request in-game before you can sign in.",
"op_start_btn": "Request operator sign-in", "op_start_btn": "Request operator sign-in",
"op_approve_hint": "A code has been emailed to you (if you received several, any recent one works). Ask an online operator to approve this request in-game:", "op_approve_hint": "A verification code has been sent to the registered email address. Codes received within their validity period remain usable. An online administrator must approve this request in-game:",
"op_waiting": "Waiting for in-game approval…", "op_waiting": "Waiting for in-game approval…",
"op_approved": "Approved — enter the code from your email.", "op_approved": "Request approved. Enter the verification code from the email.",
"op_restart": "Start over", "op_restart": "Start over",
"op_expires_in": "This request expires in {{time}}.", "op_expires_in": "This request expires in {{time}}.",
"op_expired": "This request expired before it was used. Request a new one to sign in.", "op_expired": "This request expired before it was used. Request a new one to sign in.",
"op_request_again": "Request again", "op_request_again": "Request again",
"op_poll_retrying": "Can't reach Felis right now. Still waiting, checking less often…", "op_poll_retrying": "Unable to connect to Felis. Polling will continue at a reduced frequency.",
"saving": "Saving…", "saving": "Saving…",
"setup_title": "Set up your account", "setup_title": "Account initialization",
"setup_welcome": "Welcome, {{name}}", "setup_welcome": "Welcome, {{name}}",
"setup_preparing": "Preparing setup…", "setup_preparing": "Preparing setup…",
"setup_entering": "Entering the console…", "setup_entering": "Entering the console…",
"setup_invalid_title": "Can't start setup", "setup_invalid_title": "Unable to start initialization",
"setup_invalid_subtitle": "This setup link is invalid or has already been used", "setup_invalid_subtitle": "The setup link is invalid or has already been used",
"setup_invalid_hint_prefix": "Re-run ", "setup_invalid_hint_prefix": "Re-run ",
"setup_invalid_hint_suffix": " on the server to get a fresh setup link, or head to the sign-in page.", "setup_invalid_hint_suffix": " on the server to get a fresh setup link, or head to the sign-in page.",
"setup_failed_title": "Setup didn't start", "setup_failed_title": "Initialization failed to start",
"setup_failed_subtitle": "The server couldn't sign you in just now. The setup link is kept, so you can try again.", "setup_failed_subtitle": "The server could not complete sign-in. The setup link remains valid. Retry the operation.",
"setup_retry": "Try again", "setup_retry": "Retry",
"setup_goto_login": "Go to sign in", "setup_goto_login": "Go to sign in",
"setup_email_step": "Step 1 · Add your email", "setup_email_step": "Step 1 · Add your email",
"setup_email_desc": "We'll save this address for your account. You can configure email delivery (SMTP) and verify it later from Settings — it isn't required to finish setup.", "setup_email_desc": "The email address will be saved to the current account. Mail delivery (SMTP) and email verification can be configured later and are not required for account initialization.",
"setup_email_save": "Save & continue", "setup_email_save": "Save & continue",
"setup_passkey_step": "Step 2 · Register a passkey", "setup_passkey_step": "Step 2 · Register a passkey",
"setup_passkey_desc": "Create a passkey with your fingerprint, face, or device PIN. It's how you'll sign in to the console — required to finish setup.", "setup_passkey_desc": "Create a Passkey using a fingerprint, facial recognition, or device PIN to sign in to the console. Passkey registration is required to complete initialization.",
"setup_create_passkey": "Create passkey", "setup_create_passkey": "Create passkey",
"setup_registering": "Registering…", "setup_registering": "Registering…",
"setup_default_passkey_name": "Default passkey", "setup_default_passkey_name": "Default passkey",
"session_ended_notice": "Your session expired or was revoked. Sign in again to go back to the page you were on.", "session_ended_notice": "The sign-in session has expired or been revoked. Sign in again to return to the originally requested page.",
"setup_game_optional": "Finish login setup to open the panel. A Minecraft role can be linked later from Account." "setup_game_optional": "The management panel is available after sign-in setup is complete. A Minecraft profile can be linked later from Account."
} }
+22 -22
View File
@@ -1,6 +1,6 @@
{ {
"title": "Backups & restore", "title": "Backups & restore",
"subtitle": "A world that was played is backed up automatically once a day after its server stops, and again before it is archived after long inactivity. You can use these backups to restore the world — the server must be stopped first.", "subtitle": "Servers that have run and are stopped are backed up daily. A backup is also created before long-term idle reclamation. Stop the server before restoring a world.",
"back_to_console": "Back to console", "back_to_console": "Back to console",
"not_yours_title": "No permission to access backups", "not_yours_title": "No permission to access backups",
"not_yours_body": "Only the owner or an admin can view, restore and delete this server's backups.", "not_yours_body": "Only the owner or an admin can view, restore and delete this server's backups.",
@@ -14,27 +14,27 @@
"reason_label": "Reason: {{reason}}", "reason_label": "Reason: {{reason}}",
"backup_now": "Back up now", "backup_now": "Back up now",
"backup_in_progress": "Starting backup…", "backup_in_progress": "Starting backup…",
"backup_started": "Backup started — its progress shows under recent operations below.", "backup_started": "The backup task has started. Progress is available under Recent operations.",
"backup_world_busy": "A backup, restore or world export is already running; back up once it finishes.", "backup_world_busy": "A backup, restoration, or world export is in progress. Wait for the current operation to complete before starting a backup.",
"backup_requires_stopped": "Stop the server before backing it up.", "backup_requires_stopped": "Stop the server before backing it up.",
"export_world": "Export world", "export_world": "Export world",
"export_world_hint": "Download the server's current world as a .tar.gz archive.", "export_world_hint": "Package the current world as a .tar.gz file for download.",
"export_preparing": "Preparing…", "export_preparing": "Preparing…",
"export_requires_stopped": "Stop the server before exporting its world.", "export_requires_stopped": "Stop the server before exporting its world.",
"export_world_busy": "A backup, restore or export is running on this world; export once it finishes.", "export_world_busy": "The world is undergoing backup, restoration, or export. Wait for the current operation to complete before starting an export.",
"export_another": "Another download is being prepared; wait for it to start.", "export_another": "Another download is being prepared. Retry after preparation completes.",
"export_progress_world": "Archiving the world — the download starts by itself once it is ready. Keep this page open; the server can't start until the download ends.", "export_progress_world": "The world is being archived. The download starts automatically after preparation. Keep the page open. The server cannot start until the download ends.",
"export_progress_backup": "Preparing the backup — the download starts by itself in a moment. Keep this page open.", "export_progress_backup": "The backup download is being prepared. Transfer starts automatically after preparation. Keep the page open.",
"export_started_world": "Download started: your browser is saving {{filename}}. The server can start again once it finishes.", "export_started_world": "Download started: your browser is saving {{filename}}. The server can start again once it finishes.",
"export_started_backup": "Download started: your browser is saving {{filename}}.", "export_started_backup": "Download started: your browser is saving {{filename}}.",
"export_failed": "The export failed and nothing was downloaded.", "export_failed": "Export failed. No content was downloaded.",
"export_failed_because": "The export failed: {{reason}}", "export_failed_because": "The export failed: {{reason}}",
"download_btn": "Download backup", "download_btn": "Download backup",
"expires_in": "Expires {{when}}", "expires_in": "Expires {{when}}",
"expired": "Expired", "expired": "Expired",
"former_owner": "Former owner: {{owner}}", "former_owner": "Former owner: {{owner}}",
"empty_title": "No backups for this server", "empty_title": "No backups for this server",
"empty_hint": "A scheduled backup is saved once the server has run and stopped — or create one now with “Back up now”.", "empty_hint": "Servers that have run and are stopped receive scheduled backups automatically. Select Back up now to create a manual backup.",
"jobs_title": "Recent operations", "jobs_title": "Recent operations",
"jobs_empty": "No recent backup, restore or export operations.", "jobs_empty": "No recent backup, restore or export operations.",
"job_backup": "Backup", "job_backup": "Backup",
@@ -53,32 +53,32 @@
"chain_abandoned": "Not restored; the world was left as it was", "chain_abandoned": "Not restored; the world was left as it was",
"chain_abandoned_because": "Not restored: {{reason}}", "chain_abandoned_because": "Not restored: {{reason}}",
"chain_abandoned_snapshot_failed": "Snapshot failed, so the world was left as it was", "chain_abandoned_snapshot_failed": "Snapshot failed, so the world was left as it was",
"chain_abandoned_not_configured": "Restore is not configured; nothing was restored", "chain_abandoned_not_configured": "Restoration not performed: restore service not configured",
"chain_abandoned_server_gone": "The server no longer exists; nothing was restored", "chain_abandoned_server_gone": "Restoration not performed: server no longer exists",
"chain_abandoned_server_started": "The server was started before the restore could run", "chain_abandoned_server_started": "The server was started before the restore could run",
"chain_abandoned_restore_busy": "Another restore is running on this world", "chain_abandoned_restore_busy": "Another restore is running on this world",
"restore_btn": "Restore this backup", "restore_btn": "Restore this backup",
"restore_btn_short": "Restore", "restore_btn_short": "Restore",
"restore_confirm": "Restoring a backup will stop the server (all online players will be disconnected) and completely overwrite the current world with this backup. Without a safety snapshot this cannot be undone. Do you want to continue?", "restore_confirm": "Restoring a backup will stop the server (all online players will be disconnected) and completely overwrite the current world with this backup. Without a safety snapshot this cannot be undone. Do you want to continue?",
"restore_confirm_safe": "Restoring a backup will stop the server (all online players will be disconnected), take a safety snapshot of the current world, and overwrite the world with this backup once the snapshot succeeds. If you picked the wrong backup, restore the pre-restore snapshot to get it back. Continue?", "restore_confirm_safe": "Restoration stops the server and disconnects online players, creates a safety snapshot of the current world, and overwrites the world with the selected backup after the snapshot succeeds. Restore the pre-restore snapshot if the result must be reversed. Confirm restoration?",
"safety_snapshot_label": "Take a safety snapshot of the current world first (recommended)", "safety_snapshot_label": "Take a safety snapshot of the current world first (recommended)",
"safety_snapshot_hint": "The restore starts once the snapshot succeeds; if it fails, nothing is restored and the world stays as it is.", "safety_snapshot_hint": "The restore starts once the snapshot succeeds; if it fails, nothing is restored and the world stays as it is.",
"safety_snapshot_off_hint": "No snapshot: the current world is overwritten right away and cannot be recovered.", "safety_snapshot_off_hint": "No safety snapshot will be created. The current world is overwritten directly. Existing data without another backup cannot be recovered.",
"restore_confirm_yes": "Confirm restore", "restore_confirm_yes": "Confirm restore",
"restore_confirm_yes_unsafe": "Overwrite and restore", "restore_confirm_yes_unsafe": "Overwrite and restore",
"cancel": "Cancel", "cancel": "Cancel",
"restore_started_short": "Restore started", "restore_started_short": "Restore started",
"restore_snapshot_started_short": "Snapshot, then restore", "restore_snapshot_started_short": "Snapshot, then restore",
"stopping": "Stopping the server (players online get a 30-second warning first)…", "stopping": "Stopping the server. If players are online, an in-game notice is sent 30 seconds before disconnection…",
"restoring": "Restoring…", "restoring": "Restoring…",
"stop_timeout": "The server did not stop in time. Close this window and try again shortly.", "stop_timeout": "The server did not stop in time. Close this window and try again shortly.",
"corrupt_badge": "Corrupt", "corrupt_badge": "Corrupt",
"corrupt_hint": "A read-back found this archive no longer matches what was written, so it can't be restored intact. It stays listed until it expires so the loss is visible, unless you delete it.", "corrupt_hint": "Archive read-back verification failed. The archive cannot be restored intact. It is retained until expiry for investigation and can be deleted manually if no longer required.",
"corrupt_short": "Corrupt", "corrupt_short": "Corrupt",
"verified_at": "Verified {{when}}", "verified_at": "Verified {{when}}",
"skipped_entries_one": "{{count}} entry not archived", "skipped_entries_one": "{{count}} entry not archived",
"skipped_entries_other": "{{count}} entries not archived", "skipped_entries_other": "{{count}} entries not archived",
"skipped_entries_hint": "The world held symlinks or other entries that aren't plain files, so the archive skipped them and a restore won't bring them back.", "skipped_entries_hint": "Symbolic links and other non-regular entries were excluded from the archive and will not be included in restoration.",
"col_created": "Backup Time", "col_created": "Backup Time",
"col_size": "Size", "col_size": "Size",
"col_expires": "Expires", "col_expires": "Expires",
@@ -86,12 +86,12 @@
"col_actions": "Actions", "col_actions": "Actions",
"delete_btn": "Delete backup", "delete_btn": "Delete backup",
"delete_title": "Delete this backup?", "delete_title": "Delete this backup?",
"delete_confirm": "The backup from {{relative}} ({{absolute}}, {{size}}) leaves this list at once and can no longer be restored. This cannot be undone.", "delete_confirm": "The backup from {{relative}} ({{absolute}}), size {{size}}, will be removed from the list immediately and will no longer be available for restoration. This operation cannot be undone.",
"delete_cleanup_note": "Its archive is removed at the next daily cleanup, and its off-site copy at the next sync.", "delete_cleanup_note": "Its archive is removed at the next daily cleanup, and its off-site copy at the next sync.",
"delete_archive_warning": "This backup is the archive kept when the world was reclaimed or released. Unless the world on the server has been restored from it, deleting it loses that world for good.", "delete_archive_warning": "This backup is an archive created during world reclamation or server release. After deletion, world data without another recoverable copy cannot be restored.",
"delete_only_warning": "This is the only backup this server has.", "delete_only_warning": "This is the server’s only current backup.",
"delete_confirm_yes": "Delete backup", "delete_confirm_yes": "Delete backup",
"delete_restore_busy": "A restore may be reading this backup. Delete it once the restore finishes.", "delete_restore_busy": "A restoration task may be reading this backup. Wait for restoration to complete before deleting it.",
"deleted": "Backup deleted.", "deleted": "Backup deleted.",
"delete_gone": "This backup was already gone; the list has been refreshed." "delete_gone": "The backup no longer exists. The list has been refreshed."
} }
+12 -12
View File
@@ -1,19 +1,19 @@
{ {
"loading": "Loading…", "loading": "Loading…",
"try_again": "Try again", "try_again": "Retry",
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"create": "Create", "create": "Create",
"creating": "Creating…", "creating": "Creating…",
"close_sr": "Close", "close_sr": "Close",
"not_authorized_title": "Not authorized", "not_authorized_title": "Not authorized",
"not_authorized_body": "This area is for platform administrators. If you reached it by typing the URL, your account does not have admin access — and the API would refuse the request anyway.", "not_authorized_body": "The current account lacks platform administration permissions and cannot access this page.",
"back_to_dashboard": "Back to your dashboard", "back_to_dashboard": "Back to your dashboard",
"not_wired_yet": "Not wired yet", "not_wired_yet": "Feature not integrated",
"pending_backend_prefix": "pending backend: ", "pending_backend_prefix": "pending backend: ",
"checking_access": "Checking access…", "checking_access": "Checking access…",
"access_unknown_title": "Couldn't check your access", "access_unknown_title": "Unable to verify account permissions",
"access_unknown_body": "Your account details didn't load, so this page stays hidden until they do.", "access_unknown_body": "Account information could not be loaded. Page content is unavailable until access permissions are verified.",
"loading_config": "Loading config…", "loading_config": "Loading config…",
"brand_name": "Felis", "brand_name": "Felis",
"brand_tagline": "K8s-native Minecraft orchestration", "brand_tagline": "K8s-native Minecraft orchestration",
@@ -23,22 +23,22 @@
"pagination_prev": "Previous", "pagination_prev": "Previous",
"pagination_next": "Next", "pagination_next": "Next",
"pagination_of": "Page {{current}} of {{total}}", "pagination_of": "Page {{current}} of {{total}}",
"crash_title": "This page hit an error", "crash_title": "Page loading error",
"crash_body": "The panel ran into a problem rendering this page. Reloading usually fixes it; if it keeps happening, send the error details below to your admin.", "crash_body": "Page rendering failed. Reload the page. If the issue persists, provide the error details to an administrator.",
"crash_updated_title": "The panel was updated", "crash_updated_title": "The panel was updated",
"crash_updated_body": "A new version of the panel was just released. Reload to open this page.", "crash_updated_body": "The panel version has changed. Reload the page.",
"crash_reload": "Reload", "crash_reload": "Reload",
"crash_details": "Error details", "crash_details": "Error details",
"crash_copy": "Copy error details", "crash_copy": "Copy error details",
"not_found_title": "Page not found", "not_found_title": "Page not found",
"not_found_body": "The name in the link is not valid, or the page does not exist.", "not_found_body": "The name in the link is invalid, or the requested page does not exist.",
"connection_lost": "Can't reach Felis: the network is down, or your Cloudflare Access sign-in expired. This clears by itself once the network is back; an expired Access sign-in needs a page reload.", "connection_lost": "Unable to connect to Felis. The network may be unavailable or the Cloudflare Access session may have expired. The connection resumes automatically when the network recovers. If the Access session has expired, reload the page and sign in again.",
"reload_page": "Reload page", "reload_page": "Reload page",
"config_unavailable": "The panel configuration (/config.json) did not load, so server addresses and links shown here may be wrong. Reload to try again.", "config_unavailable": "Panel configuration (/config.json) failed to load. Server addresses and links may be inaccurate. Reload the page to retry.",
"build_dev": "dev", "build_dev": "dev",
"open_menu": "Open menu", "open_menu": "Open menu",
"copy_address": "Copy address {{address}}", "copy_address": "Copy address {{address}}",
"address_copied": "Copied", "address_copied": "Copied",
"address_copy_failed": "Selected — copy it from the menu", "address_copy_failed": "Selected — copy it from the menu",
"refresh_failed": "Couldn't refresh; what you see is the last status read." "refresh_failed": "Refresh failed. The last successfully retrieved status is displayed."
} }
+15 -15
View File
@@ -1,7 +1,7 @@
{ {
"title": "Dashboard", "title": "Dashboard",
"subtitle": "Your fleet at a glance.", "subtitle": "Server runtime overview.",
"no_servers_hint": "Create your first server or claim an unowned one.", "no_servers_hint": "Create a server, claim an existing server, or request allocation from an administrator.",
"stat_servers": "Servers", "stat_servers": "Servers",
"stat_running": "Running", "stat_running": "Running",
"stat_players_online": "Players online", "stat_players_online": "Players online",
@@ -12,10 +12,10 @@
"active_load": "Player Load Rate", "active_load": "Player Load Rate",
"status_distribution": "Node Status Distribution", "status_distribution": "Node Status Distribution",
"account_linked_title": "Account Linked", "account_linked_title": "Account Linked",
"account_linked_desc": "Your identity is bound to a Minecraft UUID. You have full server ownership and wake permissions.", "account_linked_desc": "The Web account is linked to a Minecraft profile. Authorized claiming and startup operations are available.",
"account_unlinked_title": "Account Unlinked", "account_unlinked_title": "Account Unlinked",
"account_unlinked_desc": "You must link a Minecraft character to claim or operate servers. Go to Account to bind now.", "account_unlinked_desc": "Link a Minecraft profile from Account before claiming or operating servers.",
"go_link": "Bind Now", "go_link": "Link account",
"link_status_checking": "Checking whether a game character is linked…", "link_status_checking": "Checking whether a game character is linked…",
"link_status_failed": "Link status unavailable", "link_status_failed": "Link status unavailable",
"system_specs": "Specs & Metrics", "system_specs": "Specs & Metrics",
@@ -30,13 +30,13 @@
"spec_failed_ratio": "Failed Rate", "spec_failed_ratio": "Failed Rate",
"no_servers_monitor": "No active instances in the cluster", "no_servers_monitor": "No active instances in the cluster",
"images_count": "Whitelisted backend images available", "images_count": "Whitelisted backend images available",
"images_security_note": ". All backend images undergo Trivy CVE vulnerability scanning before deployment and run in isolated security containers.", "images_security_note": ". Image availability is controlled by the whitelist. Build scan results are available in build records.",
"manage_game_character": "Manage Game Character", "manage_game_character": "Manage Game Character",
"instances_count": "{{count}} instances", "instances_count": "{{count}} instances",
"quick_start_guide": "Quick Start Guide:", "quick_start_guide": "Quick Start Guide:",
"quick_start_step1": "Claiming an unowned server automatically adds your Minecraft UUID to the access whitelist.", "quick_start_step1": "After a server is claimed, the system adds the corresponding Minecraft UUID to its access whitelist.",
"quick_start_step2": "When a server sits idle with no players online, the platform automatically sleeps it to conserve resources.", "quick_start_step2": "When a server reaches its configured idle duration, it stops automatically and saves the world to release runtime resources.",
"quick_start_step3": "All game traffic is routed through the Velocity gateway — simply connect to your dedicated subdomain.", "quick_start_step3": "The Velocity gateway provides game access through each server’s dedicated subdomain.",
"legend_title": "3D Node Monitor Legend", "legend_title": "3D Node Monitor Legend",
"legend_running": "Running", "legend_running": "Running",
"legend_starting": "Starting", "legend_starting": "Starting",
@@ -44,11 +44,11 @@
"legend_stopped": "Sleeping", "legend_stopped": "Sleeping",
"legend_failed": "Failed", "legend_failed": "Failed",
"legend_unknown": "Unknown", "legend_unknown": "Unknown",
"welcome_title": "Welcome to Felis Lolicon", "welcome_title": "Felis orchestration platform",
"welcome_desc": "You have logged into the User Dashboard. Go to 'My Servers' to view and claim nodes allocated to you, or manage their sleep, wake, and console logs.", "welcome_desc": "Use My Servers to view, claim, and manage authorized servers, including startup, shutdown, and log inspection.",
"go_to_servers_btn": "Manage My Servers", "go_to_servers_btn": "Manage My Servers",
"fleet_flat_webgl": "WebGL is unavailable in this browser, so the fleet is shown flat.", "fleet_flat_webgl": "WebGL is unavailable. The fleet is displayed in a two-dimensional view.",
"fleet_flat_error": "The 3D view failed to load, so the fleet is shown flat.", "fleet_flat_error": "The 3D view failed to load. The fleet is displayed in a two-dimensional view.",
"fleet_flat_empty": "No servers yet", "fleet_flat_empty": "No servers available",
"staff_unlinked_desc": "No Minecraft role is linked yet. Panel management is available; choose an authentication source and link a role from Account when you are ready to play." "staff_unlinked_desc": "No Minecraft profile is linked. Panel administration is available. Select an authentication source and link a profile from Account before joining the game."
} }
+101 -101
View File
@@ -1,147 +1,147 @@
{ {
"local_auth_disabled": "Direct sign-in is turned off here — reach this console through your organization's secure access.", "local_auth_disabled": "Local sign-in is disabled in this deployment. Access the console through the organization’s secure entry point.",
"staff_account": "This is a staff account — use Operator sign-in instead.", "staff_account": "This is a staff account — use Operator sign-in instead.",
"not_linked": "Link your Minecraft account before claiming (Account → Link).", "not_linked": "Link your Minecraft account before claiming (Account → Link).",
"invalid_code": "That code is invalid or expired — request a fresh one and try again.", "invalid_code": "The binding code is invalid or expired. Obtain a new code.",
"already_linked": "That Minecraft account is already linked to another user.", "already_linked": "That Minecraft account is already linked to another user.",
"bad_mc_uuid": "That isn't a Minecraft UUID. It looks like 069a79f4-44e9-4726-a5be-fca90e38aaf5, with or without the dashes.", "bad_mc_uuid": "Invalid Minecraft UUID format. Example: 069a79f4-44e9-4726-a5be-fca90e38aaf5. Hyphens are optional.",
"quota_exceeded": "You have reached your server quota.", "quota_exceeded": "You have reached your server quota.",
"already_claimed": "Someone else just claimed this server.", "already_claimed": "This server has been claimed by another account.",
"image_not_whitelisted": "That image is not on the whitelist.", "image_not_whitelisted": "That image is not on the whitelist.",
"image_not_in_registry": "The internal registry doesn't hold that image tag — it was never pushed or has been deleted. Rebuild or push it, then try again.", "image_not_in_registry": "The image tag is absent from the internal registry. It may not have been pushed or may have been deleted. Rebuild or push the image before retrying.",
"registry_unavailable": "Can't reach the internal registry to look up which build that image is — try again shortly.", "registry_unavailable": "Unable to connect to the internal registry. The image build version is unconfirmed. Retry later.",
"image_change_unconfirmed": "Changing the image opens the world with the new build's Minecraft version, and upgraded chunks can't be opened by the old one again. Back the world up, tick the confirmation, then save.", "image_change_unconfirmed": "After an image change, the next startup opens the world with the new build’s Minecraft version. Upgraded chunks are not compatible with older versions. Back up the world and select the confirmation before saving.",
"subdomain_taken": "That subdomain is already in use.", "subdomain_taken": "That subdomain is already in use.",
"already_exists": "A server with that name already exists.", "already_exists": "A server with that name already exists.",
"world_volume_exists": "An earlier server with that name left its world volume behind. Choose another name, or ask the operator to delete the old volume.", "world_volume_exists": "A world volume from a server of the same name still exists. Use a different name or request an operator to verify and process the existing volume.",
"server_retiring": "This server has been given up or is being deleted, which happens at the next daily reclaim run. It can't be started or claimed unless that is cancelled.", "server_retiring": "The server is pending release or deletion at the next daily reclaim run. Startup and claiming are unavailable until the request is cancelled.",
"confirm_mismatch": "The name you typed doesn't match the server's name.", "confirm_mismatch": "The entered name does not match the server name.",
"system_server": "A system server is managed by the platform and can't be given up or deleted.", "system_server": "System servers are managed by the platform and cannot be released or deleted.",
"world_volume_orphaned": "This server's MinecraftServer was deleted by hand but its world volume is still there. Ask the operator to archive and remove that volume first, then delete the server.", "world_volume_orphaned": "The server’s MinecraftServer resource was manually deleted, but its world volume remains. An operator must archive and remove the remaining volume before deleting the server record.",
"cooldown": "Wake is cooling down — try again shortly.", "cooldown": "The startup cooldown has not elapsed. Retry later.",
"not_running": "The server isn't running — wake it before managing access.", "not_running": "The server is not running. Start it before managing access permissions.",
"console_unavailable": "Can't reach the server console right now — try again shortly.", "console_unavailable": "The server console is unavailable. Retry later.",
"luckperms_missing": "LuckPerms isn't installed on this server, so permission and group changes have no effect. For the lobby, ask the operator to re-run the installer; any other server needs the LuckPerms plugin added first.", "luckperms_missing": "LuckPerms is not installed. Permission and group changes cannot be applied. An operator must rerun the installer for the lobby. Other servers require the LuckPerms plugin to be installed first.",
"no_backup": "There's no restorable backup for this server yet.", "no_backup": "No restorable backup is available for this server.",
"backup_corrupt": "This backup failed its read-back check and can't be restored intact — pick another backup.", "backup_corrupt": "The backup failed read-back verification and cannot be restored intact. Select another backup.",
"not_stopped": "Stop the server completely first — this changes its world volume, which the running server holds.", "not_stopped": "This operation modifies the world volume, which is exclusively mounted by the running server. Stop the server completely first.",
"maintenance_in_progress": "This server's world is busy with a restore, backup, world export, file download, file change or idle reclaim. Try again once it finishes. A restore, backup or file change usually takes a minute or two; an export or a download lasts until the browser has all of it, and an idle reclaim can take longer on a large world.", "maintenance_in_progress": "The world is undergoing restoration, backup, export, file download, file modification, or reclamation. Retry after the operation completes. Exports and downloads remain active until the browser completes the transfer.",
"no_world_volume": "This server has no world volume yet — start it once so it is created, then retry.", "no_world_volume": "The server has no world volume. Start the server to create it, then retry.",
"file_changed": "This file changed after you opened it (another manager saved it, or the server rewrote it on its last run), so your save was not written, to keep that change.", "file_changed": "The file was modified by another administrator or server process after it was opened. The save was not written to avoid overwriting existing changes.",
"volume_full": "The server's volume is full, so the change was not written and the files there are unchanged. Delete files it no longer needs, or ask an admin to grow its volume.", "volume_full": "The server volume is full. The change was not written and existing files remain unchanged. Remove unnecessary files or request a capacity increase from an administrator.",
"backup_cooldown": "This server was backed up moments ago, and manual backups have a cooldown — try again in a few minutes.", "backup_cooldown": "The manual backup cooldown has not elapsed. Retry later.",
"backup_store_full": "The backup store is full, so manual backups are paused — ask an administrator to free space.", "backup_store_full": "The backup store is full, so manual backups are paused — ask an administrator to free space.",
"restore_unavailable": "Restore isn't available right now — try again later.", "restore_unavailable": "Restoration is currently unavailable. Retry later.",
"export_busy": "Too many exports are being prepared: one at a time per person, two at a time across the platform, and six an hour per person. Try again in a few minutes.", "export_busy": "Export limits have been reached: one concurrent request per account, two across the platform, and six per account per hour. Retry after existing tasks complete or rate limits reset.",
"export_expired": "This download has expired or was already used — start the export again.", "export_expired": "The download link has expired or has already been used. Export the content again.",
"export_not_ready": "The download isn't ready yet — wait a moment and try again.", "export_not_ready": "Download preparation is incomplete. Retry later.",
"export_unavailable": "Downloading worlds and backups isn't set up on this deployment — ask an administrator.", "export_unavailable": "World and backup downloads are not configured in this deployment. Contact an administrator.",
"session_expired": "Your session expired — please sign in again.", "session_expired": "The session has expired. Sign in again.",
"forbidden": "You are not allowed to do that.", "forbidden": "The current account is not authorized to perform this operation.",
"self_protected": "You can't do that to the account you're signed in with.", "self_protected": "This operation is not permitted on the signed-in account.",
"owner_protected": "The owner account can't be demoted, disabled or deleted from the panel. Only the host's break-glass console (sudo felis breakGlass) manages it.", "owner_protected": "The Owner account cannot be demoted, disabled, or deleted from the panel. Manage it through the host recovery console (sudo felis breakGlass).",
"session_not_found": "That session has already ended.", "session_not_found": "The session has ended or does not exist.",
"generic": "Something went wrong.", "generic": "The operation failed. Retry later.",
"otp_resend_cooldown": "Verification code requested too frequently, please try again later.", "otp_resend_cooldown": "Verification code requested too frequently, please try again later.",
"otp_locked": "Too many incorrect attempts, please request a new verification code.", "otp_locked": "Too many incorrect attempts, please request a new verification code.",
"otp_account_locked": "Too many wrong codes in 24 hours — email codes for this account are paused. Try again later or use a passkey.", "otp_account_locked": "Too many wrong codes in 24 hours — email codes for this account are paused. Try again later or use a passkey.",
"rate_limited": "Too many sign-in attempts from your network — wait a minute and try again.", "rate_limited": "Sign-in requests are rate-limited. Retry after the limit resets.",
"mail_rate_limited": "This server is sending too much mail right now, so codes are paused. Try again shortly or use a passkey.", "mail_rate_limited": "Mail delivery is rate-limited. Verification codes cannot currently be sent. Retry later or use a Passkey.",
"passkey_challenge_invalid": "Passkey challenge is invalid or expired, please try again.", "passkey_challenge_invalid": "Passkey challenge is invalid or expired, please try again.",
"invalid_attestation": "Could not verify this Passkey, please try again.", "invalid_attestation": "Could not verify this Passkey, please try again.",
"passkey_already_bound": "This Passkey is already bound to another account.", "passkey_already_bound": "This Passkey is already bound to another account.",
"passkey_unavailable": "Passkey subsystem is not available right now.", "passkey_unavailable": "Passkey authentication is currently unavailable.",
"last_passkey": "This is your only passkey and your email is not verified, so it can't be removed. Verify an email or add another passkey first.", "last_passkey": "The account has only one Passkey and no verified email address. Removal is not allowed. Verify an email address or register another Passkey first.",
"passkey_not_allowed": "The operation was cancelled by the user or browser.", "passkey_not_allowed": "The operation was cancelled by the user or browser.",
"passkey_aborted": "Passkey registration was aborted.", "passkey_aborted": "Passkey registration was aborted.",
"bad_name": "That server name is invalid — use 3–32 lowercase letters, digits or dashes, and avoid reserved names.", "bad_name": "Invalid server name. Use 3–32 lowercase letters, digits, or hyphens, excluding reserved names.",
"bad_subdomain": "That subdomain is invalid — use 3–32 lowercase letters, digits or dashes, and avoid reserved names.", "bad_subdomain": "Invalid subdomain. Use 3–32 lowercase letters, digits, or hyphens, excluding reserved names.",
"bad_display_name": "That display name is invalid — use at most 64 characters, all visible ones or spaces, with no line breaks, tabs or invisible control characters.", "bad_display_name": "Invalid display name. Use at most 64 visible characters or spaces, excluding line breaks, tabs, and control characters.",
"at_capacity": "The cluster is at its running-server cap — try again once a server stops.", "at_capacity": "The cluster’s running-server limit has been reached. Retry after another server stops.",
"storage_immutable": "Storage size can't be changed online.", "storage_immutable": "Storage capacity cannot be changed online.",
"bad_idle_stop": "Idle stop must be between 1 minute and 24 hours, or Never.", "bad_idle_stop": "Idle stop must be between 1 minute and 24 hours, or Never.",
"email_taken": "That email is already verified on another account — sign in with it or use another address.", "email_taken": "That email is already verified on another account — sign in with it or use another address.",
"mail_undeliverable": "The verification email could not be delivered — try again later, or ask the operator to check the mail relay.", "mail_undeliverable": "The verification email could not be delivered — try again later, or ask the operator to check the mail relay.",
"mail_unavailable": "This server can't send email codes because no mail relay is set up. Sign in with a passkey, or ask the server operator to configure email.", "mail_unavailable": "Mail delivery is not configured, so verification codes cannot be sent. Sign in with a Passkey or request mail configuration from an administrator.",
"bad_path": "That path is invalid — use a relative path inside the world directory.", "bad_path": "Invalid path. Use a relative path within the world directory.",
"too_large": "That is larger than the size limit.", "too_large": "The content exceeds the size limit.",
"files_timeout": "The file operation timed out — try again shortly.", "files_timeout": "The file operation timed out — try again shortly.",
"files_unavailable": "File editing isn't available right now.", "files_unavailable": "File management is currently unavailable.",
"file_exists": "Something with that name is already there. Pick another name, or rename or delete the one that is there first.", "file_exists": "A file or folder with this name already exists in the current directory. Use a different name, or rename or delete the existing entry.",
"upload_staging_full": "The panel's upload space is nearly full right now, so the file was not passed on. Try again later, or ask an admin to free space on the uploads volume.", "upload_staging_full": "Upload staging space is insufficient. The file transfer was not completed. Retry later or request cleanup of the uploads volume from an administrator.",
"upload_incomplete": "The upload stopped before the whole file arrived, so nothing was changed. Try again.", "upload_incomplete": "The upload was interrupted before the complete file was transferred. This operation did not modify files. Upload the file again.",
"length_required": "The upload did not say how large it is, so it was refused. Upload it again from the panel.", "length_required": "The upload request omitted the file size and was rejected. Upload the file again through the panel.",
"digest_mismatch": "The file was changed on its way to the server, so it was refused and nothing was written. Try again.", "digest_mismatch": "File transfer verification failed. The server rejected the transfer and the file was not written. Upload the file again.",
"digest_required": "The request came without a checksum, so it was refused. Reload the panel and try again.", "digest_required": "The request omitted its checksum and was rejected. Reload the panel and retry.",
"bad_digest": "The request's checksum was malformed, so it was refused. Reload the panel and try again.", "bad_digest": "The request checksum format is invalid and the request was rejected. Reload the panel and retry.",
"file_unreadable": "The file could not be read: it was changed, moved or deleted after it was picked. Pick it again and upload.", "file_unreadable": "The selected file cannot be read. It may have been modified, moved, or deleted. Select the file again before uploading.",
"read_damaged": "The file arrived damaged, so it was not opened: saving it would write the damage back. Open it again.", "read_damaged": "File transfer verification failed. The file was not opened to prevent subsequent saves from writing corrupted data. Read the file again.",
"upload_not_found": "This upload is gone: it was cancelled, already landed, sat idle for 6 hours, or the panel service restarted. Upload the file again.", "upload_not_found": "The multipart upload record is unavailable. It may have been cancelled, completed, idle for over six hours, or invalidated by a service restart. Upload the file again.",
"too_many_uploads": "You already have 4 large uploads in progress. Wait for one to finish, or cancel one, and try again.", "too_many_uploads": "The account has reached its limit of four concurrent large-file uploads. Wait for a task to complete or cancel an existing upload before retrying.",
"op_lost": "The operation's progress can no longer be read. Refresh the list to see whether the file landed.", "op_lost": "Operation progress is unavailable. Refresh the file list to verify the write result.",
"jobs_unavailable": "The background job service isn't available right now.", "jobs_unavailable": "The background task service is currently unavailable.",
"already_terminal": "This build already finished — there is nothing to cancel.", "already_terminal": "The build has finished and cannot be cancelled.",
"build_unavailable": "Image builds aren't available right now.", "build_unavailable": "Image building is currently unavailable.",
"build_logs_unavailable": "Build logs aren't available right now.", "build_logs_unavailable": "Build logs are currently unavailable.",
"scan_document_not_kept": "This build kept no copy of that scan file: it was too large to keep, or the step that writes it failed.", "scan_document_not_kept": "The scan document was not retained for this build. The file may have exceeded the size limit or its generation step may have failed.",
"already_reviewed": "This submission has already been reviewed.", "already_reviewed": "This submission has already been reviewed.",
"context_changed": "The submitter uploaded the build context again after you reviewed it. Download and review the new upload before approving.", "context_changed": "The build context was uploaded again during review. Download and review the latest content before approval.",
"submission_quota_exceeded": "Your submission quota is full: too many pending reviews, or your pending and rejected uploads fill your storage allowance. Withdraw a pending one, or wait: a rejected upload frees its space 7 days after review, and approved ones don't count.", "submission_quota_exceeded": "The submission quota has been reached for pending reviews or uploaded storage. Withdraw a pending submission if required. Rejected uploads release storage seven days after review; approved submissions do not count toward the quota.",
"submission_cooldown": "Too many submission requests — try again shortly.", "submission_cooldown": "Too many submission requests — try again shortly.",
"submissions_unavailable": "Submissions aren't available right now.", "submissions_unavailable": "Submission processing is currently unavailable.",
"uploads_unavailable": "Uploads aren't available right now.", "uploads_unavailable": "Uploads are currently unavailable.",
"upload_busy": "Another part of this upload is still being sent — wait a moment and try again.", "upload_busy": "Another part of this upload is still being sent — wait a moment and try again.",
"upload_offset_mismatch": "The upload fell out of step with the server — try again to pick up where it stopped.", "upload_offset_mismatch": "The upload offset differs from the server record. Retry to resume from the confirmed offset.",
"uploads_store_unavailable": "The uploads store did not answer — try again in a moment; what was already sent is kept.", "uploads_store_unavailable": "Upload storage did not respond. Received content is retained. Retry later.",
"part_too_large": "A piece of the upload was larger than the server accepts.", "part_too_large": "A piece of the upload was larger than the server accepts.",
"backup_unavailable": "Backups aren't available right now.", "backup_unavailable": "Backups are currently unavailable.",
"account_retired": "This account has been retired — sign in with the account it was migrated to.", "account_retired": "This account has been retired — sign in with the account it was migrated to.",
"no_migration": "There is no migration in progress.", "no_migration": "There is no migration in progress.",
"not_confirmed": "Confirm it's you in this browser first. A confirmation lasts 10 minutes.", "not_confirmed": "Verify account identity in the current browser before proceeding. Verification remains valid for ten minutes.",
"migrate_quota_exceeded": "The servers this migration brings over don't fit your quota. Ask an admin to raise it, then redeem the same code again before it expires.", "migrate_quota_exceeded": "The migration exceeds the current account’s server quota. Request a quota adjustment from an administrator and redeem the same code again before it expires.",
"invalid_quota": "A quota must be a whole number from 0 to 2147483647. Leave it empty for unlimited.", "invalid_quota": "Quota must be an integer from 0 to 2147483647. Leave the field blank for no limit.",
"already_confirmed": "This migration has already been confirmed.", "already_confirmed": "This migration has already been confirmed.",
"invalid_target": "The migration target must be a different account.", "invalid_target": "The migration target must be a different account.",
"target_not_found": "No account matches that migration target.", "target_not_found": "No account matches that migration target.",
"target_unavailable": "That migration target isn't available.", "target_unavailable": "The migration target account is unavailable.",
"no_passkey": "This account has no Passkey bound yet.", "no_passkey": "No Passkey is registered to this account.",
"passkey_required": "Bind a Passkey before doing that.", "passkey_required": "Register a Passkey before proceeding.",
"no_step_up_factor": "This account has no verified email or Passkey to confirm with yet.", "no_step_up_factor": "The account has no verified email address or Passkey available for identity verification.",
"passkey_login_failed": "Passkey verification failed — try again.", "passkey_login_failed": "Passkey verification failed — try again.",
"passkey_login_invalid": "That Passkey sign-in request is invalid or expired — start it again.", "passkey_login_invalid": "That Passkey sign-in request is invalid or expired — start it again.",
"too_many_challenges": "Too many verification attempts right now — try again shortly.", "too_many_challenges": "Verification requests are rate-limited. Retry later.",
"reauth_required": "Confirm it's you first: this change needs your Passkey or an email code from the last few minutes.", "reauth_required": "This operation requires recent identity verification. Verify identity again using a Passkey or email code.",
"staff_reauth": "Operator accounts confirm with a Passkey or by signing in again.", "staff_reauth": "Operator accounts confirm with a Passkey or by signing in again.",
"no_session": "This only works in a browser signed in to Felis.", "no_session": "This operation requires an active Felis browser session.",
"op_login_invalid": "This operator sign-in couldn't be completed — restart the sign-in.", "op_login_invalid": "Operator sign-in could not be completed. Restart the sign-in process.",
"op_login_not_found": "No pending operator sign-in with that id.", "op_login_not_found": "No pending operator sign-in request matches this ID.",
"op_login_mismatch": "That operator sign-in is for a different account.", "op_login_mismatch": "That operator sign-in is for a different account.",
"too_many_streams": "Too many live streams are open — close some pages and try again.", "too_many_streams": "Too many live streams are open — close some pages and try again.",
"protected_admin": "That name belongs to a linked administrator and can't be reclaimed.", "protected_admin": "This name belongs to a linked administrator account and cannot be claimed.",
"auth_unavailable": "The sign-in service isn't available right now.", "auth_unavailable": "The sign-in service is currently unavailable.",
"setup_token_invalid": "That setup token is invalid or expired.", "setup_token_invalid": "The setup token is invalid or expired.",
"network_error": "Can't reach Felis: the network is down, or your Cloudflare Access sign-in expired. Reload the page to sign in again.", "network_error": "Unable to connect to Felis. The network may be unavailable or the Cloudflare Access session may have expired. Check the network. If the Access session has expired, reload the page and sign in again.",
"upstream_unavailable": "The service is unavailable right now. Try again shortly.", "upstream_unavailable": "The service is currently unavailable. Retry later.",
"internal_error": "The operation failed because of an internal server error. Ask an admin to check the logs.", "internal_error": "The operation failed because of an internal server error. Ask an admin to check the logs.",
"request_id": "Request ID: {{id}}", "request_id": "Request ID: {{id}}",
"bad_path_param": "The name in this link is not valid. Open it again from the list.", "bad_path_param": "The name in the link is invalid. Reopen the target page from the list.",
"payload_too_large": "That is larger than the entry proxy accepts, so it was not sent.", "payload_too_large": "The request content exceeds the entry point size limit and could not be sent.",
"passkey_no_credential": "The browser returned no passkey. Try again.", "passkey_no_credential": "The browser returned no Passkey credential. Retry.",
"not_found": "That no longer exists. It may have been deleted; refresh and try again.", "not_found": "The requested content does not exist or has been deleted. Refresh and retry.",
"conflict": "Something changed in the meantime. Refresh and try again.", "conflict": "The state changed during the operation. Refresh and retry.",
"bad_request": "The request was not accepted: {{detail}}", "bad_request": "The request was not accepted: {{detail}}",
"restore_in_progress": "Another backup is still being restored to this server's world. Try again once it finishes.", "restore_in_progress": "Another backup is being restored to the world. Retry after the current restoration completes.",
"uploads_full": "The upload store is full. An admin has to delete reviewed submissions before new uploads fit.", "uploads_full": "The upload store is full. An administrator must delete reviewed submissions before further uploads can be accepted.",
"unsupported_media_type": "The request was sent in a format the server does not accept. Reload the page and try again.", "unsupported_media_type": "The request was sent in a format the server does not accept. Reload the page and try again.",
"schedules_unavailable": "Scheduled tasks aren't available right now.", "schedules_unavailable": "Scheduled tasks are currently unavailable.",
"schedule_limit": "This server already has as many scheduled tasks as it can hold. Delete one first.", "schedule_limit": "The server’s scheduled-task limit has been reached. Remove an existing task before retrying.",
"schedule_running": "This task is running right now. Try again once the run finishes.", "schedule_running": "The task is running. Retry after the current execution completes.",
"schedule_stale": "The server has a new owner since this task was saved. Save the task again before running it.", "schedule_stale": "The server owner changed after the task was saved. Save the task again before execution.",
"bad_schedule": "The task was not saved: {{detail}}", "bad_schedule": "The task was not saved: {{detail}}",
"auth_source_unknown": "Select a configured authentication source.", "auth_source_unknown": "Select a configured authentication source.",
"auth_source_unavailable": "The authentication source is unavailable or returned an invalid role. Try again later.", "auth_source_unavailable": "The authentication source is unavailable or returned an invalid role. Try again later.",
"auth_source_lookup_unsupported": "Role lookup is not configured for this authentication source.", "auth_source_lookup_unsupported": "Role lookup is not configured for this authentication source.",
"minecraft_profile_not_found": "No role matched in this source. Check the role name or UUID.", "minecraft_profile_not_found": "The profile was not found in the selected authentication source. Check the profile name or UUID.",
"auth_sources_unavailable": "Authentication source management is unavailable.", "auth_sources_unavailable": "Authentication source management is unavailable.",
"auth_sources_changed": "Authentication sources changed. Discard your edits and reload before saving.", "auth_sources_changed": "Authentication sources changed. Discard your edits and reload before saving.",
"auth_source_tag_locked": "Saved source IDs cannot be renamed or removed. Disable the source instead." "auth_source_tag_locked": "Saved source IDs cannot be renamed or removed. Disable the source instead."
+56 -56
View File
@@ -10,32 +10,32 @@
"col_size": "Size", "col_size": "Size",
"col_modified": "Modified", "col_modified": "Modified",
"empty_dir_title": "This folder is empty", "empty_dir_title": "This folder is empty",
"empty_dir_hint": "Drop files here to upload them, or make a file or folder with the buttons above.", "empty_dir_hint": "Upload files by dragging them into the list, or create files and folders from the toolbar.",
"list_truncated": "The listing was truncated at the server's safety limit.", "list_truncated": "The listing was truncated at the server's safety limit.",
"stopped_required_title": "Stop the server first", "stopped_required_title": "Server must be stopped",
"stopped_required_body": "Files can only be browsed or edited while the server is fully stopped — the world volume is single-attached to the running server. Stop it to continue.", "stopped_required_body": "A running server exclusively mounts its world volume. Stop the server completely before browsing or editing files.",
"not_yours_title": "Not your server", "not_yours_title": "Server file access denied",
"not_yours_body": "Only the owner or an admin can edit this server's files.", "not_yours_body": "Only the owner or an admin can edit this server's files.",
"binary_hint": "Editing this file as text would damage it. Download it to open with the appropriate application, then upload your changes.", "binary_hint": "Binary files do not support text editing. Download the file, process it with compatible software, and upload it again. Text editing may corrupt the file.",
"saved": "Saved {{path}}", "saved": "Saved {{path}}",
"saving": "Saving…", "saving": "Saving…",
"save": "Save", "save": "Save",
"too_large": "Exceeds the {{limit}} editor limit.", "too_large": "Exceeds the {{limit}} editor limit.",
"conflict_title": "Someone else changed this file", "conflict_title": "File content changed",
"conflict_body": "The file changed after you opened it (another manager saved it, or the server rewrote it on its last run). Your edits are still in the editor and have not been written.", "conflict_body": "The file was modified by another administrator or server process after it was opened. The current edits have not been written. Load the latest content or overwrite the existing file.",
"conflict_reload": "Load latest", "conflict_reload": "Load latest",
"conflict_reload_hint": "Discard your edits and show the file as it is now", "conflict_reload_hint": "Discard current edits and load the latest file content",
"conflict_overwrite": "Overwrite anyway", "conflict_overwrite": "Overwrite anyway",
"conflict_overwrite_hint": "Replace their change with your version", "conflict_overwrite_hint": "Overwrite the existing file with the current edits",
"discard_prompt": "You have unsaved changes. Discard them?", "discard_prompt": "Discard unsaved changes?",
"discard": "Discard", "discard": "Discard",
"keep_editing": "Keep editing", "keep_editing": "Keep editing",
"status_refresh_failed": "Could not refresh the server's status, so what you see may be out of date: {{reason}}", "status_refresh_failed": "Server status refresh failed. The displayed state may be outdated: {{reason}}",
"col_actions": "Actions", "col_actions": "Actions",
"new_file": "New file", "new_file": "New file",
"new_folder": "New folder", "new_folder": "New folder",
"upload": "Upload", "upload": "Upload",
"upload_hint": "Upload files into this folder, or drop them on the list. Zip a folder before uploading it", "upload_hint": "Upload files to the current directory or drag them into the list. Compress folders as .zip archives before uploading.",
"in_folder": "In {{dir}}", "in_folder": "In {{dir}}",
"name_label": "Name", "name_label": "Name",
"create": "Create", "create": "Create",
@@ -43,32 +43,32 @@
"rename_title": "Rename {{name}}", "rename_title": "Rename {{name}}",
"rename_item": "Rename {{name}}", "rename_item": "Rename {{name}}",
"delete_item": "Delete {{name}}", "delete_item": "Delete {{name}}",
"managed_no_rename": "Felis manages this one, so it keeps its name and place.", "managed_no_rename": "This entry is managed by Felis. Its name and location must remain unchanged.",
"wait_for_uploads": "Wait for the uploads to finish first.", "wait_for_uploads": "This operation is unavailable until uploads complete.",
"name_required": "Enter a name.", "name_required": "Enter a name.",
"name_slash": "A name cannot contain “/”.", "name_slash": "Names must not contain “/”.",
"name_dots": "“.” and “..” cannot be used as names.", "name_dots": "“.” and “..” cannot be used as names.",
"name_too_long": "That name is too long.", "name_too_long": "The name exceeds the length limit.",
"name_taken": "Something here already has that name.", "name_taken": "A file or folder with this name already exists in the current directory.",
"folder_created": "Created folder {{path}}", "folder_created": "Created folder {{path}}",
"renamed": "Renamed {{from}} to {{to}}", "renamed": "Renamed {{from}} to {{to}}",
"deleted": "Deleted {{path}}", "deleted": "Deleted {{path}}",
"delete": "Delete", "delete": "Delete",
"delete_file_title": "Delete {{name}}?", "delete_file_title": "Delete {{name}}?",
"delete_folder_title": "Delete the folder {{name}}?", "delete_folder_title": "Delete the folder {{name}}?",
"delete_file_body": "The file is deleted for good. Take a backup first if you may want it back.", "delete_file_body": "The file will be permanently deleted. Back up any required content before deletion.",
"delete_folder_body": "The folder and everything in it are deleted for good. Take a backup first if you may want any of it back.", "delete_folder_body": "The folder and all its contents will be permanently deleted. Back up any required content before deletion.",
"secret_config_unreadable": "config/paper-global.yml holds the proxy forwarding secret every server shares, so the editor does not open it.", "secret_config_unreadable": "config/paper-global.yml contains the shared proxy forwarding secret and cannot be read or edited online.",
"drop_here": "Drop to upload into {{dir}}", "drop_here": "Drop to upload into {{dir}}",
"upload_no_folders_one": "Folders cannot be uploaded, so one was skipped: loose files cut off halfway leave a broken save behind. Zip it, upload the .zip, then choose Extract here on it.", "upload_no_folders_one": "Direct folder upload is unsupported. Folders skipped: 1. To prevent incomplete saves after interrupted uploads, compress folders as .zip archives, upload them, and select Extract here.",
"upload_no_folders_other": "Folders cannot be uploaded, so {{count}} were skipped: loose files cut off halfway leave a broken save behind. Zip them, upload the .zip, then choose Extract here on it.", "upload_no_folders_other": "Direct folder upload is unsupported. Folders skipped: {{count}}. To prevent incomplete saves after interrupted uploads, compress folders as .zip archives, upload them, and select Extract here.",
"uploads_label": "Uploads", "uploads_label": "Uploads",
"uploads_title": "Uploads · {{done}} of {{total}} done", "uploads_title": "Uploads · {{done}} of {{total}} done",
"upload_queued": "Waiting", "upload_queued": "Waiting",
"upload_sending": "{{sent}} of {{total}} · {{percent}}%", "upload_sending": "{{sent}} of {{total}} · {{percent}}%",
"upload_landing": "Writing it to the server…", "upload_landing": "Writing it to the server…",
"upload_done": "Uploaded", "upload_done": "Uploaded",
"upload_exists": "A file with this name is already here.", "upload_exists": "A file with this name already exists in the current directory.",
"upload_replace": "Replace", "upload_replace": "Replace",
"upload_skip": "Skip", "upload_skip": "Skip",
"upload_replace_all": "Replace all", "upload_replace_all": "Replace all",
@@ -78,33 +78,33 @@
"upload_dismiss": "Dismiss {{name}}", "upload_dismiss": "Dismiss {{name}}",
"upload_clear_done": "Clear finished", "upload_clear_done": "Clear finished",
"upload_progress_label": "Uploading {{name}}", "upload_progress_label": "Uploading {{name}}",
"upload_folder_there": "A folder with this name is already here.", "upload_folder_there": "A folder with this name already exists in the current directory.",
"upload_name_too_long": "This file's name is longer than the 255 bytes a file name on the server can hold. Rename it shorter, then upload it.", "upload_name_too_long": "The filename exceeds 255 bytes. Shorten the filename and upload the file again.",
"upload_no_room": "The world volume has {{free}} free, not enough for this {{size}} file. Delete files you do not need, then retry.", "upload_no_room": "The world volume has {{free}} available, which is insufficient for a {{size}} file. Free storage space and retry.",
"upload_landing_progress": "Writing it to the server… {{percent}}%", "upload_landing_progress": "Writing it to the server… {{percent}}%",
"wait_for_op": "Wait for the background operation to finish first.", "wait_for_op": "Files cannot be modified until the background operation completes.",
"wait_for_download": "Wait until the download is ready first.", "wait_for_download": "This operation is unavailable until download preparation completes.",
"wait_for_backup": "A backup of this world is running. Change files once it finishes.", "wait_for_backup": "World backup is in progress. File changes are available after completion.",
"wait_for_restore": "This world is being restored. Change files once the restore finishes.", "wait_for_restore": "World restoration is in progress. File changes are available after completion.",
"wait_for_world_export": "This world is being exported. Change files once that download ends.", "wait_for_world_export": "World export is in progress. File changes are available after the download ends.",
"wait_for_file_download": "A download is reading this world. Change files once it has finished.", "wait_for_file_download": "A file download is reading world data. File changes are available after the transfer completes.",
"unzip_item": "Extract {{name}} here", "unzip_item": "Extract {{name}} to the current directory",
"unzip_conflicts_title_one": "Extracting {{name}} replaces {{count}} file", "unzip_conflicts_title_one": "Extracting {{name}} replaces {{count}} file",
"unzip_conflicts_title_other": "Extracting {{name}} replaces {{count}} files", "unzip_conflicts_title_other": "Extracting {{name}} replaces {{count}} files",
"unzip_conflicts_body": "These files in the archive are already here. Confirm to replace them with the archive's versions; everything else extracts as usual. Take a backup first if you may want the old versions back.", "unzip_conflicts_body": "Some archive entries conflict with files in the current directory. Confirmation overwrites conflicting files and extracts the remaining entries. Back up any required original files before proceeding.",
"unzip_conflicts_more": "{{count}} more not listed.", "unzip_conflicts_more": "Additional entries not listed: {{count}}.",
"unzip_overwrite": "Replace and extract", "unzip_overwrite": "Replace and extract",
"download_item": "Download {{name}}", "download_item": "Download {{name}}",
"download_folder_item": "Download the folder {{name}} as a .zip", "download_folder_item": "Download folder {{name}} as a .zip archive",
"download_preparing": "Preparing the download of {{name}}…", "download_preparing": "Preparing the download of {{name}}…",
"download_started": "Downloading {{filename}}.", "download_started": "Downloading {{filename}}.",
"download_started_props": "Downloading {{filename}}. Its rcon.password is redacted.", "download_started_props": "Download started for {{filename}}. The rcon.password value is redacted in the downloaded content.",
"download_started_config": "Downloading {{filename}}. paper-global.yml, the proxy forwarding secret every server shares, is left out.", "download_started_config": "Download started for {{filename}}. paper-global.yml, which contains the shared proxy forwarding secret, is excluded.",
"download_failed": "The download could not be prepared.", "download_failed": "Download preparation failed.",
"download_failed_because": "The download could not be prepared: {{reason}}", "download_failed_because": "Download preparation failed: {{reason}}",
"download_out_of_memory": "This folder holds more files than the zipping task has memory to list, so the system stopped it and nothing was downloaded. Download its folders one by one instead.", "download_out_of_memory": "The packaging task terminated because its file list exceeded available memory. The download did not start. Split the content into smaller folders and download them separately.",
"download_busy": "Too many file downloads are being prepared: two at a time per person, four at a time across the platform, and thirty an hour per person. Try again in a few minutes.", "download_busy": "File download limits have been reached: two concurrent requests per account, four across the platform, and thirty per account per hour. Retry after existing tasks complete or rate limits reset.",
"secret_config_no_download": "config/paper-global.yml holds the proxy forwarding secret every server shares, so it cannot be downloaded.", "secret_config_no_download": "config/paper-global.yml contains the shared proxy forwarding secret and cannot be downloaded.",
"ops_label": "Background operations", "ops_label": "Background operations",
"ops_refresh_failed": "Could not read the background operations: {{reason}}", "ops_refresh_failed": "Could not read the background operations: {{reason}}",
"op_unzip": "Extract {{path}}", "op_unzip": "Extract {{path}}",
@@ -117,19 +117,19 @@
"op_upload_done": "Written.", "op_upload_done": "Written.",
"op_dismiss": "Dismiss the result for {{path}}", "op_dismiss": "Dismiss the result for {{path}}",
"op_conflicts": "Review conflicts", "op_conflicts": "Review conflicts",
"op_upload_exists": "A file with this name is already here, so nothing was replaced. Upload it again and choose Replace.", "op_upload_exists": "A file with this name already exists. The upload did not replace it. Select Replace when uploading again.",
"op_unzip_conflicts_one": "It would replace {{count}} file already here, so nothing was extracted yet.", "op_unzip_conflicts_one": "Extraction would overwrite {{count}} existing file. The operation has not been performed.",
"op_unzip_conflicts_other": "It would replace {{count}} files already here, so nothing was extracted yet.", "op_unzip_conflicts_other": "Extraction would overwrite {{count}} existing files. The operation has not been performed.",
"op_volume_full": "Not enough room on the world volume: {{need}} needed, {{avail}} free. Nothing was changed.", "op_volume_full": "Insufficient world volume space: {{need}} required, {{avail}} available. This operation did not modify files.",
"archive_invalid": "The archive is damaged, or not a zip file. Nothing was changed.", "archive_invalid": "The archive is damaged or is not in ZIP format. This operation did not modify files.",
"archive_invalid_entry": "{{entry}} in the archive is damaged (its size or checksum does not match). Nothing was changed.", "archive_invalid_entry": "The size or checksum of archive entry {{entry}} does not match. This operation did not modify files.",
"archive_unsafe": "{{entry}} in the archive would land outside this folder, or is a device file. Nothing in the archive was extracted.", "archive_unsafe": "Archive entry {{entry}} contains an out-of-directory path or a device file. The entire archive was rejected without extraction.",
"archive_symlink": "{{entry}} in the archive is a symbolic link. Nothing in the archive was extracted.", "archive_symlink": "Archive entry {{entry}} is a symbolic link. The entire archive was rejected without extraction.",
"type_conflict": "{{entry}} is a file on one side and a folder on the other, which replacing cannot resolve. Rename or delete {{entry}} here, then extract again.", "type_conflict": "Entry {{entry}} conflicts with an existing entry of the same name because one is a file and the other is a folder. Rename or delete the existing {{entry}} before extraction.",
"job_failed": "The background task ended without saying why. Refresh the list to check, then try again.", "job_failed": "The background task ended without a failure reason. Refresh the file list to verify the result before retrying.",
"job_timed_out": "The background task did not finish within 2 hours and was stopped. Refresh the list to check, then try again.", "job_timed_out": "The background task exceeded its two-hour execution limit and was terminated. Refresh the file list to verify the result before retrying.",
"job_out_of_memory": "The background task ran out of memory and the system stopped it. The files on the server were not changed. Try again.", "job_out_of_memory": "The background task terminated due to insufficient memory. Server files were not modified. Check task resources before retrying.",
"job_out_of_memory_unzip": "The archive holds more files than the extraction task has memory to list, so the system stopped it. The files on the server were not changed. Split it into several smaller zips and extract each one.", "job_out_of_memory_unzip": "The extraction task terminated because its file list exceeded available memory. Server files were not modified. Split the content into smaller ZIP archives and upload and extract them separately.",
"binary_title": "This is a binary file", "binary_title": "This is a binary file",
"preview_large_title": "File exceeds the preview limit", "preview_large_title": "File exceeds the preview limit",
"preview_large_hint": "The editor reads files up to {{limit}}. Download it to edit, then upload your changes.", "preview_large_hint": "The editor reads files up to {{limit}}. Download it to edit, then upload your changes.",
+6 -6
View File
@@ -4,8 +4,8 @@
"space_label": "Choose a space", "space_label": "Choose a space",
"login": "Login space", "login": "Login space",
"lobby": "Lobby", "lobby": "Lobby",
"login_description": "Players authenticate here before entering the lobby. This Limbo scene uses a schematic, rather than a Paper world map.", "login_description": "Players enter the main lobby after authentication in the login space. Login scenes use Limbo schematics and do not directly support Paper world maps.",
"lobby_description": "Players choose their server here. Replace maps, install plugins, customize the experience and grant builders access.", "lobby_description": "The main lobby provides server selection and supports map, plugin, experience, and building-permission configuration.",
"connection_version": "Installed game version: Java {{version}}", "connection_version": "Installed game version: Java {{version}}",
"unknown_version": "not recorded", "unknown_version": "not recorded",
"version_hint": "Client compatibility depends on proxy plugins. Version changes need matching login, lobby and proxy support; this is the installation version, while the running version comes from each image.", "version_hint": "Client compatibility depends on proxy plugins. Version changes need matching login, lobby and proxy support; this is the installation version, while the running version comes from each image.",
@@ -18,7 +18,7 @@
"restarting": "Restarting…", "restarting": "Restarting…",
"restart_requested": "Restart requested. Follow the startup status above.", "restart_requested": "Restart requested. Follow the startup status above.",
"restart_title": "Restart this space?", "restart_title": "Restart this space?",
"restart_hint": "Players here will be disconnected briefly. Saved settings take effect when this space starts again.", "restart_hint": "Restarting disconnects players in this space. Saved settings take effect after startup.",
"restart_required": "Settings saved; restart to apply", "restart_required": "Settings saved; restart to apply",
"save": "Save settings", "save": "Save settings",
"saving": "Saving…", "saving": "Saving…",
@@ -57,7 +57,7 @@
"bookHelp": "Login instructions", "bookHelp": "Login instructions",
"openBook": "Open the login book automatically", "openBook": "Open the login book automatically",
"loginTimeoutSeconds": "Login timeout (30–3600 seconds)", "loginTimeoutSeconds": "Login timeout (30–3600 seconds)",
"login_settings_hint": "The system still generates the code and login URL and sends the chat guidance. An explicit login timeout environment variable takes precedence over this setting.", "login_settings_hint": "Binding codes and sign-in links are generated by the system. Chat prompts remain enabled. An explicitly configured login-timeout environment variable overrides this setting.",
"lobby_settings_hint": "The welcome message includes a clickable server menu. Spawn comes from the map; use setworldspawn x y z in the lobby console to change it. Protection switches apply across the lobby. Builders keep their own game mode.", "lobby_settings_hint": "The welcome message includes a clickable server menu. Spawn comes from the map; use setworldspawn x y z in the lobby console to change it. Protection switches apply across the lobby. Builders keep their own game mode.",
"ADVENTURE": "Adventure", "ADVENTURE": "Adventure",
"SURVIVAL": "Survival", "SURVIVAL": "Survival",
@@ -80,13 +80,13 @@
"backups_title": "Map backups & restore", "backups_title": "Map backups & restore",
"backups_hint": "Back up before replacing maps, plugins or images; choose a saved backup to restore.", "backups_hint": "Back up before replacing maps, plugins or images; choose a saved backup to restore.",
"builder_title": "Lobby builder access", "builder_title": "Lobby builder access",
"builder_hint": "Grant felis.lobby.build to let a player build without opening building to everyone.", "builder_hint": "Granting felis.lobby.build allows the specified player to build in the lobby without enabling building for all players.",
"builder_player": "Minecraft player name", "builder_player": "Minecraft player name",
"builder_grant": "Allow building", "builder_grant": "Allow building",
"builder_revoke": "Deny building", "builder_revoke": "Deny building",
"builder_running": "Start the lobby before changing builder access.", "builder_running": "Start the lobby before changing builder access.",
"discard_title": "Discard unsaved settings?", "discard_title": "Discard unsaved settings?",
"discard_hint": "Switching spaces discards your edits. Cancel to save them first.", "discard_hint": "Switching spaces discards unsaved changes. Cancel the switch and save the configuration to retain them.",
"discard": "Discard & switch", "discard": "Discard & switch",
"appearance_description": "Set the welcome message and server menu players see.", "appearance_description": "Set the welcome message and server menu players see.",
"players_description": "Choose join behavior, game mode and player protection.", "players_description": "Choose join behavior, game mode and player protection.",
+3 -3
View File
@@ -16,7 +16,7 @@
"fleet_sort": "Sort order", "fleet_sort": "Sort order",
"fleet_sort_name": "Sort by name", "fleet_sort_name": "Sort by name",
"fleet_sort_status": "Sort by status", "fleet_sort_status": "Sort by status",
"fleet_sort_players": "Most players first", "fleet_sort_players": "Online players, descending",
"fleet_count": "{{count}} servers", "fleet_count": "{{count}} servers",
"fleet_count_filtered": "{{shown}} / {{total}} servers", "fleet_count_filtered": "{{shown}} / {{total}} servers",
"fleet_col_status": "Status", "fleet_col_status": "Status",
@@ -29,13 +29,13 @@
"fleet_unclaimed": "Unclaimed", "fleet_unclaimed": "Unclaimed",
"fleet_owner_you": "You", "fleet_owner_you": "You",
"fleet_owner_unknown": "Unknown", "fleet_owner_unknown": "Unknown",
"fleet_owner_unknown_hint": "Couldn't look up the owner just now; it shows again on the next refresh.", "fleet_owner_unknown_hint": "Server ownership could not be read. It will be queried again on the next refresh.",
"fleet_endpoint_idle": "Not running", "fleet_endpoint_idle": "Not running",
"policy_owneronly": "Owner only", "policy_owneronly": "Owner only",
"policy_public": "Public", "policy_public": "Public",
"policy_allowlist": "Wake list", "policy_allowlist": "Wake list",
"fleet_empty_title": "No servers", "fleet_empty_title": "No servers",
"fleet_empty_hint": "No MinecraftServer exists in the cluster yet.", "fleet_empty_hint": "The cluster contains no MinecraftServer resources.",
"fleet_no_match_title": "No matches", "fleet_no_match_title": "No matches",
"fleet_no_match_hint": "No server matches the current search or filter." "fleet_no_match_hint": "No server matches the current search or filter."
} }
+25 -25
View File
@@ -1,28 +1,28 @@
{ {
"title": "Scheduled tasks", "title": "Scheduled tasks",
"subtitle": "Restart, stop, start or back up this server, or send its console a command, at set times.", "subtitle": "Schedule server startup, shutdown, restart, backup, or console commands.",
"back_to_console": "Back to console", "back_to_console": "Back to console",
"not_yours_title": "No permission to manage scheduled tasks", "not_yours_title": "No permission to manage scheduled tasks",
"not_yours_body": "Only the owner or an admin can view and change this server's scheduled tasks.", "not_yours_body": "Only the owner or an admin can view and change this server's scheduled tasks.",
"new_task": "New task", "new_task": "New task",
"count": "{{count}} of {{limit}} tasks", "count": "{{count}} of {{limit}} tasks",
"limit_reached": "A server holds at most {{limit}} tasks. Delete one to add another.", "limit_reached": "The scheduled-task limit of {{limit}} has been reached. Remove an existing task before adding another.",
"unavailable_title": "Scheduled tasks aren't available", "unavailable_title": "Scheduled tasks aren't available",
"unavailable_body": "This felis-api runs without a schedule store, so tasks can't be listed or saved here. Ask an administrator to check the felis-api setup.", "unavailable_body": "felis-api has no schedule storage configured. Tasks cannot be read or saved. Contact an administrator to check the configuration.",
"empty_title": "No scheduled tasks yet", "empty_title": "No scheduled tasks",
"empty_hint": "Start from a common one, or build your own with New task.", "empty_hint": "Select a task template or select New task to configure a custom task.",
"template_restart": "Restart every night at 04:00", "template_restart": "Restart every night at 04:00",
"template_backup": "Back up every night at 05:00", "template_backup": "Back up every night at 05:00",
"template_announce": "Announce in chat every 30 minutes", "template_announce": "Announce in chat every 30 minutes",
"template_announce_command": "say Welcome to the server!", "template_announce_command": "say Welcome to the server!",
"action_command": "Console command", "action_command": "Console command",
"action_command_desc": "Send one command to the server console, as if typed there. Runs only while the server is running.", "action_command_desc": "Send a command to the server console with the same behavior as direct console input. Execution requires a running server.",
"action_restart": "Restart", "action_restart": "Restart",
"action_restart_desc": "Stop the server and start it again. Online players are disconnected. Skipped while it is stopped.", "action_restart_desc": "Stop the server and start it again. Online players are disconnected. Skipped while it is stopped.",
"action_stop": "Stop", "action_stop": "Stop",
"action_stop_desc": "Stop the server. Online players are disconnected.", "action_stop_desc": "Stop the server. Online players are disconnected.",
"action_start": "Start", "action_start": "Start",
"action_start_desc": "Start the server when it is stopped.", "action_start_desc": "Start the server only when it is stopped.",
"action_backup": "Backup", "action_backup": "Backup",
"action_backup_desc": "Back up the world. A running server is stopped first and started again afterwards.", "action_backup_desc": "Back up the world. A running server is stopped first and started again afterwards.",
"days_all": "Every day", "days_all": "Every day",
@@ -63,30 +63,30 @@
"opt_hours_other": "{{count}} hours", "opt_hours_other": "{{count}} hours",
"create_title": "New scheduled task", "create_title": "New scheduled task",
"edit_title": "Edit scheduled task", "edit_title": "Edit scheduled task",
"dialog_desc": "Choose what runs and when.", "dialog_desc": "Configure the task type, execution time, and runtime parameters.",
"create": "Create", "create": "Create",
"save": "Save", "save": "Save",
"field_action": "What to do", "field_action": "Task type",
"field_command": "Command", "field_command": "Command",
"command_placeholder": "say The server restarts at 04:00", "command_placeholder": "say The server restarts at 04:00",
"command_hint": "One console command. The leading / is optional.", "command_hint": "Enter one console command. The leading / is optional.",
"field_when": "When", "field_when": "Execution mode",
"mode_daily": "Once a day", "mode_daily": "Once a day",
"mode_interval": "Repeat", "mode_interval": "Repeat",
"field_time": "Time", "field_time": "Time",
"field_every": "Repeat every", "field_every": "Repeat every",
"interval_preview": "Counted from 00:00 each day: {{times}}", "interval_preview": "Counted from 00:00 each day: {{times}}",
"interval_hourly_limit": "A restart, stop, start or backup repeats at most once an hour.", "interval_hourly_limit": "A restart, stop, start or backup repeats at most once an hour.",
"field_days": "Days", "field_days": "Execution days",
"field_timezone": "Time zone", "field_timezone": "Time zone",
"timezone_hint": "Times are read in this zone. Your browser is in {{zone}}.", "timezone_hint": "Execution times use the selected timezone. Browser timezone: {{zone}}.",
"timezone_use_browser": "Use the browser's zone", "timezone_use_browser": "Use the browser's zone",
"field_warn": "Warn players", "field_warn": "Warn players",
"warn_none": "No warning", "warn_none": "No warning",
"warn_option_one": "{{count}} minute before", "warn_option_one": "{{count}} minute before",
"warn_option_other": "{{count}} minutes before", "warn_option_other": "{{count}} minutes before",
"warn_hint": "Posts a message in game chat ahead of the stop, in Chinese and English, while the server is running.", "warn_hint": "Posts a message in game chat ahead of the stop, in Chinese and English, while the server is running.",
"backup_note": "A backup of a running server stops it (online players are disconnected), backs up the world and starts it again, which usually takes a few minutes. A stopped server is backed up and stays stopped.", "backup_note": "Backing up a running server first stops it and disconnects online players, then starts it after the backup completes. A stopped server remains stopped after backup.",
"field_label": "Name (optional)", "field_label": "Name (optional)",
"label_placeholder": "Nightly restart", "label_placeholder": "Nightly restart",
"field_enabled": "Enabled", "field_enabled": "Enabled",
@@ -99,8 +99,8 @@
"err_timezone": "Unknown time zone. Use a name such as Asia/Shanghai.", "err_timezone": "Unknown time zone. Use a name such as Asia/Shanghai.",
"err_label_long": "The name is longer than {{max}} characters.", "err_label_long": "The name is longer than {{max}} characters.",
"next_run": "Next run {{when}}", "next_run": "Next run {{when}}",
"next_run_off": "Off: it runs only when you press Run now", "next_run_off": "Disabled; runs only when Run now is selected",
"never_run": "Hasn't run yet", "never_run": "Not executed",
"last_run": "Last run {{when}}", "last_run": "Last run {{when}}",
"run_started_at": "Started {{when}}", "run_started_at": "Started {{when}}",
"result_ok": "Succeeded", "result_ok": "Succeeded",
@@ -114,20 +114,20 @@
"run_state_starting": "Starting the server…", "run_state_starting": "Starting the server…",
"warn_badge": "Warns {{count}} min before", "warn_badge": "Warns {{count}} min before",
"off_badge": "Off", "off_badge": "Off",
"tz_differs": "Times are in {{zone}}; your browser is in {{browser}}.", "tz_differs": "Task timezone: {{zone}}. Browser timezone: {{browser}}.",
"toggle_label": "Run {{name}} on schedule", "toggle_label": "Run {{name}} on schedule",
"run_now": "Run now", "run_now": "Run now",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"busy_hint": "This task is running. Wait for the run to finish.", "busy_hint": "The task is running. Wait for the current execution to complete.",
"owner_changed": "The server changed owner, so this task switched itself off. Check it, then save it or switch it on to use it again.", "owner_changed": "The server owner has changed and the task was disabled automatically. Verify the configuration before saving or enabling it again.",
"detail_not_running": "The server was not running.", "detail_not_running": "The server was not running.",
"detail_already_stopped": "The server was already stopped.", "detail_already_stopped": "The server was already stopped.",
"detail_already_running": "The server was already running.", "detail_already_running": "The server was already running.",
"detail_gone": "The server no longer exists.", "detail_gone": "The server no longer exists.",
"detail_no_world": "The server has no world yet.", "detail_no_world": "The server has no world save.",
"detail_missed": "felis-api was not running at the scheduled time.", "detail_missed": "felis-api was not running at the scheduled time.",
"detail_started_meanwhile": "Someone started the server before the run finished.", "detail_started_meanwhile": "Another operation started the server before the task completed.",
"detail_interrupted": "felis-api stopped in the middle of this run.", "detail_interrupted": "felis-api stopped in the middle of this run.",
"detail_store_full": "The backup store is full. Ask an administrator to free space.", "detail_store_full": "The backup store is full. Ask an administrator to free space.",
"detail_console": "The server console could not be reached.", "detail_console": "The server console could not be reached.",
@@ -135,15 +135,15 @@
"detail_retiring": "The server is being given up or deleted.", "detail_retiring": "The server is being given up or deleted.",
"run_title": "Run “{{name}}” now?", "run_title": "Run “{{name}}” now?",
"run_confirm": "Run now", "run_confirm": "Run now",
"run_body_command": "Sends this command to the server console now (skipped while the server is stopped):", "run_body_command": "Send the following command to the server console. Execution is skipped if the server is stopped:",
"run_body_restart": "Stops the server now and starts it again.", "run_body_restart": "Stops the server now and starts it again.",
"run_body_stop": "Stops the server now.", "run_body_stop": "Stops the server now.",
"run_body_start": "Starts the server now if it is stopped.", "run_body_start": "Start the server immediately if it is stopped.",
"run_body_backup": "Backs up the world now. A running server is stopped, backed up and started again; a stopped server stays stopped.", "run_body_backup": "Backs up the world now. A running server is stopped, backed up and started again; a stopped server stays stopped.",
"run_kick": "Online players are disconnected right away: a run started by hand skips the players' warning.", "run_kick": "Online players are disconnected right away: a run started by hand skips the players' warning.",
"run_keeps_next": "The next scheduled run stays as planned.", "run_keeps_next": "The next scheduled run stays as planned.",
"delete_title": "Delete “{{name}}”?", "delete_title": "Delete “{{name}}”?",
"delete_body": "The task is removed and won't run again.", "delete_body": "The task will be deleted and will no longer execute. Its configuration will not be retained.",
"delete_confirm": "Delete", "delete_confirm": "Delete",
"created": "Created “{{name}}”.", "created": "Created “{{name}}”.",
"saved": "Saved “{{name}}”.", "saved": "Saved “{{name}}”.",
@@ -154,6 +154,6 @@
"notes_title": "How scheduled tasks run", "notes_title": "How scheduled tasks run",
"note_timing": "felis-api checks every 15 seconds, so a task starts within about 15 seconds of its time. A run more than 10 minutes late (felis-api was down) is recorded as missed, and the next one runs as usual.", "note_timing": "felis-api checks every 15 seconds, so a task starts within about 15 seconds of its time. A run more than 10 minutes late (felis-api was down) is recorded as missed, and the next one runs as usual.",
"note_state": "A command, restart or stop runs only while the server is running, and a start only while it is stopped; otherwise the run is skipped.", "note_state": "A command, restart or stop runs only while the server is running, and a start only while it is stopped; otherwise the run is skipped.",
"note_owner": "When the server changes owner, its tasks switch off. Saving a task hands it to the current owner.", "note_owner": "Tasks are disabled automatically when server ownership changes. Saving a task again assigns it to the current owner.",
"note_run_now": "Run now also works on a task that is off. It skips the players' warning and keeps the next scheduled run." "note_run_now": "Run now also works on a task that is off. It skips the players' warning and keeps the next scheduled run."
} }
+57 -57
View File
@@ -1,12 +1,12 @@
{ {
"my_servers_title": "My servers", "my_servers_title": "My servers",
"my_servers_subtitle": "Wake, stop, or claim the servers you can manage.", "my_servers_subtitle": "Manage servers owned by or accessible to the current account.",
"search_placeholder": "Search server name or address…", "search_placeholder": "Search server name or address…",
"filter_status_all": "All statuses", "filter_status_all": "All statuses",
"search_no_match": "No matching servers found.", "search_no_match": "No matching servers found.",
"search_clear_btn": "Clear filters", "search_clear_btn": "Clear filters",
"no_servers_linked": "No servers linked to you", "no_servers_linked": "No servers linked to you",
"no_servers_hint": "Claim an unowned server you have access to, or ask an admin to provision one.", "no_servers_hint": "Claim an authorized server or request allocation from an administrator.",
"phase_running": "Running", "phase_running": "Running",
"phase_starting": "Starting", "phase_starting": "Starting",
"phase_stopping": "Stopping", "phase_stopping": "Stopping",
@@ -18,7 +18,7 @@
"console": "Console", "console": "Console",
"claim": "Claim", "claim": "Claim",
"claim_server_title": "Claim Server", "claim_server_title": "Claim Server",
"claim_server_desc": "Are you sure you want to claim the server \"{{name}}\"? Once claimed, you will have access to its console, player management, and backups.", "claim_server_desc": "Claiming “{{name}}” assigns ownership to the current account and grants console, whitelist, and backup management permissions.",
"claiming": "Claiming…", "claiming": "Claiming…",
"stop": "Stop", "stop": "Stop",
"stopping": "Stopping…", "stopping": "Stopping…",
@@ -66,7 +66,7 @@
"system_service": "System service", "system_service": "System service",
"system_service_hint": "Staff manage system services in Login & lobby.", "system_service_hint": "Staff manage system services in Login & lobby.",
"players_back_to_console": "Back to console", "players_back_to_console": "Back to console",
"players_not_yours_title": "Not your server", "players_not_yours_title": "Server management access denied",
"players_not_yours_body": "Only the owner or an admin can manage this server's players.", "players_not_yours_body": "Only the owner or an admin can manage this server's players.",
"players_not_running_title": "Server is asleep", "players_not_running_title": "Server is asleep",
"players_not_running_body": "Player management runs over a live connection to the server. Wake it to manage the whitelist, online players, and bans.", "players_not_running_body": "Player management runs over a live connection to the server. Wake it to manage the whitelist, online players, and bans.",
@@ -83,19 +83,19 @@
"access_whitelist_title": "Whitelist", "access_whitelist_title": "Whitelist",
"access_whitelist_desc": "When the whitelist is on, only listed players can join.", "access_whitelist_desc": "When the whitelist is on, only listed players can join.",
"access_whitelist_add": "Add", "access_whitelist_add": "Add",
"access_whitelist_empty": "No players on the whitelist yet.", "access_whitelist_empty": "The whitelist contains no players.",
"access_whitelist_empty_hint": "Add a player above to let them join.", "access_whitelist_empty_hint": "Players added to the whitelist are permitted to join the server.",
"access_whitelist_remove": "Remove {{player}} from the whitelist", "access_whitelist_remove": "Remove {{player}} from the whitelist",
"access_remove": "Remove", "access_remove": "Remove",
"access_whitelist_load_error": "Couldn't load the whitelist.", "access_whitelist_load_error": "Unable to load the whitelist.",
"access_whitelist_added": "Added {{player}} to the whitelist.", "access_whitelist_added": "Added {{player}} to the whitelist.",
"access_whitelist_removed": "Removed {{player}} from the whitelist.", "access_whitelist_removed": "Removed {{player}} from the whitelist.",
"access_whitelist_raw": "View raw server reply", "access_whitelist_raw": "View raw server reply",
"access_online_title": "Online players", "access_online_title": "Online players",
"access_online_desc": "Players on the server right now.", "access_online_desc": "Players currently connected to the server.",
"access_online_load_error": "Couldn't load online players.", "access_online_load_error": "Unable to load online players.",
"access_online_empty": "No one is online right now.", "access_online_empty": "No players online.",
"access_online_names_unavailable": "{{count}} online, but the names couldn't be read.", "access_online_names_unavailable": "{{count}} online, but the names could not be read.",
"access_online_names_unavailable_hint": "See the raw server reply below for names.", "access_online_names_unavailable_hint": "See the raw server reply below for names.",
"access_online_raw": "View raw server reply", "access_online_raw": "View raw server reply",
"access_updated_at": "Updated {{time}}", "access_updated_at": "Updated {{time}}",
@@ -108,19 +108,19 @@
"access_ban_confirm": "Ban {{player}}? They'll be kicked and blocked from rejoining.", "access_ban_confirm": "Ban {{player}}? They'll be kicked and blocked from rejoining.",
"access_ban_confirm_yes": "Ban", "access_ban_confirm_yes": "Ban",
"access_ban_empty": "No banned players.", "access_ban_empty": "No banned players.",
"access_ban_empty_hint": "Banned players show up here, ready to pardon in one tap.", "access_ban_empty_hint": "The ban list displays banned players and provides pardon actions.",
"access_ban_load_error": "Couldn't load the ban list.", "access_ban_load_error": "Unable to load the ban list.",
"access_ban_raw": "View raw server reply", "access_ban_raw": "View raw server reply",
"access_cancel": "Cancel", "access_cancel": "Cancel",
"access_banned": "Banned {{player}}.", "access_banned": "Banned {{player}}.",
"access_pardoned": "Pardoned {{player}}.", "access_pardoned": "Pardoned {{player}}.",
"wake_list_title": "Wake list", "wake_list_title": "Wake list",
"wake_list_desc": "With the autostart policy set to Wake list, a sleeping server can be started only by you, admins, and the players here who may wake it. A player is added on their first join. Once you take someone's wake right away, rejoining does not give it back; you can allow them again here. The list is emptied when the server changes owner.", "wake_list_desc": "With the Wake list autostart policy, only the server owner, administrators, and players with wake permission can start a sleeping server. Players are added on first join. Rejoining does not restore revoked permission; it must be granted manually. The list is cleared when server ownership changes.",
"wake_list_inactive_owner": "The autostart policy is Owner only, so only you and admins can start it while it sleeps and this list has no effect. Switch it to Wake list under Edit server configuration on the console to use it.", "wake_list_inactive_owner": "The current Owner only policy permits only the server owner and administrators to start a sleeping server. The wake list is inactive. Change the policy to Wake list in Edit Server Config to enable it.",
"wake_list_inactive_public": "The autostart policy is Public, so any player's join starts it and this list has no effect. Switch it to Wake list under Edit server configuration on the console so only the players here can start it.", "wake_list_inactive_public": "The current Public policy allows any player’s join to trigger startup. The wake list is inactive. The Wake list policy restricts startup to the server owner, administrators, and players with wake permission.",
"wake_list_load_error": "Couldn't load the wake list.", "wake_list_load_error": "Unable to load the wake list.",
"wake_list_empty": "No player has joined this server yet.", "wake_list_empty": "No player join records available.",
"wake_list_empty_hint": "Players show up here after their first join.", "wake_list_empty_hint": "Players are added to the list after their first join.",
"wake_list_unlinked": "Player without a linked account", "wake_list_unlinked": "Player without a linked account",
"wake_list_joined": "first joined {{when}}", "wake_list_joined": "first joined {{when}}",
"wake_list_revoked_badge": "Revoked", "wake_list_revoked_badge": "Revoked",
@@ -132,12 +132,12 @@
"wake_list_restored": "{{player}} can wake the server again.", "wake_list_restored": "{{player}} can wake the server again.",
"create_server_btn": "New server", "create_server_btn": "New server",
"create_server_title": "Create a server", "create_server_title": "Create a server",
"create_server_desc": "Pick from whitelisted images and sizes — the platform provisions the rest. No raw cluster config is exposed here.", "create_server_desc": "Select a whitelisted image and resource specification. The platform configures the server automatically. Raw cluster configuration editing is not provided.",
"create_server_name": "Name", "create_server_name": "Name",
"create_server_name_placeholder": "survival", "create_server_name_placeholder": "survival",
"create_server_name_hint": "3–32 lowercase letters, digits or hyphens.", "create_server_name_hint": "3–32 lowercase letters, digits or hyphens.",
"create_server_name_invalid": "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end.", "create_server_name_invalid": "Use 3–32 lowercase letters, digits or hyphens, with no hyphen at either end.",
"create_server_name_reserved": "“{{name}}” is kept for the platform. Pick another.", "create_server_name_reserved": "“{{name}}” is reserved by the platform. Use a different name.",
"create_server_subdomain": "Subdomain", "create_server_subdomain": "Subdomain",
"create_server_subdomain_placeholder": "survival", "create_server_subdomain_placeholder": "survival",
"create_server_subdomain_hint": "Will be reachable at {{host}}", "create_server_subdomain_hint": "Will be reachable at {{host}}",
@@ -146,15 +146,15 @@
"create_server_image": "Image", "create_server_image": "Image",
"create_server_image_loading": "Loading images…", "create_server_image_loading": "Loading images…",
"create_server_image_choose": "Choose an image", "create_server_image_choose": "Choose an image",
"create_server_image_unavailable": "Image list not loaded", "create_server_image_unavailable": "Image list unavailable",
"create_server_image_failed": "Couldn't load the image list: {{reason}}", "create_server_image_failed": "Unable to load the image list: {{reason}}",
"create_server_image_none": "No whitelisted images", "create_server_image_none": "No whitelisted images",
"create_server_memory": "Memory", "create_server_memory": "Memory",
"create_server_storage": "Storage", "create_server_storage": "Storage",
"create_server_policy": "Autostart policy", "create_server_policy": "Autostart policy",
"create_server_policy_owner": "Owner only — wake from the panel", "create_server_policy_owner": "Owner only — wake from the panel",
"create_server_policy_public": "Public — any player join wakes it", "create_server_policy_public": "Public — any player join wakes it",
"create_server_policy_allowlist": "Wake list — players who have joined wake it", "create_server_policy_allowlist": "Wake list — previous players with wake permission can start the server",
"create_server_cancel": "Cancel", "create_server_cancel": "Cancel",
"create_server_submit": "Create", "create_server_submit": "Create",
"edit_server_title": "Edit Server Config", "edit_server_title": "Edit Server Config",
@@ -162,8 +162,8 @@
"edit_server_desc_long": "Updating server spec. Fields left unchanged will retain their current values.", "edit_server_desc_long": "Updating server spec. Fields left unchanged will retain their current values.",
"edit_server_submit": "Save Config", "edit_server_submit": "Save Config",
"edit_server_image_build": "build {{id}}", "edit_server_image_build": "build {{id}}",
"edit_server_image_pinned_hint": "Locked to build {{id}}: the tag moving to a newer build (an installer re-run, say) leaves this server alone; only changing the image here moves it.", "edit_server_image_pinned_hint": "The server is pinned to build {{id}}. Changes to the image tag do not update this server automatically. A new build is used only after the image configuration is changed.",
"edit_server_image_warning": "After saving, the server runs the build this tag points to right now — re-picking the current tag also gets its newest build. If the Minecraft version changes, the world is upgraded on its next start, and upgraded chunks can't be opened by the old version again.", "edit_server_image_warning": "Saving selects the build currently referenced by the image tag. Selecting the same tag again also selects its latest build. A Minecraft version change may upgrade the world on the next startup. Upgraded chunks are not compatible with older versions.",
"edit_server_image_confirm": "I've backed up the world — change the image", "edit_server_image_confirm": "I've backed up the world — change the image",
"luckperms_group_name": "Group Name", "luckperms_group_name": "Group Name",
"luckperms_node": "Permission Node", "luckperms_node": "Permission Node",
@@ -183,9 +183,9 @@
"luckperms_remove_group_btn": "Remove Parent Group", "luckperms_remove_group_btn": "Remove Parent Group",
"luckperms_remove_perm_btn": "Remove Permission", "luckperms_remove_perm_btn": "Remove Permission",
"luckperms_perm_form_title": "Add or remove a permission", "luckperms_perm_form_title": "Add or remove a permission",
"luckperms_remove_perm_hint": "Remove clears the node in the given world context whether it was granted or denied; Value applies to Add only.", "luckperms_remove_perm_hint": "Removal deletes the permission node in the specified world context regardless of its allow or deny value. Permission values apply only to additions.",
"luckperms_groups_unread": "Couldn't read this player's groups. To remove one, type its name below and press Remove Parent Group.", "luckperms_groups_unread": "The player’s current groups could not be read. Enter a group name and select Remove Parent Group.",
"luckperms_perms_unread": "Couldn't read this player's permission nodes. To remove one, type the node (and world, if it was set per world) below and press Remove Permission.", "luckperms_perms_unread": "The player’s current permission nodes could not be read. Enter the node name and applicable world context, then select Remove Permission.",
"luckperms_groups_title": "Parent Groups", "luckperms_groups_title": "Parent Groups",
"luckperms_permissions_title": "Permission Nodes", "luckperms_permissions_title": "Permission Nodes",
"luckperms_node_column": "Permission Node", "luckperms_node_column": "Permission Node",
@@ -198,16 +198,16 @@
"luckperms_recent_actions": "Recent Actions History", "luckperms_recent_actions": "Recent Actions History",
"luckperms_no_recent_actions": "No recent actions.", "luckperms_no_recent_actions": "No recent actions.",
"luckperms_revert": "Revert", "luckperms_revert": "Revert",
"luckperms_revert_to_set": "Revert sets {{node}} back to {{value}}", "luckperms_revert_to_set": "Reverting restores {{node}} to {{value}}",
"luckperms_revert_to_unset": "Revert removes {{node}}", "luckperms_revert_to_unset": "Revert removes {{node}}",
"luckperms_revert_unknown": "Can't revert: value unknown", "luckperms_revert_unknown": "Cannot revert: previous value unknown",
"luckperms_remove_group": "Remove group {{group}}", "luckperms_remove_group": "Remove group {{group}}",
"luckperms_remove_perm": "Remove permission {{node}}", "luckperms_remove_perm": "Remove permission {{node}}",
"luckperms_revert_failed": "Couldn't revert: {{reason}}", "luckperms_revert_failed": "Unable to revert: {{reason}}",
"luckperms_revert_success": "Action reverted successfully!", "luckperms_revert_success": "Operation reverted.",
"luckperms_presets": "Presets", "luckperms_presets": "Presets",
"luckperms_no_parent_groups": "No parent groups assigned", "luckperms_no_parent_groups": "No parent groups assigned",
"luckperms_no_reply": "LuckPerms doesn't answer over RCON on this server, so the player's current groups and permission nodes can't be shown. Adding and removing still go through: type the group or node name below. Name-based changes only bind for players who have joined since LuckPerms was installed, so changes against a name that never joined may silently do nothing.", "luckperms_no_reply": "LuckPerms did not return query results through RCON, so current groups and permission nodes cannot be displayed. Additions and removals remain available. Name-based changes are reliable only for players who have joined since LuckPerms was installed. Changes for other names may not apply and may return no error.",
"luckperms_no_output": "(the server returned no output — result unknown)", "luckperms_no_output": "(the server returned no output — result unknown)",
"luckperms_global": "global", "luckperms_global": "global",
"luckperms_no_perms": "No explicit permission nodes assigned", "luckperms_no_perms": "No explicit permission nodes assigned",
@@ -215,43 +215,43 @@
"luckperms_clear_history": "Clear history", "luckperms_clear_history": "Clear history",
"edit_server_cpu": "CPU Limit", "edit_server_cpu": "CPU Limit",
"edit_server_cpu_placeholder": "e.g. 1, 1.5, 500m — empty for no limit", "edit_server_cpu_placeholder": "e.g. 1, 1.5, 500m — empty for no limit",
"edit_server_cpu_invalid": "Enter cores (1, 1.5) or millicores (500m), above zero.", "edit_server_cpu_invalid": "The CPU limit must be greater than zero, expressed as cores (for example, 1 or 1.5) or millicores (for example, 500m).",
"owned_filter_mine": "me", "owned_filter_mine": "me",
"edit_server_idle": "Idle auto-stop", "edit_server_idle": "Idle auto-stop",
"edit_server_idle_hint": "Stops the server after this long with nobody online, freeing memory and CPU. The next player to join wakes it.", "edit_server_idle_hint": "When the online count remains zero for the configured duration, the server stops automatically to release memory and CPU. Subsequent joins are handled according to the autostart policy.",
"edit_server_idle_unknown": "The player count can’t be read right now, so idle stop is paused until it can.", "edit_server_idle_unknown": "The online player count could not be read. Idle shutdown is paused until player count retrieval resumes.",
"idle_stop_never": "Never", "idle_stop_never": "Never",
"idle_stop_seconds": "{{count}} s", "idle_stop_seconds": "{{count}} s",
"idle_stop_minutes": "{{count}} min", "idle_stop_minutes": "{{count}} min",
"idle_stop_hours": "{{count}} h", "idle_stop_hours": "{{count}} h",
"stop_confirm_players_one": "1 player is online. They get a 30-second warning in game, then are disconnected. Stop anyway?", "stop_confirm_players_one": "One player is online. Shutdown sends an in-game notice and disconnects the player after 30 seconds. Confirm shutdown?",
"stop_confirm_players_other": "{{count}} players are online. They get a 30-second warning in game, then are disconnected. Stop anyway?", "stop_confirm_players_other": "There are {{count}} players online. Shutdown sends an in-game notice and disconnects players after 30 seconds. Confirm shutdown?",
"stop_confirm_unknown": "The player count can't be read, so players may be online. Stop anyway?", "stop_confirm_unknown": "The online player count could not be read. Players may still be connected. Confirm shutdown?",
"retire_card_title": "Give up server", "retire_card_title": "Give up server",
"retire_card_title_admin": "Give up or delete server", "retire_card_title_admin": "Give up or delete server",
"retire_card_desc": "Hand it back when you no longer need it: the world is archived, then the server is freed for someone else to claim.", "retire_card_desc": "Releasing the server archives its world and makes the server available for other accounts to claim.",
"retire_card_desc_admin": "Giving up archives the world and frees the server; deleting archives the world and removes the server, freeing its name and subdomain.", "retire_card_desc_admin": "Giving up archives the world and frees the server; deleting archives the world and removes the server, freeing its name and subdomain.",
"retire_release": "Give up", "retire_release": "Give up",
"retire_delete": "Delete", "retire_delete": "Delete",
"retire_release_title": "Give up \"{{name}}\"?", "retire_release_title": "Give up \"{{name}}\"?",
"retire_delete_title": "Delete \"{{name}}\"?", "retire_delete_title": "Delete \"{{name}}\"?",
"retire_step_stop": "The server stops now and can't be started until this is done.", "retire_step_stop": "The server enters the shutdown process. Startup is unavailable until reclamation completes or the request is cancelled.",
"retire_step_release": "At the next daily reclaim run the world is archived (kept 90 days by default), its storage deleted, and the server freed for someone else to claim.", "retire_step_release": "At the next daily reclaim run the world is archived (kept 90 days by default), its storage deleted, and the server freed for someone else to claim.",
"retire_step_delete": "At the next daily reclaim run the world is archived (kept 90 days by default), then its storage and this server are deleted, freeing the name and subdomain.", "retire_step_delete": "The next daily reclaim run archives the world (retained for 90 days by default), then deletes world storage and the server record, releasing the name and subdomain.",
"retire_step_cancel_owner": "You can cancel from the console until the run; until then it still counts toward your quota.", "retire_step_cancel_owner": "The request can be cancelled from the console before reclamation runs. The server counts toward the account quota until reclamation completes.",
"retire_step_cancel_admin": "An administrator can cancel from the console until the run.", "retire_step_cancel_admin": "An administrator can cancel from the console until the run.",
"retire_confirm_label": "Type the server name to confirm:", "retire_confirm_label": "Type the server name to confirm:",
"retire_confirm_release": "Give up server", "retire_confirm_release": "Give up server",
"retire_confirm_delete": "Delete server", "retire_confirm_delete": "Delete server",
"retiring_title_release": "This server has been given up", "retiring_title_release": "Server pending reclamation",
"retiring_title_delete": "This server is about to be deleted", "retiring_title_delete": "Server pending deletion",
"retiring_body_release": "Requested {{when}}. At the next daily reclaim run the world is archived, then its storage is deleted and the server freed. Until then it stays stopped, can't be started and still counts toward the quota.", "retiring_body_release": "Requested {{when}}. The next daily reclaim run archives the world, deletes world storage, and releases the server. Until completion, the server remains stopped, cannot be started, and counts toward the account quota.",
"retiring_body_delete": "Requested {{when}}. At the next daily reclaim run the world is archived, then its storage and this server are deleted. Until then it stays stopped and can't be started.", "retiring_body_delete": "Requested {{when}}. The next daily reclaim run archives the world and deletes world storage and the server record. Until completion, the server remains stopped and cannot be started.",
"retiring_cancel": "Cancel request", "retiring_cancel": "Cancel request",
"retiring_cancel_admin_only": "An administrator asked for the deletion, so only an administrator can cancel it.", "retiring_cancel_admin_only": "An administrator asked for the deletion, so only an administrator can cancel it.",
"retiring_badge_release": "Given up", "retiring_badge_release": "Given up",
"retiring_badge_delete": "Deleting", "retiring_badge_delete": "Deleting",
"retiring_badge_hint": "Given up or being deleted; handled at the next daily reclaim run, and it can't be started until then.", "retiring_badge_hint": "Release or deletion is pending until the next daily reclaim run. Startup is unavailable until completion or cancellation.",
"nodes": "Execution nodes", "nodes": "Execution nodes",
"node": "Execution node", "node": "Execution node",
"node_choose": "Choose an approved node", "node_choose": "Choose an approved node",
@@ -278,21 +278,21 @@
"startup_preparing": "Preparing files and pulling images", "startup_preparing": "Preparing files and pulling images",
"startup_booting": "Starting Minecraft and checking readiness", "startup_booting": "Starting Minecraft and checking readiness",
"startup_elapsed": "Startup requested {{seconds}} seconds ago", "startup_elapsed": "Startup requested {{seconds}} seconds ago",
"startup_memory": "Not enough schedulable memory. Reduce this server’s memory allocation, stop another server, or add node capacity. Java has not started yet.", "startup_memory": "Insufficient schedulable memory. Reduce this server’s memory allocation, stop other servers, or increase node capacity. Java has not started.",
"startup_hint": "Startup status refreshes automatically. Scheduling, file preparation and Minecraft startup are separate stages; no log output does not mean the process has hung.", "startup_hint": "Startup includes resource scheduling, file preparation, and game initialization. Status updates automatically. When logs are unavailable, use the current stage and diagnostics to assess progress.",
"status_stale": "Status unavailable", "status_stale": "Status unavailable",
"status_stale_detail": "Cannot confirm the current server state. Last successful read: {{time}}. Retrying automatically. {{error}}", "status_stale_detail": "Cannot confirm the current server state. Last successful read: {{time}}. Retrying automatically. {{error}}",
"startup_failed": "Startup blocked or container failed", "startup_failed": "Startup blocked or container failed",
"logs_unavailable_title": "Logs are not available yet", "logs_unavailable_title": "Logs unavailable",
"logs_unavailable_body": "The container has not started producing logs. Check the startup diagnostics above; logs attach automatically once available.", "logs_unavailable_body": "The container has not started producing logs. Check the startup diagnostics above; logs attach automatically once available.",
"startup_problem": "Startup issue", "startup_problem": "Startup issue",
"startup_waiting_resources": "Waiting for resources", "startup_waiting_resources": "Waiting for resources",
"startup_failed_hint": "Check the image, configuration and memory using the reported error. Correct the problem before restarting the server.", "startup_failed_hint": "Check the image, configuration and memory using the reported error. Correct the problem before restarting the server.",
"emergency_stop": "Emergency stop", "emergency_stop": "Emergency stop",
"emergency_stop_warning": "Use when normal stop is unresponsive. Scale the workload to zero independently of the game process and Operator, retaining normal shutdown grace. Worlds are not deleted, but unsaved changes may be lost. Fresh authentication is required.", "emergency_stop_warning": "When normal shutdown is unresponsive, set workload replicas directly to zero, bypassing the game process and Operator while retaining the normal termination grace period. World data is not deleted, but unsaved changes may be lost. Identity verification is required before execution.",
"emergency_stop_accepted": "Stop intent saved and workload scaled to zero. Wait for the Pod to exit; status will continue checking. Accepted does not mean stopped.", "emergency_stop_accepted": "Stop intent has been saved and workload replicas set to zero. The system will continue checking Pod termination. Request acceptance does not confirm process termination.",
"emergency_stop_unknown": "The result is unconfirmed. Stop intent may already be saved. Refresh status; if the control API is unreachable, use the host recovery steps.", "emergency_stop_unknown": "The result is unconfirmed. Stop intent may already be saved. Refresh status; if the control API is unreachable, use the host recovery steps.",
"host_recovery": "Control API unreachable? Show host recovery steps", "host_recovery": "Host emergency shutdown procedure",
"host_recovery_hint": "The panel cannot bypass an offline control API. Run these commands on the deployment host: save stop intent, scale to zero, then check game Pods.", "host_recovery_hint": "When the control API is offline, the panel cannot perform cluster operations. Run these commands on the deployment host to save stop intent, set replicas to zero, and inspect game Pods. Replace minecraft if the deployment uses a different namespace.",
"host_recovery_verify": "A terminating Pod or unreachable node does not confirm that the process stopped. Check the node and container runtime; force-deleting a Pod is not proof of shutdown." "host_recovery_verify": "If a Pod is terminating or its node is unreachable, process termination cannot be confirmed. Inspect the node and container runtime. Force-deleting a Pod is not evidence of process termination."
} }
@@ -6,7 +6,7 @@
"display_name_label": "Display Name", "display_name_label": "Display Name",
"display_name_placeholder": "e.g., Pixelmon Adventure Pack", "display_name_placeholder": "e.g., Pixelmon Adventure Pack",
"file_label": "Build Context (.tar.gz)", "file_label": "Build Context (.tar.gz)",
"file_drag_hint": "Drag and drop a .tar.gz file here, or click to browse", "file_drag_hint": "Drag a .tar.gz file into the upload area or select a file.",
"submit_btn": "Submit", "submit_btn": "Submit",
"submitting_create": "Creating submission...", "submitting_create": "Creating submission...",
"submitting_upload": "Uploading build context...", "submitting_upload": "Uploading build context...",
@@ -27,7 +27,7 @@
"build_status_failed": "Failed", "build_status_failed": "Failed",
"build_status_cancelled": "Cancelled", "build_status_cancelled": "Cancelled",
"no_submissions_title": "No Submissions Found", "no_submissions_title": "No Submissions Found",
"no_submissions_hint": "You haven't submitted any modpacks yet.", "no_submissions_hint": "The current account has no modpack submission records.",
"search_no_results": "No matches", "search_no_results": "No matches",
"search_no_results_hint": "Try a different search term or filter.", "search_no_results_hint": "Try a different search term or filter.",
"search_placeholder": "Search submissions...", "search_placeholder": "Search submissions...",
@@ -38,17 +38,17 @@
"field_context_ref": "Context Reference", "field_context_ref": "Context Reference",
"field_image_ref": "Image Reference", "field_image_ref": "Image Reference",
"field_context_sha256": "Context SHA-256", "field_context_sha256": "Context SHA-256",
"field_context_sha256_hint": "The digest of the file the platform received; compare it with sha256sum of your local file. Approval binds exactly this content.", "field_context_sha256_hint": "Digest of the content received by the server. Use sha256sum to compare it with the local file. Approval pins this content.",
"clear_btn": "Clear", "clear_btn": "Clear",
"file_hint": "Supports .tar.gz, up to {{limit}}", "file_hint": "Supports .tar.gz, up to {{limit}}",
"withdraw_btn": "Withdraw", "withdraw_btn": "Withdraw",
"withdraw_hint": "Withdrawing deletes this submission and its uploaded context, freeing your pending slot and storage budget.", "withdraw_hint": "Withdrawal deletes the submission and its uploaded build context, releasing the associated pending-review and storage quota.",
"withdraw_confirm": "Withdraw", "withdraw_confirm": "Withdraw",
"withdraw_cancel": "Cancel", "withdraw_cancel": "Cancel",
"upload_progress_label": "Upload progress", "upload_progress_label": "Upload progress",
"upload_progress": "{{sent}} of {{total}}", "upload_progress": "{{sent}} of {{total}}",
"pause_btn": "Pause", "pause_btn": "Pause",
"resume_btn": "Continue upload", "resume_btn": "Continue upload",
"resume_hint": "“{{name}}” is saved and {{sent}} of its build context is on the server. Submit again to continue from there.", "resume_hint": "“{{name}}” is saved and the server has received {{sent}} of its build context. Resubmission resumes from the confirmed offset.",
"restart_hint": "“{{name}}” is saved. The file you picked uploads from the start." "restart_hint": "“{{name}}” is saved. A newly selected file is uploaded from the beginning."
} }
+34 -34
View File
@@ -2,8 +2,8 @@
"title": "账户", "title": "账户",
"subtitle": "身份验证与 Minecraft 关联。", "subtitle": "身份验证与 Minecraft 关联。",
"sessions_title": "已登录的设备", "sessions_title": "已登录的设备",
"sessions_desc": "所有登录了你账号的浏览器。发现不认识的设备,请立即让它退出。", "sessions_desc": "查看当前账户已登录的浏览器会话。发现未知设备时,请撤销对应会话。",
"sessions_staff_idle": "运维账号的会话连续 30 分钟没有操作会自动退出。", "sessions_staff_idle": "运维账户会话连续 30 分钟无操作后将自动退出。",
"loading_sessions": "正在加载设备…", "loading_sessions": "正在加载设备…",
"sessions_empty": "没有设备以面板会话登录。", "sessions_empty": "没有设备以面板会话登录。",
"session_this_device": "本设备", "session_this_device": "本设备",
@@ -12,36 +12,36 @@
"session_active_now": "正在使用", "session_active_now": "正在使用",
"session_active": "{{when}}活跃", "session_active": "{{when}}活跃",
"session_signed_in": "{{when}}登录", "session_signed_in": "{{when}}登录",
"session_sign_out_aria": "让 {{device}} 退出登录", "session_sign_out_aria": "退出 {{device}} 的登录会话",
"session_signed_out_device": "{{device}} 已退出登录。", "session_signed_out_device": "{{device}} 已退出登录。",
"sessions_sign_out_others": "退出其它设备", "sessions_sign_out_others": "退出其它设备",
"sessions_sign_out_others_title": "让其它所有设备退出登录?", "sessions_sign_out_others_title": "退出所有其他设备的登录会话?",
"sessions_sign_out_others_desc": "除本设备外,其它设备都会退出登录,需要重新登录才能继续使用。", "sessions_sign_out_others_desc": "除当前设备外,所有设备的登录会话将失效,需要重新登录。",
"sessions_signed_out_others_other": "已让 {{count}} 台其它设备退出登录。", "sessions_signed_out_others_other": "已退出 {{count}} 台其他设备的登录会话。",
"sign_out": "退出登录", "sign_out": "退出登录",
"signing_out": "退出中…", "signing_out": "退出中…",
"minecraft_link": "Minecraft 关联", "minecraft_link": "Minecraft 关联",
"checking_link": "正在检查关联状态…", "checking_link": "正在检查关联状态…",
"linked_title": "Minecraft 账户已关联。", "linked_title": "Minecraft 账户已关联。",
"linked_desc": "关联后可认领及管理服务器——所有权相关操作(认领、启动、停止)已解锁。", "linked_desc": "Minecraft 账户已关联,可执行服务器认领、启动、停止及其他授权管理操作。",
"uuid_label": "UUID", "uuid_label": "UUID",
"step1_title": "在游戏中获取绑定码", "step1_title": "在游戏中获取绑定码",
"step1_desc_prefix": "加入任意服务器,在聊天框输入 ", "step1_desc_prefix": "连接服务器,在聊天栏执行 ",
"step1_desc_suffix": " 。服务器已确认你的身份,会提供一个一次性绑定码(约 10 分钟内有效)。", "step1_desc_suffix": "。身份验证通过后,服务器将提供有效期约为 10 分钟的一次性绑定码。",
"step2_title": "在此输入", "step2_title": "输入绑定码",
"link_code": "绑定码", "link_code": "绑定码",
"link_code_placeholder": "ABCD2345", "link_code_placeholder": "ABCD2345",
"verify_btn": "关联", "verify_btn": "关联",
"verifying": "验证中…", "verifying": "验证中…",
"email_verification": "邮箱验证", "email_verification": "邮箱验证",
"email_desc": "验证你的电子邮箱以确保账号安全。", "email_desc": "验证绑定邮箱,用于账户身份验证。",
"email_verified": "已验证", "email_verified": "已验证",
"send_code": "获取验证码", "send_code": "获取验证码",
"sending_code": "发送中…", "sending_code": "发送中…",
"email_step1": "输入电子邮箱", "email_step1": "输入电子邮箱",
"email_step1_desc": "输入你想要绑定的电子邮箱地址以获取验证码。", "email_step1_desc": "输入待绑定的电子邮箱地址以获取验证码。",
"email_step2": "输入验证码", "email_step2": "输入验证码",
"email_step2_desc": "输入发送至你邮箱的 6 位数字验证码(10 分钟内有效)。", "email_step2_desc": "输入邮件中的 6 位数字验证码,有效期为 10 分钟。",
"email_verify_btn": "验证", "email_verify_btn": "验证",
"email_verifying": "验证中…", "email_verifying": "验证中…",
"email_otp_sent": "验证码已发送。", "email_otp_sent": "验证码已发送。",
@@ -49,7 +49,7 @@
"change_email": "修改邮箱", "change_email": "修改邮箱",
"continue_btn": "继续", "continue_btn": "继续",
"passkeys": "Passkey 注册管理", "passkeys": "Passkey 注册管理",
"passkeys_desc": "Passkey 允许你使用指纹、面容或设备 PIN 码安全登录面板。", "passkeys_desc": "使用指纹、面容或设备 PIN,通过 Passkey 登录面板。",
"no_passkeys": "未绑定任何 Passkey。", "no_passkeys": "未绑定任何 Passkey。",
"loading_passkeys": "加载 Passkey 列表中…", "loading_passkeys": "加载 Passkey 列表中…",
"add_passkey": "注册新 Passkey", "add_passkey": "注册新 Passkey",
@@ -60,24 +60,24 @@
"last_used": "上次使用:", "last_used": "上次使用:",
"never": "从未", "never": "从未",
"passkey_delete_aria": "删除 Passkey「{{name}}」", "passkey_delete_aria": "删除 Passkey「{{name}}」",
"passkey_delete_title": "删除这个 Passkey?", "passkey_delete_title": "删除此 Passkey?",
"passkey_delete_desc": "「{{name}}」(注册于 {{created}})删除后无法再用它登录,需要时可以重新注册。其它设备上的登录也会一并退出。", "passkey_delete_desc": "删除「{{name}}」(注册于 {{created}})后,该密钥将无法用于登录。其他设备的登录会话将退出。需要时可重新注册。",
"passkey_delete_confirm": "删除", "passkey_delete_confirm": "删除",
"passkey_last_hint": "这是你唯一的 Passkey,邮箱也还没验证,删掉就没法登录了。先验证邮箱或再注册一个 Passkey,才能删除它。", "passkey_last_hint": "当前账户仅有一个 Passkey,且邮箱未验证,删除后将失去可用的登录方式。请先验证邮箱或注册其他 Passkey。",
"migration": "账户迁移", "migration": "账户迁移",
"migration_desc": "将被弃用账户名下的所有服务器转移到本账户。迁移在游戏内发起,在此完成。", "migration_desc": "将退役账户名下的全部服务器转移至当前账户。迁移请求在游戏内发起,在面板中完成。",
"account_id": "账户 ID", "account_id": "账户 ID",
"migration_checking": "正在检查迁移状态…", "migration_checking": "正在检查迁移状态…",
"migration_none_prefix": "当前没有进行中的迁移。若要将本账户的服务器转移到其他账户,请用被弃用的账户在游戏内输入 ", "migration_none_prefix": "当前没有进行中的迁移。若要将本账户的服务器转移到其他账户,请用被弃用的账户在游戏内输入 ",
"migration_none_suffix": " 。", "migration_none_suffix": " 。",
"migration_confirm_title": "确认身份", "migration_confirm_title": "账户身份验证",
"migration_confirm_desc": "迁移会停用本账户,因此需要先完成一次身份核验。", "migration_confirm_desc": "迁移会停用本账户,因此需要先完成一次身份核验。",
"migration_confirming": "确认中…", "migration_confirming": "确认中…",
"migration_confirm_passkey_btn": "使用 Passkey 确认", "migration_confirm_passkey_btn": "使用 Passkey 确认",
"migration_confirm_otp_btn": "发送验证码到我的邮箱", "migration_confirm_otp_btn": "发送验证码到我的邮箱",
"migration_issue_title": "指定目标账户", "migration_issue_title": "指定目标账户",
"migration_issue_desc": "将目标账户本页面显示的「账户 ID」粘贴到此处,然后签发一次性转移码。", "migration_issue_desc": "填写目标账户页面显示的「账户 ID」,然后签发一次性转移码。",
"migration_issue_deadline": "请在 {{time}} 前签发。确认只在当前浏览器有效,过时需要重新确认。签发后,已验证的邮箱会收到一封通知。", "migration_issue_deadline": "须在 {{time}} 前签发。身份确认仅在当前浏览器有效,过期后须重新验证。签发后将向已验证的邮箱发送通知。",
"migration_target_placeholder": "目标账户 ID", "migration_target_placeholder": "目标账户 ID",
"migration_issuing": "签发中…", "migration_issuing": "签发中…",
"migration_issue_btn": "签发转移码", "migration_issue_btn": "签发转移码",
@@ -85,13 +85,13 @@
"migration_code_desc": "请在过期前登录目标账户并在其页面兑换此码", "migration_code_desc": "请在过期前登录目标账户并在其页面兑换此码",
"migration_code_pending": "转移码已签发。请在过期前用目标账户完成兑换", "migration_code_pending": "转移码已签发。请在过期前用目标账户完成兑换",
"migration_redeem_title": "兑换转移码", "migration_redeem_title": "兑换转移码",
"migration_redeem_desc": "收到了被弃用账户的转移码?在此兑换即可接管其服务器。", "migration_redeem_desc": "兑换来源账户签发的转移码,将其服务器转移至当前账户。",
"migration_redeem_placeholder": "转移码", "migration_redeem_placeholder": "转移码",
"migration_redeeming": "兑换中…", "migration_redeeming": "兑换中…",
"migration_redeem_btn": "兑换", "migration_redeem_btn": "兑换",
"migration_redeemed": "迁移完成——已有 {{count}} 台服务器转移至本账户。", "migration_redeemed": "迁移完成——已有 {{count}} 台服务器转移至本账户。",
"reauth_title": "确认是你本人", "reauth_title": "账户身份验证",
"reauth_desc": "添加或删除 Passkey、修改邮箱前,需要用你已有的登录方式再验证一次,验证后 5 分钟内有效。", "reauth_desc": "此操作需要重新验证身份。请使用已注册的 Passkey 或已验证邮箱完成验证,验证结果有效期为 5 分钟。",
"reauth_checking": "正在查看可用的验证方式…", "reauth_checking": "正在查看可用的验证方式…",
"reauth_passkey_btn": "用 Passkey 验证", "reauth_passkey_btn": "用 Passkey 验证",
"reauth_passkey_waiting": "等待 Passkey…", "reauth_passkey_waiting": "等待 Passkey…",
@@ -103,24 +103,24 @@
"reauth_resend": "重新发送验证码", "reauth_resend": "重新发送验证码",
"reauth_sign_in_desc": "管理员账户也可以退出后重新登录,重新登录后 5 分钟内视为已验证。", "reauth_sign_in_desc": "管理员账户也可以退出后重新登录,重新登录后 5 分钟内视为已验证。",
"reauth_sign_in_btn": "退出并重新登录", "reauth_sign_in_btn": "退出并重新登录",
"reauth_done_continue": "已确认。点“继续”添加 Passkey。", "reauth_done_continue": "身份验证已完成。请选择「继续」以添加 Passkey。",
"email_change_desc": "输入新邮箱,我们会向新地址发送验证码。验证通过后登录验证码改发到新邮箱,旧邮箱会收到通知,其它设备会退出登录。", "email_change_desc": "输入新邮箱以接收验证码。验证通过后,登录验证码将发送至新邮箱,旧邮箱将收到变更通知,其他设备的登录会话将退出。",
"staff_linked_desc": "此角色已关联到你的管理员账户,进服时将识别你的管理身份。", "staff_linked_desc": "此角色已关联至当前管理员账户,加入游戏时将识别对应的管理身份。",
"staff_link_desc": "关联游戏角色后,进服时可识别你的管理身份。你现在已能使用全部面板管理功能,可以稍后再关联。", "staff_link_desc": "关联 Minecraft 角色后,加入游戏时将识别对应的管理身份。面板管理功能不依赖此关联,可稍后配置。",
"staff_sources_loading": "正在读取认证源…", "staff_sources_loading": "正在读取认证源…",
"staff_source_mojang": "Minecraft 正版", "staff_source_mojang": "Minecraft 正版",
"staff_source": "认证源", "staff_source": "认证源",
"staff_profile": "Minecraft 角色名或 UUID", "staff_profile": "Minecraft 角色名或 UUID",
"staff_lookup": "查询角色", "staff_lookup": "查询角色",
"staff_working": "处理中…", "staff_working": "处理中…",
"staff_lookup_unsupported": "此认证源需要配置 api_url 才能查询角色。你仍可继续管理面板。", "staff_lookup_unsupported": "此认证源须配置 api_url 才能查询角色。面板管理功能不受影响。",
"staff_confirm_desc": "确认将这个角色关联到你的管理员账户,进服时赋予对应的管理员身份。", "staff_confirm_desc": "将此角色关联至当前管理员账户,加入游戏时将识别对应的管理身份。",
"staff_confirm": "确认关联此角色", "staff_confirm": "确认关联此角色",
"staff_source_help": "“世界树”(Yggdrasil)是 Minecraft 的账号认证协议。正版与第三方认证站都通过它提供角色信息;请选择实际登录游戏所用的认证源,输入的是角色名或 UUID,不是邮箱。", "staff_source_help": "“世界树”(Yggdrasil)是 Minecraft 的账号认证协议。正版与第三方认证站都通过它提供角色信息;请选择实际登录游戏所用的认证源,输入的是角色名或 UUID,不是邮箱。",
"game_guide": "准备进入 Minecraft", "game_guide": "准备进入 Minecraft",
"game_version": "使用 Minecraft Java 版 {{version}} 连接登录服和大厅。目标服务器可能需要不同版本或模组,请查看对应服务器说明。", "game_version": "使用 Minecraft Java 版 {{version}} 连接登录服务和大厅。目标服务器可能需要其他版本或模组,请查看对应说明。",
"game_version_unknown": "登录服的客户端版本尚未配置,请由运维确认后填写 game_version;不要按面板版本猜测。", "game_version_unknown": "登录服务的客户端版本未配置。请由运维确认并填写 game_version;面板版本不代表游戏版本。",
"game_lobby": "连接后先经过登录服验证身份,随后进入大厅。大厅是选择目标服务器的中转站,可用 /menu 选择服务器并等待它启动;登录服或大厅未就绪时,可在服务列表查看状态。", "game_lobby": "玩家经登录服务完成身份验证后进入大厅。大厅用于选择目标服务器,可执行 /menu 打开服务器菜单并等待启动。登录服务或大厅未就绪时,可查看服务列表中的运行状态。",
"game_thirdparty": "使用第三方认证源时,启动器也须配置同一个认证站。角色查询只能读取身份,进服时仍需通过该站的登录验证。", "game_thirdparty": "使用第三方认证源时,启动器也须配置同一个认证站。角色查询只能读取身份,进服时仍需通过该站的登录验证。",
"staff_code_alternative": "认证站不支持查询?也可以使用游戏内链接码" "staff_code_alternative": "角色查询不可用时,可使用游戏内绑定码"
} }
+34 -34
View File
@@ -12,10 +12,10 @@
"image_source_built": "平台构建", "image_source_built": "平台构建",
"image_source_external": "外部", "image_source_external": "外部",
"image_source_recommended": "推荐", "image_source_recommended": "推荐",
"delete_image_title": "从白名单移除这个镜像?", "delete_image_title": "从白名单移除此镜像?",
"delete_image_desc": "移除后,新服务器将不能再选用", "delete_image_desc": "移除后,新服务器将不能再选用",
"delete_image_confirm": "移除镜像", "delete_image_confirm": "移除镜像",
"cancel_build_title": "停止这次构建?", "cancel_build_title": "停止此构建?",
"cancel_build_desc": "构建会立即停止,不会推送任何镜像:", "cancel_build_desc": "构建会立即停止,不会推送任何镜像:",
"cancel_build_keep": "继续构建", "cancel_build_keep": "继续构建",
"cancel_build_confirm": "停止构建", "cancel_build_confirm": "停止构建",
@@ -32,10 +32,10 @@
"download_context_btn": "下载上下文", "download_context_btn": "下载上下文",
"download_context_hint": "下载上传的构建上下文 (.tar.gz)——其中包含将被实际执行的 Dockerfile。", "download_context_hint": "下载上传的构建上下文 (.tar.gz)——其中包含将被实际执行的 Dockerfile。",
"context_sha256_label": "上下文 SHA-256", "context_sha256_label": "上下文 SHA-256",
"context_sha256_hint": "通过会绑定这个摘要:提交者之后再上传,构建会拒绝执行。离线审阅时请用 sha256sum 核对下载的文件。", "context_sha256_hint": "审核通过后将固定此摘要。后续上传与该摘要不一致时,构建将拒绝执行。离线审核时请使用 sha256sum 校验下载文件。",
"context_sha256_missing": "尚未上传构建上下文,或上传早于摘要记录,需要提交者重新上传。", "context_sha256_missing": "尚未上传构建上下文,或上传早于摘要记录,需要提交者重新上传。",
"context_sha256_downloaded": "与你刚下载的文件一致,通过会绑定这份内容。", "context_sha256_downloaded": "摘要与已下载文件一致,审核通过后将固定此内容。",
"context_sha256_stale": "你下载的版本({{digest}}…)已被新的上传替换,请重新下载审阅。", "context_sha256_stale": "已下载版本({{digest}}…)已被新上传替换。请重新下载并审核。",
"approve_needs_context": "没有可通过的上下文:提交者尚未上传或需要重新上传", "approve_needs_context": "没有可通过的上下文:提交者尚未上传或需要重新上传",
"base_image_label": "基础镜像", "base_image_label": "基础镜像",
"base_image_placeholder": "例如: library/postgres:15", "base_image_placeholder": "例如: library/postgres:15",
@@ -44,11 +44,11 @@
"build_status_succeeded": "成功", "build_status_succeeded": "成功",
"build_status_failed": "失败", "build_status_failed": "失败",
"build_status_cancelled": "已取消", "build_status_cancelled": "已取消",
"build_queued_hint": "同时运行的构建已达上限([registry] max_concurrent_builds),这个构建会在前面的构建结束后自动开始。", "build_queued_hint": "并发构建数量已达上限([registry] max_concurrent_builds)。任务将在运行中的构建结束后自动开始。",
"view_logs_btn": "扫描与日志", "view_logs_btn": "扫描与日志",
"cancel_build_btn": "取消", "cancel_build_btn": "取消",
"no_builds_title": "暂无构建任务", "no_builds_title": "暂无构建任务",
"no_builds_hint": "您可以使用右上角表单触发第一个镜像构建任务。", "no_builds_hint": "可使用构建表单提交镜像构建任务。",
"no_images_title": "无白名单镜像", "no_images_title": "无白名单镜像",
"no_images_hint": "白名单为空——平台管理员需通过 CLI 添加镜像。", "no_images_hint": "白名单为空——平台管理员需通过 CLI 添加镜像。",
"enabled": "已启用", "enabled": "已启用",
@@ -112,7 +112,7 @@
"updates_stat_end": "维护结束时间", "updates_stat_end": "维护结束时间",
"updates_stat_timezone": "本地时区", "updates_stat_timezone": "本地时区",
"dbbackup_title": "控制面数据库备份", "dbbackup_title": "控制面数据库备份",
"dbbackup_subtitle": "用户、Passkey、服务器归属、配额、审计日志和世界存档索引都在这个数据库里。主机上的 felis-db-backup.timer 每天备份一次,每次升级迁移前也会先快照。", "dbbackup_subtitle": "控制面数据库存储用户、Passkey、服务器归属、配额、审计日志及世界存档索引。felis-db-backup.timer 每日执行备份,升级迁移前另行创建快照。",
"dbbackup_status_ok": "正常", "dbbackup_status_ok": "正常",
"dbbackup_status_stale": "已过期", "dbbackup_status_stale": "已过期",
"dbbackup_status_never": "从未备份", "dbbackup_status_never": "从未备份",
@@ -129,18 +129,18 @@
"dbbackup_label_pre_restore": "恢复前快照", "dbbackup_label_pre_restore": "恢复前快照",
"dbbackup_label_offsite": "异地复制快照", "dbbackup_label_offsite": "异地复制快照",
"dbbackup_label_manual": "手动", "dbbackup_label_manual": "手动",
"dbbackup_never_title": "还没有记录到任何数据库备份", "dbbackup_never_title": "暂无数据库备份记录",
"dbbackup_stale_title": "最近一次备份已超过 {{hours}} 小时", "dbbackup_stale_title": "最近一次备份已超过 {{hours}} 小时",
"dbbackup_daily_stale_title": "每日定时备份最近一次完成是{{when}},已超过 {{hours}} 小时", "dbbackup_daily_stale_title": "每日定时备份最近一次完成是{{when}},已超过 {{hours}} 小时",
"dbbackup_daily_never_title": "每日定时任务还没有完成过一次备份", "dbbackup_daily_never_title": "每日定时任务尚未完成备份",
"dbbackup_daily_fix_hint": "上面这份更新的备份来自手动或其他任务,可以用来恢复,但定时任务修好之前它会一天天变旧。在主机上立即运行一次每日备份,再查看日志找出它停下的原因:", "dbbackup_daily_fix_hint": "较新的备份由手动操作或其他任务生成,可用于恢复,但不代表每日定时任务正常。请在主机执行每日备份,并检查定时任务日志:",
"dbbackup_servers_title": "最近一次备份缺少服务器定义", "dbbackup_servers_title": "最近一次备份缺少服务器定义",
"dbbackup_servers_hint": "备份时集群没有响应。用它恢复能找回账号和数据库,但一台服务器都不会有。等集群恢复响应后,在主机上重新备份一次:", "dbbackup_servers_hint": "备份期间集群未响应,归档缺少服务器定义。恢复数据库无法重建服务器。请在集群恢复后重新执行完整备份:",
"dbbackup_fix_hint": "此时主机出故障,账号、服务器归属和存档索引都无法恢复。在主机上立即运行一次每日备份,再查看它的日志找出定时任务错过的原因:", "dbbackup_fix_hint": "缺少有效数据库备份时,主机故障可能导致账户、服务器归属及存档索引无法恢复。请在主机立即执行每日备份,并检查定时任务日志:",
"dbbackup_copy": "复制命令", "dbbackup_copy": "复制命令",
"dbbackup_offsite_note": "备份只保存在这台主机上,硬盘损坏或主机丢失时会一起丢失。请定期把备份目录复制到另一台机器;恢复与灾备步骤见故障排查文档 §16。", "dbbackup_offsite_note": "备份仅保存在当前主机。磁盘或主机故障可能导致备份与原始数据同时丢失,请定期复制备份至独立主机。恢复与灾备步骤见故障排查文档 §16。",
"versions_title": "组件版本", "versions_title": "组件版本",
"versions_subtitle": "felis-update-check.timer 每天把本机运行的版本与上游最新发行版比对。Felis 不会自行应用任何更新。", "versions_subtitle": "felis-update-check.timer 每日比对本机组件版本与上游发行版本。Felis 不自动应用更新。",
"versions_status_available_other": "{{count}} 个可用更新", "versions_status_available_other": "{{count}} 个可用更新",
"versions_status_current": "已是最新", "versions_status_current": "已是最新",
"versions_status_stale": "检查已逾期", "versions_status_stale": "检查已逾期",
@@ -156,19 +156,19 @@
"versions_state_available": "有可用更新", "versions_state_available": "有可用更新",
"versions_state_current": "已是最新", "versions_state_current": "已是最新",
"versions_state_unknown": "无法访问发行源", "versions_state_unknown": "无法访问发行源",
"versions_state_unreadable": "读不到版本", "versions_state_unreadable": "版本读取失败",
"versions_state_pinned": "已固定", "versions_state_pinned": "已固定",
"versions_apply_hint": "在宿主机执行下面的只读检查,查看更新目标、范围和确切的 `--apply` 命令。确认后,在维护窗口内执行该命令:", "versions_apply_hint": "在宿主机执行以下只读检查,确认更新目标、范围及对应的 `--apply` 命令,然后在维护窗口内执行:",
"versions_never_title": "还没有记录过版本检查", "versions_never_title": "暂无版本检查记录",
"versions_stale_title": "最近一次版本检查已超过 {{hours}} 小时", "versions_stale_title": "最近一次版本检查已超过 {{hours}} 小时",
"versions_fix_hint": "下面的版本可能已过时。请先在主机上立即检查一次,再查看定时器日志找出它没有运行的原因:", "versions_fix_hint": "当前显示的版本信息可能已过期。请在主机重新执行检查,并查看定时器日志以确认未执行原因:",
"build_import_submission_label": "从已有的审核提交导入参数", "build_import_submission_label": "从已有的审核提交导入参数",
"build_import_submission_placeholder": "选择一个用户提交...", "build_import_submission_placeholder": "选择一个用户提交...",
"build_import_submission_none": "还没有可导入的提交", "build_import_submission_none": "暂无可导入的提交",
"build_import_submission_no_match": "没有匹配“{{query}}”的提交", "build_import_submission_no_match": "没有匹配“{{query}}”的提交",
"build_import_submission_unavailable": "提交列表未读到", "build_import_submission_unavailable": "提交列表不可用",
"build_import_submission_failed": "提交列表读取失败:{{reason}}", "build_import_submission_failed": "提交列表读取失败:{{reason}}",
"build_import_submission_more": "列出最新的 {{shown}} 条,共 {{total}} 条;更早的提交用上面的搜索找。", "build_import_submission_more": "当前显示最新 {{shown}} 条,共 {{total}} 条。可通过搜索查询更早的提交。",
"build_import_search_placeholder": "按名称、ID 或提交人搜索提交", "build_import_search_placeholder": "按名称、ID 或提交人搜索提交",
"build_import_submission_hint": "选择提交将自动填充镜像引用、构建上下文引用和对应的 Dockerfile 审计头。", "build_import_submission_hint": "选择提交将自动填充镜像引用、构建上下文引用和对应的 Dockerfile 审计头。",
"build_import_submission_warning_title": "警告:该提交状态为「{{status}}」", "build_import_submission_warning_title": "警告:该提交状态为「{{status}}」",
@@ -200,7 +200,7 @@
"users_col_created": "创建时间", "users_col_created": "创建时间",
"users_view_detail": "详情", "users_view_detail": "详情",
"user_not_found_title": "用户不存在", "user_not_found_title": "用户不存在",
"user_not_found_body": "这个账户可能已被删除,请回到用户列表重新查找。", "user_not_found_body": "账户不存在或已被删除。请返回用户列表重新查询。",
"users_empty_title": "未找到用户", "users_empty_title": "未找到用户",
"users_empty_hint": "没有匹配当前筛选条件的用户。", "users_empty_hint": "没有匹配当前筛选条件的用户。",
"users_back_to_list": "返回用户列表", "users_back_to_list": "返回用户列表",
@@ -208,8 +208,8 @@
"users_field_username": "用户名", "users_field_username": "用户名",
"users_field_email": "邮箱", "users_field_email": "邮箱",
"users_field_role": "角色", "users_field_role": "角色",
"users_role_locked_self": "不能修改自己的角色。", "users_role_locked_self": "不允许修改当前账户的角色。",
"users_role_locked_owner": "所有者的角色是固定的,只能在主机的应急控制台(sudo felis breakGlass)上管理。", "users_role_locked_owner": "Owner 角色不可修改。请使用主机应急控制台(sudo felis breakGlass)管理。",
"users_save_btn": "保存更改", "users_save_btn": "保存更改",
"users_save_ok": "更改保存成功。", "users_save_ok": "更改保存成功。",
"users_linked_accounts": "已关联的 Minecraft 账号", "users_linked_accounts": "已关联的 Minecraft 账号",
@@ -229,7 +229,7 @@
"users_quota_memory": "最大内存(MiB)", "users_quota_memory": "最大内存(MiB)",
"users_quota_storage": "最大存储(GiB)", "users_quota_storage": "最大存储(GiB)",
"users_quota_unlimited": "无限制", "users_quota_unlimited": "无限制",
"users_quota_hint": "留空为不限。填 0 表示这一项不给额度,该用户需要它的认领都会被拒绝。调低到已用量以下只挡新的认领,已有的服务器不受影响。", "users_quota_hint": "留空表示不限制,0 表示不分配该项资源。认领所需资源超出配额时将被拒绝。降低配额仅限制后续认领,不影响已有服务器。",
"users_sessions": "活跃会话", "users_sessions": "活跃会话",
"users_no_sessions": "无活跃会话。", "users_no_sessions": "无活跃会话。",
"users_session_expires": "过期时间", "users_session_expires": "过期时间",
@@ -242,8 +242,8 @@
"users_session_revoke_all_dlg_desc": "确定撤销所有活跃会话吗?用户将从所有设备登出。", "users_session_revoke_all_dlg_desc": "确定撤销所有活跃会话吗?用户将从所有设备登出。",
"users_session_revoke_confirm": "撤销", "users_session_revoke_confirm": "撤销",
"users_danger_zone": "危险操作区", "users_danger_zone": "危险操作区",
"users_protected_self": "不能禁用或删除当前登录的账号。", "users_protected_self": "不允许禁用或删除当前登录账户。",
"users_protected_owner": "所有者账号不能在面板里禁用或删除,只能在主机的应急控制台(sudo felis breakGlass)上管理。", "users_protected_owner": "面板不允许禁用或删除 Owner 账户。请使用主机应急控制台(sudo felis breakGlass)管理。",
"users_danger_disable": "禁用用户", "users_danger_disable": "禁用用户",
"users_danger_disable_desc": "阻止此用户登录。所有活跃会话将被立即撤销。", "users_danger_disable_desc": "阻止此用户登录。所有活跃会话将被立即撤销。",
"users_danger_disable_btn": "禁用用户", "users_danger_disable_btn": "禁用用户",
@@ -259,12 +259,12 @@
"users_danger_delete": "删除用户", "users_danger_delete": "删除用户",
"users_danger_delete_desc": "软删除此用户。其拥有的服务器将被释放,所有会话将被撤销,账号将被永久禁用。此操作无法通过面板撤销。", "users_danger_delete_desc": "软删除此用户。其拥有的服务器将被释放,所有会话将被撤销,账号将被永久禁用。此操作无法通过面板撤销。",
"users_danger_delete_btn": "删除用户", "users_danger_delete_btn": "删除用户",
"users_danger_delete_yes": "是的,永久删除", "users_danger_delete_yes": "确认永久删除",
"users_danger_passkeys": "解绑通行密钥", "users_danger_passkeys": "解绑通行密钥",
"users_danger_passkeys_desc": "解除该账号持有的全部通行密钥——密钥丢失或被盗用时的补救手段。其他登录方式(邮箱验证码、游戏内审批)不受影响。", "users_danger_passkeys_desc": "移除此账户的全部通行密钥,用于处理密钥丢失或泄露。邮箱验证码及游戏内审批等其他登录方式不受影响。",
"users_danger_passkeys_btn": "解绑通行密钥", "users_danger_passkeys_btn": "解绑通行密钥",
"users_danger_passkeys_dlg_title": "解绑通行密钥", "users_danger_passkeys_dlg_title": "解绑通行密钥",
"users_danger_passkeys_dlg_desc": "该账号的全部通行密钥将被永久移除。这不会锁死账号:用户可用其他方式登录后,在账户页重新注册密钥。", "users_danger_passkeys_dlg_desc": "此账户的全部通行密钥将被永久移除。其他登录方式仍可使用;登录后可在账户页面重新注册密钥。",
"users_danger_passkeys_yes": "确认解绑", "users_danger_passkeys_yes": "确认解绑",
"add_image_desc": "将外部 Docker 镜像引用录入白名单,供后续创建服务器使用。", "add_image_desc": "将外部 Docker 镜像引用录入白名单,供后续创建服务器使用。",
"images_search_placeholder": "搜索镜像名称或来源...", "images_search_placeholder": "搜索镜像名称或来源...",
@@ -279,14 +279,14 @@
"scan_blocked": "已拦截", "scan_blocked": "已拦截",
"scan_passed": "已通过", "scan_passed": "已通过",
"scan_loading": "正在读取安全扫描…", "scan_loading": "正在读取安全扫描…",
"scan_none": "这次构建没有留存安全扫描:它在扫描步骤前就结束了,或者早于开始留存扫描的版本。", "scan_none": "此构建未留存安全扫描记录,可能在扫描前结束,或创建于扫描留存功能启用前。",
"scan_load_failed": "读取安全扫描失败:{{reason}}", "scan_load_failed": "读取安全扫描失败:{{reason}}",
"scan_meta_other": "扫描于 {{when}} · {{count}} 个软件包", "scan_meta_other": "扫描于 {{when}} · {{count}} 个软件包",
"scan_policy": "拦截级别 {{severities}};尚无修复版本的漏洞只列出", "scan_policy": "拦截级别 {{severities}};尚无修复版本的漏洞只列出",
"scan_policy_unfixed": "拦截级别 {{severities}},尚无修复版本的漏洞同样拦截", "scan_policy_unfixed": "拦截级别 {{severities}},尚无修复版本的漏洞同样拦截",
"scan_counts_label": "按严重度统计的发现", "scan_counts_label": "按严重度统计的发现",
"scan_blocking_count_other": "其中 {{count}} 项拦截镜像", "scan_blocking_count_other": "其中 {{count}} 项拦截镜像",
"scan_clean": "Trivy 在这个镜像里没有发现任何问题。", "scan_clean": "Trivy 未在此镜像中检测到问题。",
"scan_showing": "显示 {{total}} 项中的 {{shown}} 项,完整报告列出全部发现。", "scan_showing": "显示 {{total}} 项中的 {{shown}} 项,完整报告列出全部发现。",
"scan_col_id": "编号", "scan_col_id": "编号",
"scan_col_severity": "严重度", "scan_col_severity": "严重度",
@@ -301,7 +301,7 @@
"scan_secret": "密钥:{{title}}", "scan_secret": "密钥:{{title}}",
"scan_download_report": "Trivy 报告", "scan_download_report": "Trivy 报告",
"scan_download_sbom": "SBOM", "scan_download_sbom": "SBOM",
"scan_not_kept": "这次构建未留存该文件:文件过大,或生成它的步骤失败。", "scan_not_kept": "构建未留存此文件,可能由于文件过大或生成步骤失败。",
"scan_download_failed": "下载失败:{{reason}}", "scan_download_failed": "下载失败:{{reason}}",
"platform_title": "平台设置", "platform_title": "平台设置",
"platform_subtitle": "统一管理服务器启动与唤醒的全局策略。", "platform_subtitle": "统一管理服务器启动与唤醒的全局策略。",
+21 -21
View File
@@ -13,56 +13,56 @@
"otp_placeholder": "请输入 6 位验证码", "otp_placeholder": "请输入 6 位验证码",
"send_otp": "发送验证码", "send_otp": "发送验证码",
"sending_otp": "发送中…", "sending_otp": "发送中…",
"otp_sent": "验证码已发送至您的邮箱,请查收。收到多封时,最近几分钟内的任一验证码都可用。", "otp_sent": "验证码已发送至绑定邮箱。同一有效期内收到的验证码均可使用。",
"otp_btn": "验证并登录", "otp_btn": "验证并登录",
"resend_in": "秒后重试", "resend_in": "秒后重试",
"passkey_btn": "使用 Passkey 登录", "passkey_btn": "使用 Passkey 登录",
"passkey_email_hint": "使用 Passkey 请在上方输入绑定邮箱,或留空直接免密登录。", "passkey_email_hint": "使用 Passkey 登录时,可填写绑定邮箱,或留空进行免密登录。",
"bind_code": "绑定码", "bind_code": "绑定码",
"bind_code_placeholder": "例如:ABCD2345", "bind_code_placeholder": "例如:ABCD2345",
"bind_hint": "用 Minecraft Java 版加入下面的地址。第一次进入时登录服会给出一次性绑定码,之后在游戏内输入 /link 获取新的。", "bind_hint": "使用 Minecraft Java 版连接下方地址。首次加入时,登录服务将提供一次性绑定码;后续可在游戏内执行 /link 获取新的绑定码。",
"bind_hint_no_address": "用 Minecraft Java 版加入本服务器。第一次进入时登录服会给出一次性绑定码,之后在游戏内输入 /link 获取新的。", "bind_hint_no_address": "使用 Minecraft Java 版连接本服务器。首次加入时,登录服务将提供一次性绑定码;后续可在游戏内执行 /link 获取新的绑定码。",
"no_owner_title": "还没有 Owner,暂时无法登录", "no_owner_title": "Owner 账户未初始化,登录不可用",
"no_owner_subtitle": "Owner 是这台服务器的所有者账号", "no_owner_subtitle": "Owner 为平台所有者账户",
"no_owner_intro": "这台服务器尚未创建 Owner 登录。主机管理员完成以下步骤后即可使用面板,无需启动 Minecraft。", "no_owner_intro": "平台尚未初始化 Owner 登录方式。主机管理员须完成以下设置,无需启动 Minecraft。",
"no_owner_step_setup": "在服务器终端运行下面的命令,完成部署与面板访问设置:", "no_owner_step_setup": "在服务器终端运行下面的命令,完成部署与面板访问设置:",
"no_owner_step_link": "用浏览器打开终端给出的一次性设置链接,登记邮箱并创建通行密钥。之后可在账户页关联 Minecraft 角色。", "no_owner_step_link": "在浏览器中打开终端提供的一次性设置链接,登记邮箱并创建通行密钥。Minecraft 角色可在账户页面关联。",
"no_owner_recheck": "已完成设置,重新检查", "no_owner_recheck": "已完成设置,重新检查",
"no_owner_rechecking": "检查中…", "no_owner_rechecking": "检查中…",
"no_owner_still_unbound": "还没有检测到 Owner。请确认终端已经显示设置链接。", "no_owner_still_unbound": "未检测到已初始化的 Owner 账户。请确认终端已生成设置链接并完成账户初始化。",
"bind_btn": "验证并登录", "bind_btn": "验证并登录",
"binding": "验证中…", "binding": "验证中…",
"op_hint": "仅限管理员:验证码将发送至您的邮箱,且需要一位在线管理员在游戏内批准此次登录。", "op_hint": "仅限管理员:验证码将发送至您的邮箱,且需要一位在线管理员在游戏内批准此次登录。",
"op_start_btn": "发起管理员登录", "op_start_btn": "发起管理员登录",
"op_approve_hint": "验证码已发送至您的邮箱(收到多封时,最近的任一封都可用)。请让一位在线管理员在游戏内批准此次请求:", "op_approve_hint": "验证码已发送至绑定邮箱,有效期内收到的验证码均可使用。此次请求须由在线管理员在游戏内批准:",
"op_waiting": "等待游戏内批准…", "op_waiting": "等待游戏内批准…",
"op_approved": "已批准——请输入邮件中的验证码。", "op_approved": "请求已批准。请输入邮件中的验证码。",
"op_restart": "重新开始", "op_restart": "重新开始",
"op_expires_in": "此请求将在 {{time}} 后过期。", "op_expires_in": "此请求将在 {{time}} 后过期。",
"op_expired": "此请求在使用前已过期,请重新发起登录。", "op_expired": "此请求在使用前已过期,请重新发起登录。",
"op_request_again": "重新发起", "op_request_again": "重新发起",
"op_poll_retrying": "暂时连不上 Felis,仍在等待,查询会放慢…", "op_poll_retrying": "无法连接 Felis。系统将降低查询频率并继续等待响应。",
"saving": "保存中…", "saving": "保存中…",
"setup_title": "初始化你的账户", "setup_title": "账户初始化",
"setup_welcome": "欢迎,{{name}}", "setup_welcome": "欢迎,{{name}}",
"setup_preparing": "正在准备初始化…", "setup_preparing": "正在准备初始化…",
"setup_entering": "正在进入控制台…", "setup_entering": "正在进入控制台…",
"setup_invalid_title": "无法开始设置", "setup_invalid_title": "无法启动初始化",
"setup_invalid_subtitle": "这个设置链接无效或已被使用", "setup_invalid_subtitle": "设置链接无效或已被使用",
"setup_invalid_hint_prefix": "请在服务器上重新运行 ", "setup_invalid_hint_prefix": "请在服务器上重新运行 ",
"setup_invalid_hint_suffix": " 获取新的设置链接,或直接前往登录页。", "setup_invalid_hint_suffix": " 获取新的设置链接,或直接前往登录页。",
"setup_failed_title": "设置没能开始", "setup_failed_title": "初始化启动失败",
"setup_failed_subtitle": "服务器暂时没能让你登录。设置链接仍然有效,可以再试一次。", "setup_failed_subtitle": "服务器未能完成登录。设置链接仍有效,请重试。",
"setup_retry": "重试", "setup_retry": "重试",
"setup_goto_login": "前往登录", "setup_goto_login": "前往登录",
"setup_email_step": "第一步 · 填写邮箱", "setup_email_step": "第一步 · 填写邮箱",
"setup_email_desc": "我们会为你的账户保存这个邮箱地址。发信服务(SMTP)和邮箱验证可稍后在设置中配置——完成初始化并不需要它。", "setup_email_desc": "邮箱地址将保存至当前账户。邮件服务(SMTP)及邮箱验证可稍后配置,不影响账户初始化。",
"setup_email_save": "保存并继续", "setup_email_save": "保存并继续",
"setup_passkey_step": "第二步 · 注册通行密钥", "setup_passkey_step": "第二步 · 注册通行密钥",
"setup_passkey_desc": "使用指纹、面容或设备 PIN 创建一个通行密钥,这将是你登录控制台的方式——完成初始化必须注册。", "setup_passkey_desc": "使用指纹、面容或设备 PIN 创建通行密钥,用于登录控制台。完成初始化前必须注册通行密钥。",
"setup_create_passkey": "创建通行密钥", "setup_create_passkey": "创建通行密钥",
"setup_registering": "注册中…", "setup_registering": "注册中…",
"setup_default_passkey_name": "默认通行密钥", "setup_default_passkey_name": "默认通行密钥",
"session_ended_notice": "你的登录已过期或已被撤销,请重新登录。登录后会回到刚才的页面。", "session_ended_notice": "登录会话已过期或被撤销。请重新登录,完成后将返回原访问页面。",
"setup_game_optional": "完成登录设置即可进入管理面板。Minecraft 角色可以稍后在账户页关联。" "setup_game_optional": "完成登录设置后可进入管理面板。Minecraft 角色可稍后在账户页面关联。"
} }
+20 -20
View File
@@ -1,6 +1,6 @@
{ {
"title": "备份与恢复", "title": "备份与恢复",
"subtitle": "玩过的世界会在服务器停止后每天自动备份一次,长期闲置被自动回收前也会自动备份。可以使用备份随时恢复世界,恢复前需要先停止服务器。", "subtitle": "已运行且停止的服务器每日自动备份,长期闲置回收前也会备份。恢复世界前须停止服务器。",
"back_to_console": "返回控制台", "back_to_console": "返回控制台",
"not_yours_title": "无权访问备份", "not_yours_title": "无权访问备份",
"not_yours_body": "只有所有者或管理员才能查看、恢复和删除该服务器的备份。", "not_yours_body": "只有所有者或管理员才能查看、恢复和删除该服务器的备份。",
@@ -14,17 +14,17 @@
"reason_label": "原因:{{reason}}", "reason_label": "原因:{{reason}}",
"backup_now": "立即备份", "backup_now": "立即备份",
"backup_in_progress": "正在启动备份……", "backup_in_progress": "正在启动备份……",
"backup_started": "已开始备份——进度可在下方“最近操作”中查看。", "backup_started": "备份任务已启动,可在「最近操作」中查看进度。",
"backup_world_busy": "已有备份、恢复或世界导出在进行,等它结束再备份。", "backup_world_busy": "备份、恢复或世界导出任务进行中。请等待当前操作结束后再备份。",
"backup_requires_stopped": "备份前需要先停止服务器。", "backup_requires_stopped": "备份前需要先停止服务器。",
"export_world": "导出世界", "export_world": "导出世界",
"export_world_hint": "把服务器当前的世界打包成 .tar.gz 下载。", "export_world_hint": "将当前世界打包为 .tar.gz 文件并下载。",
"export_preparing": "准备中……", "export_preparing": "准备中……",
"export_requires_stopped": "导出世界前需要先停止服务器。", "export_requires_stopped": "导出世界前需要先停止服务器。",
"export_world_busy": "这个世界正在备份、恢复或导出,等它结束再导出。", "export_world_busy": "世界正在执行备份、恢复或导出。请等待当前操作结束后再导出。",
"export_another": "另一个下载正在准备,等它开始后再试。", "export_another": "其他下载正在准备。请等待准备完成后重试。",
"export_progress_world": "正在打包世界——准备好后会自动开始下载。请保持此页面打开;下载结束前服务器无法启动。", "export_progress_world": "正在归档世界,完成后将自动开始下载。请保持页面打开,下载结束前服务器无法启动。",
"export_progress_backup": "正在准备备份——稍后会自动开始下载。请保持此页面打开。", "export_progress_backup": "正在准备备份下载,完成后将自动开始传输。请保持页面打开。",
"export_started_world": "已开始下载:浏览器正在保存 {{filename}}。下载结束后服务器即可再次启动。", "export_started_world": "已开始下载:浏览器正在保存 {{filename}}。下载结束后服务器即可再次启动。",
"export_started_backup": "已开始下载:浏览器正在保存 {{filename}}。", "export_started_backup": "已开始下载:浏览器正在保存 {{filename}}。",
"export_failed": "导出失败,没有下载任何内容。", "export_failed": "导出失败,没有下载任何内容。",
@@ -34,7 +34,7 @@
"expired": "已过期", "expired": "已过期",
"former_owner": "原世界所有者:{{owner}}", "former_owner": "原世界所有者:{{owner}}",
"empty_title": "暂无备份", "empty_title": "暂无备份",
"empty_hint": "服务器运行过并停止后会自动生成定时备份,也可以点“立即备份”马上创建。", "empty_hint": "已运行且停止的服务器将自动生成定时备份,也可执行「立即备份」手动创建。",
"jobs_title": "最近操作", "jobs_title": "最近操作",
"jobs_empty": "暂无备份、恢复或导出操作记录。", "jobs_empty": "暂无备份、恢复或导出操作记录。",
"job_backup": "备份", "job_backup": "备份",
@@ -60,37 +60,37 @@
"restore_btn": "恢复此备份", "restore_btn": "恢复此备份",
"restore_btn_short": "恢复", "restore_btn_short": "恢复",
"restore_confirm": "恢复备份将停止服务器(所有在线玩家将被断开),并用该备份完全覆盖当前世界的全部数据。未勾选安全快照时此操作不可逆,请确认是否继续?", "restore_confirm": "恢复备份将停止服务器(所有在线玩家将被断开),并用该备份完全覆盖当前世界的全部数据。未勾选安全快照时此操作不可逆,请确认是否继续?",
"restore_confirm_safe": "恢复备份将停止服务器(所有在线玩家将被断开),先为当前世界做一份安全快照,快照成功后再用该备份覆盖世界。选错了备份,可以用这份“恢复前快照”恢复回来。确认继续?", "restore_confirm_safe": "恢复将停止服务器并断开在线玩家,先创建当前世界的安全快照,快照成功后再用所选备份覆盖世界。需要撤销恢复结果时,可恢复「恢复前快照」。确认继续?",
"safety_snapshot_label": "先为当前世界做安全快照(推荐)", "safety_snapshot_label": "先为当前世界做安全快照(推荐)",
"safety_snapshot_hint": "快照成功后自动开始恢复;快照失败则不恢复,世界保持原样。", "safety_snapshot_hint": "快照成功后自动开始恢复;快照失败则不恢复,世界保持原样。",
"safety_snapshot_off_hint": "不做快照,立即覆盖当前世界,之后无法找回。", "safety_snapshot_off_hint": "不创建安全快照,直接覆盖当前世界。未备份的现有数据将无法恢复。",
"restore_confirm_yes": "确认恢复", "restore_confirm_yes": "确认恢复",
"restore_confirm_yes_unsafe": "直接覆盖并恢复", "restore_confirm_yes_unsafe": "直接覆盖并恢复",
"cancel": "取消", "cancel": "取消",
"restore_started_short": "已启动恢复", "restore_started_short": "已启动恢复",
"restore_snapshot_started_short": "快照中,随后恢复", "restore_snapshot_started_short": "快照中,随后恢复",
"stopping": "正在停止服务器(有玩家在线时先在游戏里预告 30 秒)……", "stopping": "正在停止服务器;有玩家在线时将提前 30 秒发送游戏内通知…",
"restoring": "正在恢复备份……", "restoring": "正在恢复备份……",
"stop_timeout": "服务器停止超时。请关闭此窗口,稍后重试。", "stop_timeout": "服务器停止超时。请关闭此窗口,稍后重试。",
"corrupt_badge": "已损坏", "corrupt_badge": "已损坏",
"corrupt_hint": "回读校验发现这份归档与写入时不一致,已无法完整恢复。它会保留到过期,方便排查,不需要时也可以直接删除。", "corrupt_hint": "归档回读校验失败,无法完整恢复。归档将保留至过期,以便排查;不再需要时可手动删除。",
"corrupt_short": "已损坏", "corrupt_short": "已损坏",
"verified_at": "{{when}}已校验", "verified_at": "{{when}}已校验",
"skipped_entries": "{{count}} 个条目未归档", "skipped_entries": "{{count}} 个条目未归档",
"skipped_entries_hint": "世界目录里有符号链接等非普通文件,归档时跳过了它们,恢复后这些条目不会存在。", "skipped_entries_hint": "归档已跳过符号链接及其他非普通文件条目,恢复时不包含这些条目。",
"col_created": "创建时间", "col_created": "创建时间",
"col_size": "大小", "col_size": "大小",
"col_expires": "过期时间", "col_expires": "过期时间",
"col_owner": "原所有者", "col_owner": "原所有者",
"col_actions": "操作", "col_actions": "操作",
"delete_btn": "删除备份", "delete_btn": "删除备份",
"delete_title": "删除这份备份?", "delete_title": "删除此备份?",
"delete_confirm": "{{relative}}({{absolute}})的这份备份({{size}})会立刻从列表中移除,之后不能再用它恢复,也无法撤销。", "delete_confirm": "备份 {{relative}}({{absolute}}),大小 {{size}},将立即从列表移除并停止提供恢复。此操作不可撤销。",
"delete_cleanup_note": "归档文件在下一次每日清理时删除,异地副本在下一次同步时删除。", "delete_cleanup_note": "归档文件在下一次每日清理时删除,异地副本在下一次同步时删除。",
"delete_archive_warning": "这份备份是世界被回收或放弃时存下的归档。服务器上的世界如果还没从它恢复回来,删掉它就再也找不回这个世界了。", "delete_archive_warning": "此备份为世界回收或服务器放弃时生成的归档。删除后,缺少其他可用副本的世界数据将无法恢复。",
"delete_only_warning": "这是这台服务器目前唯一的一份备份。", "delete_only_warning": "此备份为服务器当前唯一的备份。",
"delete_confirm_yes": "删除备份", "delete_confirm_yes": "删除备份",
"delete_restore_busy": "有一次恢复可能正在读取这份备份,等恢复完成后再删除。", "delete_restore_busy": "恢复任务可能正在读取此备份。请等待恢复完成后再删除。",
"deleted": "备份已删除。", "deleted": "备份已删除。",
"delete_gone": "这份备份已经不在了,列表已刷新。" "delete_gone": "备份已不存在,列表已刷新。"
} }
+11 -11
View File
@@ -7,13 +7,13 @@
"creating": "创建中…", "creating": "创建中…",
"close_sr": "关闭", "close_sr": "关闭",
"not_authorized_title": "无权访问", "not_authorized_title": "无权访问",
"not_authorized_body": "此区域仅供平台管理员使用。若通过直接输入 URL 到达此页面,说明你的账号无管理员权限,API 侧同样会拒绝该请求。", "not_authorized_body": "当前账户无平台管理权限,无法访问此页面。",
"back_to_dashboard": "返回首页", "back_to_dashboard": "返回首页",
"not_wired_yet": "功能尚未接入", "not_wired_yet": "功能尚未接入",
"pending_backend_prefix": "待接入接口:", "pending_backend_prefix": "待接入接口:",
"checking_access": "正在验证权限…", "checking_access": "正在验证权限…",
"access_unknown_title": "暂时无法确认你的权限", "access_unknown_title": "无法确认账户权限",
"access_unknown_body": "账户信息没有读到,读到之前这一页先不显示。", "access_unknown_body": "账户信息读取失败。确认访问权限前,页面内容不可用。",
"loading_config": "正在加载配置…", "loading_config": "正在加载配置…",
"brand_name": "Felis", "brand_name": "Felis",
"brand_tagline": "Kubernetes 原生的 Minecraft 管理平台", "brand_tagline": "Kubernetes 原生的 Minecraft 管理平台",
@@ -23,22 +23,22 @@
"pagination_prev": "上一页", "pagination_prev": "上一页",
"pagination_next": "下一页", "pagination_next": "下一页",
"pagination_of": "第 {{current}} / {{total}} 页", "pagination_of": "第 {{current}} / {{total}} 页",
"crash_title": "这个页面出错了", "crash_title": "页面加载异常",
"crash_body": "面板渲染这个页面时遇到了问题。重新加载通常就能恢复;如果反复出现,请把下面的错误信息发给管理员。", "crash_body": "页面渲染失败。请重新加载;若问题持续,请向管理员提供错误信息。",
"crash_updated_title": "面板已更新", "crash_updated_title": "面板已更新",
"crash_updated_body": "面板刚发布了新版本,这个页面需要重新加载才能打开。", "crash_updated_body": "面板版本已更新,请重新加载页面。",
"crash_reload": "重新加载", "crash_reload": "重新加载",
"crash_details": "错误信息", "crash_details": "错误信息",
"crash_copy": "复制错误信息", "crash_copy": "复制错误信息",
"not_found_title": "找不到这个页面", "not_found_title": "页面不存在",
"not_found_body": "链接里的名称不合法,或者这个页面不存在。", "not_found_body": "链接中的名称无效,或请求的页面不存在。",
"connection_lost": "连不上 Felis:网络断开了,或者 Cloudflare Access 的登录已过期。网络恢复后会自动消失;Access 过期需要刷新页面重新登录。", "connection_lost": "无法连接 Felis,可能由于网络中断或 Cloudflare Access 会话过期。网络恢复后将自动重新连接;Access 会话过期时,请刷新页面并重新登录。",
"reload_page": "刷新页面", "reload_page": "刷新页面",
"config_unavailable": "面板配置(/config.json)没有加载成功,这里显示的服务器地址和链接可能不对。刷新页面可重试。", "config_unavailable": "面板配置(/config.json)加载失败,服务器地址和链接可能不准确。请刷新页面重试。",
"build_dev": "开发版", "build_dev": "开发版",
"open_menu": "打开菜单", "open_menu": "打开菜单",
"copy_address": "复制地址 {{address}}", "copy_address": "复制地址 {{address}}",
"address_copied": "已复制", "address_copied": "已复制",
"address_copy_failed": "已选中,请用系统菜单复制", "address_copy_failed": "已选中,请用系统菜单复制",
"refresh_failed": "刷新失败,下面是上次读到的状态。" "refresh_failed": "刷新失败,当前显示上次成功读取的状态。"
} }
@@ -1,7 +1,7 @@
{ {
"title": "仪表盘", "title": "仪表盘",
"subtitle": "服务器运行状态一览。", "subtitle": "服务器运行状态一览。",
"no_servers_hint": "创建一个新服务器或认领一台现有服务器即可开始。", "no_servers_hint": "可创建服务器、认领已有服务器,或联系管理员分配。",
"stat_servers": "服务器", "stat_servers": "服务器",
"stat_running": "运行中", "stat_running": "运行中",
"stat_players_online": "在线玩家", "stat_players_online": "在线玩家",
@@ -12,9 +12,9 @@
"active_load": "玩家在线负载", "active_load": "玩家在线负载",
"status_distribution": "节点状态分布", "status_distribution": "节点状态分布",
"account_linked_title": "已关联游戏身份", "account_linked_title": "已关联游戏身份",
"account_linked_desc": "您的 Web 身份已成功绑定至 Minecraft 角色。拥有完整的所有权认领及启动权限。", "account_linked_desc": "Web 账户已关联至 Minecraft 角色,可执行已授权的认领与启动操作。",
"account_unlinked_title": "未关联游戏角色", "account_unlinked_title": "未关联游戏角色",
"account_unlinked_desc": "您需要关联 Minecraft 角色才能认领或操作服务器。请立即前往个人中心绑定。", "account_unlinked_desc": "认领及操作服务器前须关联 Minecraft 角色。请在账户页面完成关联。",
"go_link": "前往绑定", "go_link": "前往绑定",
"link_status_checking": "正在确认是否已关联游戏角色…", "link_status_checking": "正在确认是否已关联游戏角色…",
"link_status_failed": "暂时无法获取绑定状态", "link_status_failed": "暂时无法获取绑定状态",
@@ -30,13 +30,13 @@
"spec_failed_ratio": "节点故障率", "spec_failed_ratio": "节点故障率",
"no_servers_monitor": "当前集群暂无服务器实例", "no_servers_monitor": "当前集群暂无服务器实例",
"images_count": "个支持的白名单镜像环境", "images_count": "个支持的白名单镜像环境",
"images_security_note": "。所有后端镜像运行前均通过 Trivy CVE 漏洞扫描,强制在隔离的安全容器中托管。", "images_security_note": "。镜像可用范围由白名单控制,构建扫描结果可在构建记录中查看。",
"manage_game_character": "管理游戏角色", "manage_game_character": "管理游戏角色",
"instances_count": "{{count}} 实例", "instances_count": "{{count}} 实例",
"quick_start_guide": "快速入门指引:", "quick_start_guide": "快速入门指引:",
"quick_start_step1": "认领无主服后,系统会自动将您的 Minecraft UUID 添加至准入白名单。", "quick_start_step1": "认领服务器后,系统将对应 Minecraft UUID 加入访问白名单。",
"quick_start_step2": "服务器如果长时间处于无人在线空闲状态,平台会自动执行休眠存档以节约资源。", "quick_start_step2": "服务器达到配置的空闲时长后将自动停止并保存世界,以释放运行资源。",
"quick_start_step3": "游戏入口统一由 Velocity 网关承载,只需连接您的专用子域名即可。", "quick_start_step3": "游戏入口由 Velocity 网关提供,通过服务器专用子域名连接。",
"legend_title": "3D 节点监控图例", "legend_title": "3D 节点监控图例",
"legend_running": "运行中", "legend_running": "运行中",
"legend_starting": "启动中", "legend_starting": "启动中",
@@ -45,10 +45,10 @@
"legend_failed": "异常失败", "legend_failed": "异常失败",
"legend_unknown": "未知状态", "legend_unknown": "未知状态",
"welcome_title": "欢迎使用 Felis 编排平台", "welcome_title": "欢迎使用 Felis 编排平台",
"welcome_desc": "您已成功登录用户控制台。您可以在「我的服务器」页面查看并认领分配给您的专属节点,或者对其进行休眠、启动和控制台日志监控。", "welcome_desc": "在「我的服务器」中查看、认领和管理已授权的服务器,并执行启动、停止及日志查看操作。",
"go_to_servers_btn": "操作我的服务器", "go_to_servers_btn": "操作我的服务器",
"fleet_flat_webgl": "浏览器未启用 WebGL,已切换为平面视图。", "fleet_flat_webgl": "浏览器未启用 WebGL,已切换为平面视图。",
"fleet_flat_error": "3D 视图加载失败,已切换为平面视图。", "fleet_flat_error": "3D 视图加载失败,已切换为平面视图。",
"fleet_flat_empty": "还没有服务器", "fleet_flat_empty": "暂无服务器",
"staff_unlinked_desc": "Minecraft 角色尚未关联。面板管理功能已可使用;准备进入游戏时,可在账户页选择认证源并关联角色。" "staff_unlinked_desc": "Minecraft 角色尚未关联。面板管理功能已可使用;准备进入游戏时,可在账户页选择认证源并关联角色。"
} }
+65 -65
View File
@@ -2,107 +2,107 @@
"local_auth_disabled": "当前部署已禁用本地登录,请通过组织的安全入口访问控制台。", "local_auth_disabled": "当前部署已禁用本地登录,请通过组织的安全入口访问控制台。",
"staff_account": "该账户为管理员账户——请使用管理员登录。", "staff_account": "该账户为管理员账户——请使用管理员登录。",
"not_linked": "请先关联 Minecraft 账户(账户页 → 关联)。", "not_linked": "请先关联 Minecraft 账户(账户页 → 关联)。",
"invalid_code": "代码无效或已过期——请重新获取后再试。", "invalid_code": "绑定码无效或已过期,请重新获取。",
"already_linked": "该 Minecraft 账户已关联至其他用户。", "already_linked": "该 Minecraft 账户已关联至其他用户。",
"bad_mc_uuid": "这不是 Minecraft UUID。它的样子是 069a79f4-44e9-4726-a5be-fca90e38aaf5,短横线可带可不带。", "bad_mc_uuid": "Minecraft UUID 格式无效。示例:069a79f4-44e9-4726-a5be-fca90e38aaf5,可省略连字符。",
"quota_exceeded": "服务器数量已达配额上限。", "quota_exceeded": "服务器数量已达配额上限。",
"already_claimed": "该服务器已被他人抢先认领。", "already_claimed": "该服务器已被其他账户认领。",
"image_not_whitelisted": "该镜像未在白名单中。", "image_not_whitelisted": "该镜像未在白名单中。",
"image_not_in_registry": "内部镜像仓库里没有这个镜像标签,可能从未推送过,或已被删除。请重新构建或推送该镜像后再试。", "image_not_in_registry": "内部镜像仓库不存在此镜像标签,可能尚未推送或已被删除。请重新构建或推送镜像后重试。",
"registry_unavailable": "暂时连不上内部镜像仓库,无法确定该镜像对应的构建,请稍后再试。", "registry_unavailable": "无法连接内部镜像仓库,镜像构建版本未确认。请稍后重试。",
"image_change_unconfirmed": "更换镜像会让世界用新构建的 Minecraft 版本打开,区块升级后无法再用旧版本打开。请先备份世界,再勾选确认后保存。", "image_change_unconfirmed": "更换镜像后,世界将在下次启动时使用新构建的 Minecraft 版本。升级后的区块不支持旧版本。请先备份世界并勾选确认,再保存配置。",
"subdomain_taken": "该子域名已被占用。", "subdomain_taken": "该子域名已被占用。",
"already_exists": "同名服务器已存在。", "already_exists": "同名服务器已存在。",
"world_volume_exists": "同名的旧服务器留下了世界卷。请换一个名称,或请运维删除旧的世界卷。", "world_volume_exists": "同名服务器的世界卷仍存在。请使用其他名称,或由运维确认并处理旧世界卷。",
"server_retiring": "这台服务器已被放弃或正在删除,下一次每日回收时处理。撤销之前它不能启动或被认领。", "server_retiring": "服务器已进入放弃或删除流程,将在下一次每日回收时处理。撤销请求前无法启动或认领。",
"confirm_mismatch": "输入的名称和服务器名称不一致。", "confirm_mismatch": "输入的名称和服务器名称不一致。",
"system_server": "系统服务器由平台管理,不能放弃或删除。", "system_server": "系统服务器由平台管理,不允许放弃或删除。",
"world_volume_orphaned": "这台服务器的 MinecraftServer 已被手动删除,但世界卷还在。请运维先归档并删除这个世界卷,再删除服务器。", "world_volume_orphaned": "服务器的 MinecraftServer 资源已被手动删除,但世界卷仍存在。请由运维归档并删除残留世界卷,再删除服务器记录。",
"cooldown": "启动冷却中——请稍后再试。", "cooldown": "启动冷却中——请稍后再试。",
"not_running": "服务器未在运行——请先启动它再管理访问权限。", "not_running": "服务器未运行。请先启动服务器,再管理访问权限。",
"console_unavailable": "暂时无法连接服务器控制台,请稍后重试。", "console_unavailable": "暂时无法连接服务器控制台,请稍后重试。",
"luckperms_missing": "这台服务器没有装 LuckPerms,权限和用户组的改动不会生效。大厅请让运维重跑安装脚本来补上;其他服务器需要先装 LuckPerms 插件。", "luckperms_missing": "服务器未安装 LuckPerms,无法应用权限和用户组更改。大厅需由运维重新执行安装脚本;其他服务器须先安装 LuckPerms 插件。",
"no_backup": "这台服务器暂时没有可回档的备份。", "no_backup": "服务器暂无可恢复的备份。",
"backup_corrupt": "这份备份回读校验未通过,已无法完整恢复——请选择另一份备份。", "backup_corrupt": "备份回读校验失败,无法完整恢复。请选择其他备份。",
"not_stopped": "请先把服务器完全停止——这项操作要改动世界存储卷,运行中的服务器独占着它。", "not_stopped": "此操作需要修改世界卷,运行中的服务器独占挂载该卷。请先完全停止服务器。",
"maintenance_in_progress": "这台服务器的世界正在回档、备份、导出、下载文件、改动文件或闲置回收,等它完成后再试。回档、备份和改动文件通常一两分钟,导出和下载要等浏览器下载完,闲置回收视世界大小可能更久。", "maintenance_in_progress": "世界正在执行恢复、备份、导出、文件下载、文件修改或回收操作。请等待操作结束后重试。导出和下载需等待浏览器完成传输。",
"no_world_volume": "这台服务器还没有世界卷——先启动一次让它创建,然后再试。", "no_world_volume": "服务器尚未创建世界卷。请先启动服务器以创建世界卷,再重试。",
"file_changed": "这个文件在你打开之后被改过了(另一位管理者保存过,或者服务器上次运行时改写了它)。为了不覆盖那次修改,这次保存没有写入。", "file_changed": "文件在打开后已被其他管理者或服务器进程修改。本次保存未写入,以避免覆盖现有更改。",
"volume_full": "服务器的存储卷已满,这次改动没有写入,原有文件保持不变。删掉用不着的文件,或者请管理员给它扩容。", "volume_full": "服务器存储卷已满,本次更改未写入,原有文件保持不变。请清理文件或联系管理员扩容。",
"backup_cooldown": "这台服务器刚备份过,手动备份之间有冷却时间——请过几分钟再试。", "backup_cooldown": "手动备份冷却时间尚未结束,请稍后重试。",
"backup_store_full": "备份存储已满,暂时无法手动备份——请联系管理员清理空间。", "backup_store_full": "备份存储已满,暂时无法手动备份——请联系管理员清理空间。",
"restore_unavailable": "回档功能当前不可用,请稍后再试。", "restore_unavailable": "回档功能当前不可用,请稍后再试。",
"export_busy": "正在准备的导出太多了:每人同时一个,整个平台同时最多两个,每人每小时最多六个。请过几分钟再试。", "export_busy": "导出请求已达限制:每账户最多同时 1 个,平台最多同时 2 个,每账户每小时最多 6 个。请等待现有任务结束或限流解除后重试。",
"export_expired": "这个下载已过期或已经用过了——请重新导出。", "export_expired": "下载链接已过期或已被使用,请重新导出。",
"export_not_ready": "下载还没准备好——请稍等片刻再试。", "export_not_ready": "下载准备未完成,请稍后重试。",
"export_unavailable": "当前部署没有开通世界和备份下载——请联系管理员。", "export_unavailable": "当前部署未配置世界及备份下载功能,请联系管理员。",
"session_expired": "会话已过期——请重新登录。", "session_expired": "会话已过期——请重新登录。",
"forbidden": "你无权执行此操作。", "forbidden": "当前账户无权执行此操作。",
"self_protected": "不能对当前登录的账号执行此操作。", "self_protected": "不能对当前登录的账号执行此操作。",
"owner_protected": "所有者账号不能在面板里降级、禁用或删除,只能在主机的应急控制台(sudo felis breakGlass)上管理。", "owner_protected": "面板不允许降级、禁用或删除 Owner 账户。请使用主机应急控制台(sudo felis breakGlass)管理。",
"session_not_found": "这个会话已经结束了。", "session_not_found": "会话已结束或不存在。",
"generic": "出了点问题,请稍后重试。", "generic": "操作失败,请稍后重试。",
"otp_resend_cooldown": "验证码发送频繁,请稍后再试。", "otp_resend_cooldown": "验证码发送频繁,请稍后再试。",
"otp_locked": "验证码错误次数过多,请重新获取验证码。", "otp_locked": "验证码错误次数过多,请重新获取验证码。",
"otp_account_locked": "24 小时内验证码错误次数已达上限,此账号的邮箱验证码暂时停用,请稍后再试或改用 Passkey。", "otp_account_locked": "24 小时内验证码错误次数已达上限,此账号的邮箱验证码暂时停用,请稍后再试或改用 Passkey。",
"rate_limited": "登录请求过于频繁,请稍等一分钟再试。", "rate_limited": "登录请求已触发限流,请等待限流解除后重试。",
"mail_rate_limited": "服务器当前发信过多,验证码暂时发不出去,请稍后再试或改用 Passkey。", "mail_rate_limited": "邮件发送已触发限流,验证码暂时无法发送。请稍后重试或使用 Passkey。",
"passkey_challenge_invalid": "验证挑战无效或已过期,请重试。", "passkey_challenge_invalid": "验证挑战无效或已过期,请重试。",
"invalid_attestation": "无法验证此 Passkey,请重试。", "invalid_attestation": "无法验证此 Passkey,请重试。",
"passkey_already_bound": "此 Passkey 已被其他账户绑定。", "passkey_already_bound": "此 Passkey 已被其他账户绑定。",
"passkey_unavailable": "Passkey 功能当前不可用。", "passkey_unavailable": "Passkey 功能当前不可用。",
"last_passkey": "这是你唯一的 Passkey,邮箱也还没验证,不能删除。先验证邮箱或再注册一个 Passkey。", "last_passkey": "当前账户仅有一个 Passkey,且邮箱未验证,不允许删除。请先验证邮箱或注册其他 Passkey。",
"passkey_not_allowed": "操作已被用户或浏览器取消。", "passkey_not_allowed": "操作已被用户或浏览器取消。",
"passkey_aborted": "Passkey 注册已被取消。", "passkey_aborted": "Passkey 注册已被取消。",
"bad_name": "服务器名称不合法:需为 3–32 位小写字母、数字或连字符,且不能使用保留名。", "bad_name": "服务器名称不合法:需为 3–32 位小写字母、数字或连字符,且不能使用保留名。",
"bad_subdomain": "子域名不合法:需为 3–32 位小写字母、数字或连字符,且不能使用保留名。", "bad_subdomain": "子域名不合法:需为 3–32 位小写字母、数字或连字符,且不能使用保留名。",
"bad_display_name": "显示名称最多 64 个字符,只能包含可见字符和空格,不能含换行、制表符或不可见的控制字符。", "bad_display_name": "显示名称最多 64 个字符,只能包含可见字符和空格,不能含换行、制表符或不可见的控制字符。",
"at_capacity": "集群的在线服务器已达上限——等一台服务器停止后再试。", "at_capacity": "集群运行服务器数量已达上限。请等待其他服务器停止后重试。",
"storage_immutable": "存储容量不支持在线调整。", "storage_immutable": "存储容量不支持在线调整。",
"bad_idle_stop": "空闲停服时长须在 1 分钟到 24 小时之间,或选择“从不”。", "bad_idle_stop": "空闲停服时长须在 1 分钟到 24 小时之间,或选择“从不”。",
"email_taken": "该邮箱已在其他账户上完成验证;请直接用该邮箱登录,或换一个地址。", "email_taken": "该邮箱已在其他账户上完成验证;请直接用该邮箱登录,或换一个地址。",
"mail_undeliverable": "验证码邮件发送失败——请稍后重试,或联系管理员检查邮件服务。", "mail_undeliverable": "验证码邮件发送失败——请稍后重试,或联系管理员检查邮件服务。",
"mail_unavailable": "服务器没有配置邮件服务,发不出验证码。请改用 Passkey 登录,或请管理员配置邮件。", "mail_unavailable": "邮件服务未配置,无法发送验证码。请使用 Passkey 登录,或联系管理员配置邮件服务。",
"bad_path": "路径不合法——请使用世界目录内的相对路径。", "bad_path": "路径不合法——请使用世界目录内的相对路径。",
"too_large": "内容超出大小限制,无法处理。", "too_large": "内容超出大小限制,无法处理。",
"files_timeout": "文件操作超时——请稍后重试。", "files_timeout": "文件操作超时——请稍后重试。",
"files_unavailable": "文件功能当前不可用。", "files_unavailable": "文件功能当前不可用。",
"file_exists": "这里已经有同名的文件或文件夹。换个名字,或者先把原来那个改名或删除。", "file_exists": "当前目录已存在同名文件或文件夹。请使用其他名称,或先重命名、删除现有条目。",
"upload_staging_full": "面板的上传暂存空间快满了,这个文件没有转存过去。稍后再试,或者请管理员清理上传卷。", "upload_staging_full": "上传暂存空间不足,文件未完成转存。请稍后重试,或联系管理员清理上传卷。",
"upload_incomplete": "文件还没传完上传就中断了,什么都没有改动。请重试。", "upload_incomplete": "上传在文件传输完成前中断,本次操作未修改文件。请重新上传。",
"length_required": "这次上传没有声明文件大小,被拒绝了。请从面板重新上传。", "length_required": "上传请求未声明文件大小,已被拒绝。请通过面板重新上传。",
"digest_mismatch": "文件在传输途中被改动了,服务器已拒收,什么都没有写入。请重试。", "digest_mismatch": "文件传输校验失败,服务器已拒绝接收,文件未写入。请重新上传。",
"digest_required": "这次请求没有附带校验值,被拒绝了。请刷新面板后再试。", "digest_required": "请求缺少校验值,已被拒绝。请刷新面板后重试。",
"bad_digest": "这次请求附带的校验值格式不对,被拒绝了。请刷新面板后再试。", "bad_digest": "请求校验值格式无效,已被拒绝。请刷新面板后重试。",
"file_unreadable": "读不出这个文件:它在选中之后被改动、移走或删除了。请重新选择文件再上传。", "file_unreadable": "无法读取所选文件,文件可能已被修改、移动或删除。请重新选择后上传。",
"read_damaged": "文件传过来时损坏了,所以没有打开:保存它会把损坏写回去。请重新打开。", "read_damaged": "文件传输校验失败,未打开文件,以避免后续保存损坏数据。请重新读取。",
"upload_not_found": "这次分片上传已经不在了(取消过、已经写入、闲置超过 6 小时,或者面板服务重启过)。请重新上传。", "upload_not_found": "分片上传记录不存在,可能已取消、已完成写入、闲置超过 6 小时或因服务重启失效。请重新上传。",
"too_many_uploads": "你同时进行的大文件上传已经有 4 个了。等其中一个完成,或者取消一个再试。", "too_many_uploads": "当前账户已达 4 个并发大文件上传上限。请等待任务完成,或取消现有上传后重试。",
"op_lost": "看不到这次操作的进度了。刷新列表看看文件有没有写入。", "op_lost": "无法读取操作进度。请刷新文件列表以确认写入结果。",
"jobs_unavailable": "后台任务服务当前不可用。", "jobs_unavailable": "后台任务服务当前不可用。",
"already_terminal": "该构建已经结束,无法重复取消。", "already_terminal": "该构建已经结束,无法重复取消。",
"build_unavailable": "构建功能当前不可用。", "build_unavailable": "构建功能当前不可用。",
"build_logs_unavailable": "构建日志暂时不可用。", "build_logs_unavailable": "构建日志暂时不可用。",
"scan_document_not_kept": "这次构建没有留下这份扫描文件:文件太大没有保存,或者生成它的步骤失败了。", "scan_document_not_kept": "构建未留存此扫描文件,可能由于文件过大或生成步骤失败。",
"already_reviewed": "该提交已经审核过了。", "already_reviewed": "该提交已经审核过了。",
"context_changed": "提交者在你审阅之后重新上传了构建上下文。请重新下载并审阅新的上传再通过。", "context_changed": "构建上下文在审核期间已被重新上传。请重新下载并审核最新内容后再批准。",
"submission_quota_exceeded": "你的提交配额已满:待审核提交过多,或待审核和被拒绝的上传占满了存储额度。可以撤回一个待审核的提交;被拒绝的上传在审核 7 天后释放空间,已批准的不占额度。", "submission_quota_exceeded": "提交配额已满,待审核数量或上传存储已达上限。可撤回待审核提交;被拒绝的上传在审核 7 天后释放空间,已批准的提交不计入配额。",
"submission_cooldown": "操作太频繁——请稍后再试。", "submission_cooldown": "操作太频繁——请稍后再试。",
"submissions_unavailable": "提交流程当前不可用。", "submissions_unavailable": "提交流程当前不可用。",
"uploads_unavailable": "上传功能当前不可用。", "uploads_unavailable": "上传功能当前不可用。",
"upload_busy": "这次上传的另一部分仍在发送——请稍等片刻再试。", "upload_busy": "这次上传的另一部分仍在发送——请稍等片刻再试。",
"upload_offset_mismatch": "上传进度与服务器对不上——重试即可从中断处接着传。", "upload_offset_mismatch": "上传偏移量与服务器记录不一致。请重试,系统将从已确认的位置继续上传。",
"uploads_store_unavailable": "上传存储暂时没有响应——稍后重试即可,已传的部分会保留。", "uploads_store_unavailable": "上传存储未响应,已接收的内容保留。请稍后重试。",
"part_too_large": "上传的某一片超过了服务器接受的大小。", "part_too_large": "上传的某一片超过了服务器接受的大小。",
"backup_unavailable": "备份功能当前不可用。", "backup_unavailable": "备份功能当前不可用。",
"account_retired": "该账户已退役——请使用迁移后的账户登录。", "account_retired": "该账户已退役——请使用迁移后的账户登录。",
"no_migration": "当前没有进行中的迁移。", "no_migration": "当前没有进行中的迁移。",
"not_confirmed": "请先在当前浏览器完成身份确认,确认 10 分钟内有效。", "not_confirmed": "请先在当前浏览器完成身份确认,确认 10 分钟内有效。",
"migrate_quota_exceeded": "这次迁移带来的服务器超出了你的配额。请管理员调高配额后,在迁移码过期前用同一个码再兑换一次。", "migrate_quota_exceeded": "迁移服务器数量超出当前账户配额。请联系管理员调整配额,并在迁移码过期前重新兑换同一迁移码。",
"invalid_quota": "配额要填 0 到 2147483647 之间的整数,不限就留空。", "invalid_quota": "配额须为 0–2147483647 的整数。留空表示不限制。",
"already_confirmed": "该迁移已经确认过了。", "already_confirmed": "该迁移已经确认过了。",
"invalid_target": "迁移目标账户不能与来源账户相同。", "invalid_target": "迁移目标账户不能与来源账户相同。",
"target_not_found": "找不到迁移目标账户。", "target_not_found": "迁移目标账户不存在。",
"target_unavailable": "迁移目标账户当前不可用。", "target_unavailable": "迁移目标账户当前不可用。",
"no_passkey": "该账户尚未绑定 Passkey。", "no_passkey": "该账户尚未绑定 Passkey。",
"passkey_required": "此操作需要先绑定 Passkey。", "passkey_required": "此操作需要先绑定 Passkey。",
@@ -110,38 +110,38 @@
"passkey_login_failed": "Passkey 验证失败——请重试。", "passkey_login_failed": "Passkey 验证失败——请重试。",
"passkey_login_invalid": "Passkey 登录请求无效或已过期——请重新发起。", "passkey_login_invalid": "Passkey 登录请求无效或已过期——请重新发起。",
"too_many_challenges": "验证请求过于频繁——请稍后再试。", "too_many_challenges": "验证请求过于频繁——请稍后再试。",
"reauth_required": "请先确认是你本人:这项更改需要你在几分钟内用 Passkey 或邮箱验证码验证过。", "reauth_required": "此操作需要近期身份验证。请使用 Passkey 或邮箱验证码重新验证身份。",
"staff_reauth": "管理员账户须用 Passkey 或重新登录来确认身份。", "staff_reauth": "管理员账户须用 Passkey 或重新登录来确认身份。",
"no_session": "只能在已登录 Felis 的浏览器中进行此操作。", "no_session": "只能在已登录 Felis 的浏览器中进行此操作。",
"op_login_invalid": "本次管理员登录未能完成——请重新发起登录。", "op_login_invalid": "本次管理员登录未能完成——请重新发起登录。",
"op_login_not_found": "找不到该管理员登录请求。", "op_login_not_found": "管理员登录请求不存在。",
"op_login_mismatch": "该管理员登录请求属于另一个账户。", "op_login_mismatch": "该管理员登录请求属于另一个账户。",
"too_many_streams": "同时打开的实时连接过多——请关闭一些页面后再试。", "too_many_streams": "同时打开的实时连接过多——请关闭一些页面后再试。",
"protected_admin": "该名字属于已绑定的管理员账户,不能被认领。", "protected_admin": "该名字属于已绑定的管理员账户,不能被认领。",
"auth_unavailable": "登录服务当前不可用。", "auth_unavailable": "登录服务当前不可用。",
"setup_token_invalid": "初始化令牌无效或已过期。", "setup_token_invalid": "初始化令牌无效或已过期。",
"network_error": "连不上 Felis:网络断开了,或者 Cloudflare Access 的登录已过期。刷新页面可以重新登录。", "network_error": "无法连接 Felis,可能由于网络中断或 Cloudflare Access 会话过期。请检查网络;Access 会话过期时,请刷新页面并重新登录。",
"upstream_unavailable": "服务暂时不可用,请稍后重试。", "upstream_unavailable": "服务暂时不可用,请稍后重试。",
"internal_error": "操作失败,服务器出现内部错误。请联系管理员查看日志。", "internal_error": "操作失败,服务器出现内部错误。请联系管理员查看日志。",
"request_id": "请求编号:{{id}}", "request_id": "请求编号:{{id}}",
"bad_path_param": "链接里的名称无效,请从列表重新打开。", "bad_path_param": "链接中的名称无效,请从列表重新打开目标页面。",
"payload_too_large": "内容超过了入口允许的大小,没有发送成功。", "payload_too_large": "请求内容超出入口大小限制,发送失败。",
"passkey_no_credential": "浏览器没有返回 Passkey,请重试。", "passkey_no_credential": "浏览器没有返回 Passkey,请重试。",
"not_found": "要找的内容已不存在,可能已被删除,请刷新后重试。", "not_found": "请求的内容不存在或已被删除,请刷新后重试。",
"conflict": "期间状态已发生变化,请刷新后重试。", "conflict": "操作期间状态已变更,请刷新后重试。",
"bad_request": "请求未被接受:{{detail}}", "bad_request": "请求未被接受:{{detail}}",
"restore_in_progress": "这台服务器的世界正在恢复另一份备份,请等它完成后再试。", "restore_in_progress": "世界正在恢复其他备份。请等待当前恢复完成后重试。",
"uploads_full": "上传存储已满,需要管理员删除已审核的提交后才能继续上传。", "uploads_full": "上传存储已满,需要管理员删除已审核的提交后才能继续上传。",
"unsupported_media_type": "请求格式不被服务器接受,请刷新页面后重试。", "unsupported_media_type": "请求格式不被服务器接受,请刷新页面后重试。",
"schedules_unavailable": "计划任务当前不可用。", "schedules_unavailable": "计划任务当前不可用。",
"schedule_limit": "这台服务器的计划任务数量已达上限,请先删除一个。", "schedule_limit": "服务器计划任务数量已达上限。请删除现有任务后重试。",
"schedule_running": "这个任务正在执行,请等本次执行结束后再试。", "schedule_running": "任务正在执行,请等待本次执行结束后重试。",
"schedule_stale": "保存这个任务后服务器换了所有者,请先重新保存任务再执行。", "schedule_stale": "任务保存后,服务器所有者已变更。请重新保存任务后再执行。",
"bad_schedule": "计划任务未保存:{{detail}}", "bad_schedule": "计划任务未保存:{{detail}}",
"auth_source_unknown": "请选择已配置的认证源。", "auth_source_unknown": "请选择已配置的认证源。",
"auth_source_unavailable": "认证源暂时不可用或返回了无效角色,请稍后重试。", "auth_source_unavailable": "认证源暂时不可用或返回了无效角色,请稍后重试。",
"auth_source_lookup_unsupported": "此认证源尚未配置角色查询地址。", "auth_source_lookup_unsupported": "此认证源尚未配置角色查询地址。",
"minecraft_profile_not_found": "在所选认证源中没有找到这个角色,请检查角色名或 UUID。", "minecraft_profile_not_found": "所选认证源中不存在此角色。请检查角色名或 UUID。",
"auth_sources_unavailable": "认证源管理暂不可用。", "auth_sources_unavailable": "认证源管理暂不可用。",
"auth_sources_changed": "认证源已被其他操作修改。请放弃当前更改并重新读取后再保存。", "auth_sources_changed": "认证源已被其他操作修改。请放弃当前更改并重新读取后再保存。",
"auth_source_tag_locked": "已保存的认证源标识不能改名或移除,请使用停用。" "auth_source_tag_locked": "已保存的认证源标识不能改名或移除,请使用停用。"
+54 -54
View File
@@ -10,24 +10,24 @@
"col_size": "大小", "col_size": "大小",
"col_modified": "修改时间", "col_modified": "修改时间",
"empty_dir_title": "此目录为空", "empty_dir_title": "此目录为空",
"empty_dir_hint": "把文件拖到这里上传,或者用上方的按钮新建文件、文件夹。", "empty_dir_hint": "可拖拽文件至列表上传,或使用工具栏创建文件和文件夹。",
"list_truncated": "列表已按服务器安全上限截断。", "list_truncated": "列表已按服务器安全上限截断。",
"stopped_required_title": "需要先停服", "stopped_required_title": "服务器必须停止",
"stopped_required_body": "只有在服务器完全停止后才能浏览或编辑文件——世界卷被运行中的服务器独占挂载。请先停止服务器。", "stopped_required_body": "运行中的服务器独占挂载世界卷。浏览或编辑文件前,必须完全停止服务器。",
"not_yours_title": "这不是你的服务器", "not_yours_title": "无权访问服务器文件",
"not_yours_body": "只有服务器所有者或管理员才能编辑其文件。", "not_yours_body": "只有服务器所有者或管理员才能编辑其文件。",
"binary_hint": "这是二进制文件,文本编辑会损坏它。请下载后使用对应的软件打开,修改后重新上传。", "binary_hint": "二进制文件不支持文本编辑。请下载后使用适配软件处理,再重新上传;文本编辑可能损坏文件。",
"saved": "已保存 {{path}}", "saved": "已保存 {{path}}",
"saving": "保存中…", "saving": "保存中…",
"save": "保存", "save": "保存",
"too_large": "超出编辑器 {{limit}} 上限。", "too_large": "超出编辑器 {{limit}} 上限。",
"conflict_title": "文件已被其他人修改", "conflict_title": "文件内容已变更",
"conflict_body": "你打开这个文件之后,它被改过了(另一位管理者保存过,或者服务器上次运行时改写了它)。你的修改还在编辑框里,没有写入。", "conflict_body": "文件在打开后已被其他管理者或服务器进程修改。本次编辑尚未写入,请选择载入最新内容或覆盖现有文件。",
"conflict_reload": "载入最新内容", "conflict_reload": "载入最新内容",
"conflict_reload_hint": "放弃你的修改,改看现在的文件", "conflict_reload_hint": "放弃当前编辑并载入最新文件内容",
"conflict_overwrite": "仍然覆盖", "conflict_overwrite": "仍然覆盖",
"conflict_overwrite_hint": "用你的版本替换对方的修改", "conflict_overwrite_hint": "使用当前编辑内容覆盖现有文件",
"discard_prompt": "有未保存的修改,要放弃吗?", "discard_prompt": "放弃未保存的修改?",
"discard": "放弃修改", "discard": "放弃修改",
"keep_editing": "继续编辑", "keep_editing": "继续编辑",
"status_refresh_failed": "刷新服务器状态失败,页面显示的可能不是最新状态:{{reason}}", "status_refresh_failed": "刷新服务器状态失败,页面显示的可能不是最新状态:{{reason}}",
@@ -35,7 +35,7 @@
"new_file": "新建文件", "new_file": "新建文件",
"new_folder": "新建文件夹", "new_folder": "新建文件夹",
"upload": "上传", "upload": "上传",
"upload_hint": "上传文件到这个文件夹,也可以直接拖到列表上。文件夹请先压成 .zip 再上传", "upload_hint": "上传文件至当前目录,或拖拽文件至列表。上传文件夹前须将其压缩为 .zip。",
"in_folder": "位置:{{dir}}", "in_folder": "位置:{{dir}}",
"name_label": "名称", "name_label": "名称",
"create": "创建", "create": "创建",
@@ -43,31 +43,31 @@
"rename_title": "重命名 {{name}}", "rename_title": "重命名 {{name}}",
"rename_item": "重命名 {{name}}", "rename_item": "重命名 {{name}}",
"delete_item": "删除 {{name}}", "delete_item": "删除 {{name}}",
"managed_no_rename": "这个由 Felis 管理,名称和位置都要保持不变。", "managed_no_rename": "此条目由 Felis 管理,不允许更改名称或位置。",
"wait_for_uploads": "等上传完成后再操作。", "wait_for_uploads": "上传完成前无法执行此操作。",
"name_required": "请输入名称。", "name_required": "请输入名称。",
"name_slash": "名称里不能有“/”。", "name_slash": "名称不得包含“/”。",
"name_dots": "“.” 和 “..” 不能用作名称。", "name_dots": "“.” 和 “..” 不能用作名称。",
"name_too_long": "名称太长了。", "name_too_long": "名称超出长度限制。",
"name_taken": "这里已经有同名的文件或文件夹。", "name_taken": "当前目录已存在同名文件或文件夹。",
"folder_created": "已新建文件夹 {{path}}", "folder_created": "已新建文件夹 {{path}}",
"renamed": "已把 {{from}} 重命名为 {{to}}", "renamed": "已将 {{from}} 重命名为 {{to}}",
"deleted": "已删除 {{path}}", "deleted": "已删除 {{path}}",
"delete": "删除", "delete": "删除",
"delete_file_title": "删除 {{name}}?", "delete_file_title": "删除 {{name}}?",
"delete_folder_title": "删除文件夹 {{name}}?", "delete_folder_title": "删除文件夹 {{name}}?",
"delete_file_body": "文件会被永久删除。之后可能还要用的话,先做一次备份。", "delete_file_body": "文件将被永久删除。请在删除前备份需要保留的内容。",
"delete_folder_body": "文件夹和里面的所有内容都会被永久删除。之后可能还要用的话,先做一次备份。", "delete_folder_body": "文件夹及其全部内容将被永久删除。请在删除前备份需要保留的内容。",
"secret_config_unreadable": "config/paper-global.yml 里有所有服务器共用的代理转发密钥,编辑器不打开它。", "secret_config_unreadable": "config/paper-global.yml 包含共享代理转发密钥,不允许在线读取或编辑。",
"drop_here": "松开即可上传到 {{dir}}", "drop_here": "松开即可上传到 {{dir}}",
"upload_no_folders": "不支持上传文件夹(已跳过 {{count}} 个):散文件传到一半断掉会留下残缺存档。请先压成 .zip 上传,再对它点「解压到此处」。", "upload_no_folders": "不支持直接上传文件夹,已跳过 {{count}} 个。为避免上传中断导致存档不完整,请先压缩为 .zip,上传后执行「解压到此处」。",
"uploads_label": "上传", "uploads_label": "上传",
"uploads_title": "上传 · 已完成 {{done}}/{{total}}", "uploads_title": "上传 · 已完成 {{done}}/{{total}}",
"upload_queued": "等待中", "upload_queued": "等待中",
"upload_sending": "{{sent}} / {{total}} · {{percent}}%", "upload_sending": "{{sent}} / {{total}} · {{percent}}%",
"upload_landing": "正在写入服务器…", "upload_landing": "正在写入服务器…",
"upload_done": "已上传", "upload_done": "已上传",
"upload_exists": "这里已经有同名文件。", "upload_exists": "当前目录已存在同名文件。",
"upload_replace": "替换", "upload_replace": "替换",
"upload_skip": "跳过", "upload_skip": "跳过",
"upload_replace_all": "全部替换", "upload_replace_all": "全部替换",
@@ -77,32 +77,32 @@
"upload_dismiss": "移除 {{name}}", "upload_dismiss": "移除 {{name}}",
"upload_clear_done": "清除已完成", "upload_clear_done": "清除已完成",
"upload_progress_label": "正在上传 {{name}}", "upload_progress_label": "正在上传 {{name}}",
"upload_folder_there": "这里已经有同名文件夹。", "upload_folder_there": "当前目录已存在同名文件夹。",
"upload_name_too_long": "这个文件名超过 255 字节,服务器上的文件名存不下。先改短再上传。", "upload_name_too_long": "文件名超过 255 字节。请缩短文件名后重新上传。",
"upload_no_room": "世界卷只剩 {{free}},放不下这个 {{size}} 的文件。先删掉些用不着的文件再重试。", "upload_no_room": "世界卷可用空间为 {{free}},不足以写入大小为 {{size}} 的文件。请释放存储空间后重试。",
"upload_landing_progress": "正在写入服务器… {{percent}}%", "upload_landing_progress": "正在写入服务器… {{percent}}%",
"wait_for_op": "等后台操作完成后再改动。", "wait_for_op": "后台操作完成前无法修改文件。",
"wait_for_download": "等下载准备好后再操作。", "wait_for_download": "下载准备完成前无法执行此操作。",
"wait_for_backup": "正在备份这台服务器的世界,备份结束后才能改动文件。", "wait_for_backup": "世界备份进行中,完成后方可修改文件。",
"wait_for_restore": "正在回档这台服务器的世界,回档结束后才能改动文件。", "wait_for_restore": "世界恢复进行中,完成后方可修改文件。",
"wait_for_world_export": "正在导出这台服务器的世界,那边的下载结束后才能改动文件。", "wait_for_world_export": "世界导出进行中,下载结束后方可修改文件。",
"wait_for_file_download": "有一个下载正在读取这台服务器的世界,传完后才能改动文件。", "wait_for_file_download": "文件下载正在读取世界数据,传输完成后方可修改文件。",
"unzip_item": "把 {{name}} 解压到此处", "unzip_item": "将 {{name}} 解压至当前目录",
"unzip_conflicts_title": "解压 {{name}} 会覆盖 {{count}} 个文件", "unzip_conflicts_title": "解压 {{name}} 会覆盖 {{count}} 个文件",
"unzip_conflicts_body": "压缩包里的这些文件在这里已经存在。确认后会用压缩包里的版本替换它们,其余文件照常解压。旧版本之后可能还要用的话,先做一次备份。", "unzip_conflicts_body": "压缩包中的部分文件与当前目录内容冲突。确认后将覆盖冲突文件并解压其余内容。请在操作前备份需要保留的原始文件。",
"unzip_conflicts_more": "还有 {{count}} 个没有列出。", "unzip_conflicts_more": "另有 {{count}} 个条目未列出。",
"unzip_overwrite": "覆盖并解压", "unzip_overwrite": "覆盖并解压",
"download_item": "下载 {{name}}", "download_item": "下载 {{name}}",
"download_folder_item": "把文件夹 {{name}} 打包成 .zip 下载", "download_folder_item": "将文件夹 {{name}} 打包为 .zip 下载",
"download_preparing": "正在准备 {{name}} 的下载…", "download_preparing": "正在准备 {{name}} 的下载…",
"download_started": "已开始下载 {{filename}}。", "download_started": "已开始下载 {{filename}}。",
"download_started_props": "已开始下载 {{filename}}。里面的 rcon.password 已隐去。", "download_started_props": "已开始下载 {{filename}}。下载内容中的 rcon.password 已隐去。",
"download_started_config": "已开始下载 {{filename}}。所有服务器共用的代理转发密钥 paper-global.yml 不在里面。", "download_started_config": "已开始下载 {{filename}}。包含共享代理转发密钥的 paper-global.yml 已排除。",
"download_failed": "下载没有准备好。", "download_failed": "下载准备失败。",
"download_failed_because": "下载没有准备好:{{reason}}", "download_failed_because": "下载准备失败:{{reason}}",
"download_out_of_memory": "这个文件夹里的文件太多,打包任务的内存装不下它的文件清单,被系统停掉了,什么都没下载。请分成几个小一些的文件夹分别下载。", "download_out_of_memory": "打包任务因文件清单超出可用内存而终止,下载未开始。请拆分为较小的文件夹后分别下载。",
"download_busy": "正在准备的文件下载太多了:每人同时最多两个,整个平台同时最多四个,每人每小时最多三十个。请过几分钟再试。", "download_busy": "文件下载请求已达限制:每账户最多同时 2 个,平台最多同时 4 个,每账户每小时最多 30 个。请等待现有任务结束或限流解除后重试。",
"secret_config_no_download": "config/paper-global.yml 里有所有服务器共用的代理转发密钥,不能下载。", "secret_config_no_download": "config/paper-global.yml 包含共享代理转发密钥,不允许下载。",
"ops_label": "后台操作", "ops_label": "后台操作",
"ops_refresh_failed": "读取后台操作失败:{{reason}}", "ops_refresh_failed": "读取后台操作失败:{{reason}}",
"op_unzip": "解压 {{path}}", "op_unzip": "解压 {{path}}",
@@ -114,18 +114,18 @@
"op_upload_done": "已写入。", "op_upload_done": "已写入。",
"op_dismiss": "关闭 {{path}} 的结果", "op_dismiss": "关闭 {{path}} 的结果",
"op_conflicts": "查看冲突", "op_conflicts": "查看冲突",
"op_upload_exists": "这里已经有同名文件,没有替换。重新上传时选「替换」即可。", "op_upload_exists": "同名文件已存在,本次上传未替换原文件。重新上传时可选择「替换」。",
"op_unzip_conflicts": "会覆盖 {{count}} 个已有文件,还没有解压。", "op_unzip_conflicts": "解压将覆盖 {{count}} 个现有文件,操作尚未执行。",
"op_volume_full": "世界卷空间不够:需要 {{need}},只剩 {{avail}}。什么都没有改动。", "op_volume_full": "世界卷空间不足:需要 {{need}},可用 {{avail}}。本次操作未修改文件。",
"archive_invalid": "这个压缩包损坏了,或者不是 zip 文件。什么都没有改动。", "archive_invalid": "压缩包损坏或格式不是 ZIP。本次操作未修改文件。",
"archive_invalid_entry": "压缩包里的 {{entry}} 损坏了(大小或校验和对不上)。什么都没有改动。", "archive_invalid_entry": "压缩包条目 {{entry}} 的大小或校验和不匹配。本次操作未修改文件。",
"archive_unsafe": "压缩包里的 {{entry}} 会写到这个文件夹外面,或者是设备文件。整个压缩包都没有解压。", "archive_unsafe": "压缩包条目 {{entry}} 包含越界路径或设备文件,整个压缩包未解压。",
"archive_symlink": "压缩包里的 {{entry}} 是符号链接。整个压缩包都没有解压。", "archive_symlink": "压缩包条目 {{entry}} 为符号链接,整个压缩包未解压。",
"type_conflict": "压缩包里的 {{entry}} 和这里已有的同名项一个是文件、一个是文件夹,覆盖解决不了。先把这里的 {{entry}} 改名或删掉再解压。", "type_conflict": "条目 {{entry}} 与当前目录中的同名项存在文件与文件夹类型冲突,无法直接覆盖。请先重命名或删除现有的 {{entry}},再执行解压。",
"job_failed": "后台任务没说明原因就结束了。刷新列表确认一下,再试一次。", "job_failed": "后台任务已结束,未提供失败原因。请刷新文件列表确认操作结果后重试。",
"job_timed_out": "后台任务 2 小时还没做完,被停下了。刷新列表确认一下,再试一次。", "job_timed_out": "后台任务超过 2 小时执行上限,已终止。请刷新文件列表确认操作结果后重试。",
"job_out_of_memory": "后台任务用完了内存,被系统停掉了。服务器上的文件没有被改动,再试一次。", "job_out_of_memory": "后台任务因内存不足而终止,服务器文件未修改。请检查任务资源后重试。",
"job_out_of_memory_unzip": "压缩包里的文件太多,解压任务的内存装不下它的文件清单,被系统停掉了。服务器上的文件没有被改动。把它拆成几个小一些的 zip,分别上传解压。", "job_out_of_memory_unzip": "解压任务因文件清单超出可用内存而终止,服务器文件未修改。请将内容拆分为较小的 ZIP 文件,分别上传并解压。",
"binary_title": "此文件不能作为文本编辑", "binary_title": "此文件不能作为文本编辑",
"preview_large_title": "文件超出在线预览上限", "preview_large_title": "文件超出在线预览上限",
"preview_large_hint": "在线编辑器最多读取 {{limit}}。请下载后编辑,再重新上传。", "preview_large_hint": "在线编辑器最多读取 {{limit}}。请下载后编辑,再重新上传。",
+6 -6
View File
@@ -4,8 +4,8 @@
"space_label": "选择要自定义的空间", "space_label": "选择要自定义的空间",
"login": "登录空间", "login": "登录空间",
"lobby": "正式大厅", "lobby": "正式大厅",
"login_description": "玩家先在这里验证身份,再进入正式大厅。登录场景使用 Limbo schematic,不能直接使用 Paper 世界地图。", "login_description": "玩家在登录空间完成身份验证后进入正式大厅。登录场景使用 Limbo schematic,不支持直接使用 Paper 世界地图。",
"lobby_description": "玩家在这里选择服务器。可以更换地图、安装插件、自定义大厅体验并授权建造。", "lobby_description": "正式大厅提供服务器选择,可配置地图、插件、场景体验及建造权限。",
"connection_version": "安装时的游戏版本:Java {{version}}", "connection_version": "安装时的游戏版本:Java {{version}}",
"unknown_version": "未记录", "unknown_version": "未记录",
"version_hint": "客户端版本兼容由代理插件决定。更换版本需配套更新登录空间、大厅镜像和代理;此处显示安装版本,实际版本以镜像为准。", "version_hint": "客户端版本兼容由代理插件决定。更换版本需配套更新登录空间、大厅镜像和代理;此处显示安装版本,实际版本以镜像为准。",
@@ -18,7 +18,7 @@
"restarting": "正在重启…", "restarting": "正在重启…",
"restart_requested": "已发送重启请求,可在上方查看启动状态。", "restart_requested": "已发送重启请求,可在上方查看启动状态。",
"restart_title": "重启该空间?", "restart_title": "重启该空间?",
"restart_hint": "重启会暂时断开这里的玩家,重新启动后应用已保存的设置。", "restart_hint": "重启将断开此空间内的玩家连接。已保存配置在启动后生效。",
"restart_required": "设置已保存,等待重启生效", "restart_required": "设置已保存,等待重启生效",
"save": "保存设置", "save": "保存设置",
"saving": "正在保存…", "saving": "正在保存…",
@@ -57,7 +57,7 @@
"bookHelp": "登录说明", "bookHelp": "登录说明",
"openBook": "自动打开登录书", "openBook": "自动打开登录书",
"loginTimeoutSeconds": "登录等待时间(30–3600 秒)", "loginTimeoutSeconds": "登录等待时间(30–3600 秒)",
"login_settings_hint": "绑定码和登录链接始终由系统生成,聊天中的提示仍保留。部署中若显式设置登录超时环境变量,它优先于这里的等待时间。", "login_settings_hint": "绑定码及登录链接由系统生成,聊天提示保持启用。部署中显式设置的登录超时环境变量优先于此处配置。",
"lobby_settings_hint": "欢迎提示会附带可点击的服务器菜单。出生点使用地图保存的位置,可在大厅控制台用 setworldspawn x y z 设置。保护开关影响整个大厅;建造权限玩家保留自己的游戏模式。", "lobby_settings_hint": "欢迎提示会附带可点击的服务器菜单。出生点使用地图保存的位置,可在大厅控制台用 setworldspawn x y z 设置。保护开关影响整个大厅;建造权限玩家保留自己的游戏模式。",
"ADVENTURE": "冒险", "ADVENTURE": "冒险",
"SURVIVAL": "生存", "SURVIVAL": "生存",
@@ -80,13 +80,13 @@
"backups_title": "地图备份与恢复", "backups_title": "地图备份与恢复",
"backups_hint": "更换地图、插件或镜像前保存备份;可选择已有备份恢复。", "backups_hint": "更换地图、插件或镜像前保存备份;可选择已有备份恢复。",
"builder_title": "大厅建造权限", "builder_title": "大厅建造权限",
"builder_hint": "授予 felis.lobby.build 后,该玩家可在大厅建造,不必开放所有人的建造权限。", "builder_hint": "授予 felis.lobby.build 后,指定玩家可在大厅建造,无需向所有玩家开放建造权限。",
"builder_player": "Minecraft 角色名", "builder_player": "Minecraft 角色名",
"builder_grant": "允许建造", "builder_grant": "允许建造",
"builder_revoke": "禁止建造", "builder_revoke": "禁止建造",
"builder_running": "请先启动大厅,才能修改建造权限。", "builder_running": "请先启动大厅,才能修改建造权限。",
"discard_title": "放弃未保存的设置?", "discard_title": "放弃未保存的设置?",
"discard_hint": "切换空间会丢失本次编辑。可以取消后先保存。", "discard_hint": "切换空间将放弃未保存的修改。需要保留时,请取消切换并保存配置。",
"discard": "放弃并切换", "discard": "放弃并切换",
"appearance_description": "设置玩家加入时看到的欢迎信息与菜单。", "appearance_description": "设置玩家加入时看到的欢迎信息与菜单。",
"players_description": "设置加入行为、游戏模式与玩家保护。", "players_description": "设置加入行为、游戏模式与玩家保护。",
+2 -2
View File
@@ -16,7 +16,7 @@
"fleet_sort": "排序方式", "fleet_sort": "排序方式",
"fleet_sort_name": "按名称排序", "fleet_sort_name": "按名称排序",
"fleet_sort_status": "按状态排序", "fleet_sort_status": "按状态排序",
"fleet_sort_players": "在线人数多的在前", "fleet_sort_players": "按在线人数降序",
"fleet_count": "共 {{count}} 台", "fleet_count": "共 {{count}} 台",
"fleet_count_filtered": "{{shown}} / {{total}} 台", "fleet_count_filtered": "{{shown}} / {{total}} 台",
"fleet_col_status": "状态", "fleet_col_status": "状态",
@@ -29,7 +29,7 @@
"fleet_unclaimed": "未认领", "fleet_unclaimed": "未认领",
"fleet_owner_you": "你", "fleet_owner_you": "你",
"fleet_owner_unknown": "未知", "fleet_owner_unknown": "未知",
"fleet_owner_unknown_hint": "暂时查不到所有者,下次刷新时会重新显示。", "fleet_owner_unknown_hint": "服务器所有者读取失败,将在下次刷新时重新查询。",
"fleet_endpoint_idle": "未运行", "fleet_endpoint_idle": "未运行",
"policy_owneronly": "仅所有者", "policy_owneronly": "仅所有者",
"policy_public": "公开", "policy_public": "公开",
+24 -24
View File
@@ -1,28 +1,28 @@
{ {
"title": "计划任务", "title": "计划任务",
"subtitle": "按设定的时间重启、关闭、启动或备份这台服务器,或向它的控制台发送命令。", "subtitle": "按计划执行服务器启动、停止、重启、备份或控制台命令。",
"back_to_console": "返回控制台", "back_to_console": "返回控制台",
"not_yours_title": "无权管理计划任务", "not_yours_title": "无权管理计划任务",
"not_yours_body": "只有所有者或管理员才能查看和修改该服务器的计划任务。", "not_yours_body": "只有所有者或管理员才能查看和修改该服务器的计划任务。",
"new_task": "新建任务", "new_task": "新建任务",
"count": "{{count}} / {{limit}} 个任务", "count": "{{count}} / {{limit}} 个任务",
"limit_reached": "每台服务器最多 {{limit}} 个计划任务,删除一个后才能再添加。", "limit_reached": "计划任务数量已达 {{limit}} 个上限。请删除现有任务后再添加。",
"unavailable_title": "计划任务不可用", "unavailable_title": "计划任务不可用",
"unavailable_body": "当前 felis-api 未配置计划任务存储,这里无法查看或保存任务。请联系管理员检查 felis-api 的配置。", "unavailable_body": "felis-api 未配置计划任务存储,无法读取或保存任务。请联系管理员检查配置。",
"empty_title": "还没有计划任务", "empty_title": "暂无计划任务",
"empty_hint": "可以从常用任务开始,也可以点“新建任务”自己设置。", "empty_hint": "可选择常用任务模板,或执行「新建任务」配置自定义任务。",
"template_restart": "每天 04:00 重启", "template_restart": "每天 04:00 重启",
"template_backup": "每天 05:00 备份", "template_backup": "每天 05:00 备份",
"template_announce": "每 30 分钟在聊天栏发公告", "template_announce": "每 30 分钟在聊天栏发公告",
"template_announce_command": "say 欢迎来到服务器!", "template_announce_command": "say 欢迎来到服务器!",
"action_command": "控制台命令", "action_command": "控制台命令",
"action_command_desc": "向服务器控制台发送一条命令,效果和在控制台里输入一样。仅在服务器运行时执行。", "action_command_desc": "向服务器控制台发送命令,与直接输入控制台命令一致。仅在服务器运行时执行。",
"action_restart": "重启", "action_restart": "重启",
"action_restart_desc": "关闭服务器后再启动,在线玩家会被断开。服务器已关闭时跳过。", "action_restart_desc": "关闭服务器后再启动,在线玩家会被断开。服务器已关闭时跳过。",
"action_stop": "关闭", "action_stop": "关闭",
"action_stop_desc": "关闭服务器,在线玩家会被断开。", "action_stop_desc": "关闭服务器,在线玩家会被断开。",
"action_start": "启动", "action_start": "启动",
"action_start_desc": "服务器关闭时启动它。", "action_start_desc": "仅在服务器停止时执行启动。",
"action_backup": "备份", "action_backup": "备份",
"action_backup_desc": "备份世界。运行中的服务器会先关闭,备份完成后再启动。", "action_backup_desc": "备份世界。运行中的服务器会先关闭,备份完成后再启动。",
"days_all": "每天", "days_all": "每天",
@@ -59,29 +59,29 @@
"opt_hours": "{{count}} 小时", "opt_hours": "{{count}} 小时",
"create_title": "新建计划任务", "create_title": "新建计划任务",
"edit_title": "编辑计划任务", "edit_title": "编辑计划任务",
"dialog_desc": "选择执行什么、什么时候执行。", "dialog_desc": "配置任务类型、执行时间及运行参数。",
"create": "创建", "create": "创建",
"save": "保存", "save": "保存",
"field_action": "执行什么", "field_action": "任务类型",
"field_command": "命令", "field_command": "命令",
"command_placeholder": "say 服务器将在 04:00 重启", "command_placeholder": "say 服务器将在 04:00 重启",
"command_hint": "一条控制台命令,开头的 / 可加可不加。", "command_hint": "填写一条控制台命令,命令开头的 / 可省略。",
"field_when": "什么时候", "field_when": "执行方式",
"mode_daily": "每天一次", "mode_daily": "每天一次",
"mode_interval": "循环执行", "mode_interval": "循环执行",
"field_time": "时间", "field_time": "时间",
"field_every": "间隔", "field_every": "间隔",
"interval_preview": "每天从 00:00 起算:{{times}}", "interval_preview": "每天从 00:00 起算:{{times}}",
"interval_hourly_limit": "重启、关闭、启动和备份最多每小时执行一次。", "interval_hourly_limit": "重启、关闭、启动和备份最多每小时执行一次。",
"field_days": "哪几天", "field_days": "执行日期",
"field_timezone": "时区", "field_timezone": "时区",
"timezone_hint": "按这个时区计算时间。你的浏览器时区是 {{zone}}。", "timezone_hint": "执行时间按所选时区计算。浏览器时区:{{zone}}。",
"timezone_use_browser": "改用浏览器时区", "timezone_use_browser": "改用浏览器时区",
"field_warn": "提醒玩家", "field_warn": "提醒玩家",
"warn_none": "不提醒", "warn_none": "不提醒",
"warn_option": "提前 {{count}} 分钟", "warn_option": "提前 {{count}} 分钟",
"warn_hint": "服务器运行时,在关闭前于游戏聊天栏发一条中英双语提醒。", "warn_hint": "服务器运行时,在关闭前于游戏聊天栏发一条中英双语提醒。",
"backup_note": "备份运行中的服务器时会先关闭它(在线玩家会被断开),备份世界后再启动,通常需要几分钟。已关闭的服务器备份后保持关闭。", "backup_note": "备份运行中的服务器时,将先停止服务器并断开在线玩家,备份完成后重新启动。已停止的服务器在备份后保持停止。",
"field_label": "名称(可选)", "field_label": "名称(可选)",
"label_placeholder": "每晚重启", "label_placeholder": "每晚重启",
"field_enabled": "启用", "field_enabled": "启用",
@@ -94,7 +94,7 @@
"err_timezone": "无法识别的时区,请填写类似 Asia/Shanghai 的名称。", "err_timezone": "无法识别的时区,请填写类似 Asia/Shanghai 的名称。",
"err_label_long": "名称超过 {{max}} 个字符。", "err_label_long": "名称超过 {{max}} 个字符。",
"next_run": "下次执行:{{when}}", "next_run": "下次执行:{{when}}",
"next_run_off": "已停用,只在点“立即执行”时运行", "next_run_off": "已停用,仅在执行「立即执行」时运行",
"never_run": "尚未执行", "never_run": "尚未执行",
"last_run": "上次执行:{{when}}", "last_run": "上次执行:{{when}}",
"run_started_at": "开始于:{{when}}", "run_started_at": "开始于:{{when}}",
@@ -109,20 +109,20 @@
"run_state_starting": "正在启动服务器…", "run_state_starting": "正在启动服务器…",
"warn_badge": "提前 {{count}} 分钟提醒", "warn_badge": "提前 {{count}} 分钟提醒",
"off_badge": "已停用", "off_badge": "已停用",
"tz_differs": "按 {{zone}} 时区计算,你的浏览器时区是 {{browser}}。", "tz_differs": "任务时区:{{zone}}。浏览器时区:{{browser}}。",
"toggle_label": "按计划执行「{{name}}」", "toggle_label": "按计划执行「{{name}}」",
"run_now": "立即执行", "run_now": "立即执行",
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
"busy_hint": "任务正在执行,请等本次执行结束。", "busy_hint": "任务正在执行,请等待本次执行结束。",
"owner_changed": "服务器更换了所有者,这个任务已自动停用。检查无误后保存或重新启用即可恢复。", "owner_changed": "服务器所有者已变更,任务已自动停用。确认配置后,可重新保存或启用任务。",
"detail_not_running": "服务器未在运行。", "detail_not_running": "服务器未在运行。",
"detail_already_stopped": "服务器已经是关闭状态。", "detail_already_stopped": "服务器已经是关闭状态。",
"detail_already_running": "服务器已经在运行。", "detail_already_running": "服务器已经在运行。",
"detail_gone": "服务器已不存在。", "detail_gone": "服务器已不存在。",
"detail_no_world": "服务器还没有世界存档。", "detail_no_world": "服务器尚未创建世界存档。",
"detail_missed": "计划时间点 felis-api 未在运行。", "detail_missed": "计划时间点 felis-api 未在运行。",
"detail_started_meanwhile": "执行结束前有人启动了服务器。", "detail_started_meanwhile": "任务结束前,服务器已被其他操作启动。",
"detail_interrupted": "felis-api 在执行过程中停止了。", "detail_interrupted": "felis-api 在执行过程中停止了。",
"detail_store_full": "备份存储已满,请联系管理员清理空间。", "detail_store_full": "备份存储已满,请联系管理员清理空间。",
"detail_console": "无法连接服务器控制台。", "detail_console": "无法连接服务器控制台。",
@@ -130,15 +130,15 @@
"detail_retiring": "服务器正在被放弃或删除。", "detail_retiring": "服务器正在被放弃或删除。",
"run_title": "立即执行「{{name}}」?", "run_title": "立即执行「{{name}}」?",
"run_confirm": "立即执行", "run_confirm": "立即执行",
"run_body_command": "立即向服务器控制台发送下面这条命令,服务器关闭时跳过:", "run_body_command": "向服务器控制台发送以下命令;服务器停止时将跳过:",
"run_body_restart": "立即关闭服务器,然后重新启动。", "run_body_restart": "立即关闭服务器,然后重新启动。",
"run_body_stop": "立即关闭服务器。", "run_body_stop": "立即关闭服务器。",
"run_body_start": "服务器关闭时立即启动它。", "run_body_start": "服务器停止时立即执行启动。",
"run_body_backup": "立即备份世界。运行中的服务器会先关闭,备份后再启动;已关闭的服务器备份后保持关闭。", "run_body_backup": "立即备份世界。运行中的服务器会先关闭,备份后再启动;已关闭的服务器备份后保持关闭。",
"run_kick": "在线玩家会被立即断开:手动执行不发送提醒。", "run_kick": "在线玩家会被立即断开:手动执行不发送提醒。",
"run_keeps_next": "下次计划执行时间保持不变。", "run_keeps_next": "下次计划执行时间保持不变。",
"delete_title": "删除「{{name}}」?", "delete_title": "删除「{{name}}」?",
"delete_body": "任务会被删除,之后不再执行。", "delete_body": "删除后任务将停止执行,此操作不保留任务配置。",
"delete_confirm": "删除", "delete_confirm": "删除",
"created": "已创建「{{name}}」。", "created": "已创建「{{name}}」。",
"saved": "已保存「{{name}}」。", "saved": "已保存「{{name}}」。",
@@ -149,6 +149,6 @@
"notes_title": "计划任务如何执行", "notes_title": "计划任务如何执行",
"note_timing": "felis-api 每 15 秒检查一次,任务会在设定时间后约 15 秒内开始。晚于计划 10 分钟以上的执行(例如 felis-api 当时未运行)会记为错过,下一次照常执行。", "note_timing": "felis-api 每 15 秒检查一次,任务会在设定时间后约 15 秒内开始。晚于计划 10 分钟以上的执行(例如 felis-api 当时未运行)会记为错过,下一次照常执行。",
"note_state": "命令、重启和关闭仅在服务器运行时执行,启动仅在服务器关闭时执行,其余情况会跳过。", "note_state": "命令、重启和关闭仅在服务器运行时执行,启动仅在服务器关闭时执行,其余情况会跳过。",
"note_owner": "服务器更换所有者后,它的任务会自动停用;重新保存即可交给当前所有者。", "note_owner": "服务器所有者变更后,关联任务将自动停用。重新保存任务后,任务归属将更新为当前所有者。",
"note_run_now": "“立即执行”对已停用的任务同样有效,不发送提醒,也不影响下次计划执行。" "note_run_now": "“立即执行”对已停用的任务同样有效,不发送提醒,也不影响下次计划执行。"
} }
+42 -42
View File
@@ -1,12 +1,12 @@
{ {
"my_servers_title": "我的服务器", "my_servers_title": "我的服务器",
"my_servers_subtitle": "管理你拥有或可用的服务器。", "my_servers_subtitle": "管理当前账户拥有或有权访问的服务器。",
"search_placeholder": "搜索服务器名称或地址…", "search_placeholder": "搜索服务器名称或地址…",
"filter_status_all": "全部状态", "filter_status_all": "全部状态",
"search_no_match": "没有匹配的服务器。", "search_no_match": "没有匹配的服务器。",
"search_clear_btn": "清除筛选", "search_clear_btn": "清除筛选",
"no_servers_linked": "暂无关联的服务器", "no_servers_linked": "暂无关联的服务器",
"no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。", "no_servers_hint": "可认领已授权的服务器,或联系管理员分配。",
"phase_running": "运行中", "phase_running": "运行中",
"phase_starting": "启动中", "phase_starting": "启动中",
"phase_stopping": "停止中", "phase_stopping": "停止中",
@@ -18,7 +18,7 @@
"console": "控制台", "console": "控制台",
"claim": "认领", "claim": "认领",
"claim_server_title": "认领服务器", "claim_server_title": "认领服务器",
"claim_server_desc": "确认认领服务器「{{name}}」吗?认领后你将成为该服务器的所有者,获得控制台、白名单与备份管理权限。", "claim_server_desc": "认领「{{name}}」后,当前账户将成为服务器所有者,并获得控制台、白名单及备份管理权限。",
"claiming": "认领中…", "claiming": "认领中…",
"stop": "停止", "stop": "停止",
"stopping": "停止中…", "stopping": "停止中…",
@@ -66,7 +66,7 @@
"system_service": "系统服务", "system_service": "系统服务",
"system_service_hint": "系统服务由管理员在“登录与大厅”中管理。", "system_service_hint": "系统服务由管理员在“登录与大厅”中管理。",
"players_back_to_console": "返回控制台", "players_back_to_console": "返回控制台",
"players_not_yours_title": "这不是你的服务器", "players_not_yours_title": "无权管理此服务器",
"players_not_yours_body": "只有所有者或管理员才能管理此服务器的玩家。", "players_not_yours_body": "只有所有者或管理员才能管理此服务器的玩家。",
"players_not_running_title": "服务器已休眠", "players_not_running_title": "服务器已休眠",
"players_not_running_body": "玩家管理需要与服务器保持实时连接。启动后即可管理白名单、在线玩家与封禁。", "players_not_running_body": "玩家管理需要与服务器保持实时连接。启动后即可管理白名单、在线玩家与封禁。",
@@ -84,7 +84,7 @@
"access_whitelist_desc": "白名单开启时,仅名单内玩家可进服。", "access_whitelist_desc": "白名单开启时,仅名单内玩家可进服。",
"access_whitelist_add": "添加", "access_whitelist_add": "添加",
"access_whitelist_empty": "白名单暂无玩家。", "access_whitelist_empty": "白名单暂无玩家。",
"access_whitelist_empty_hint": "在上方添加玩家即可放行进服。", "access_whitelist_empty_hint": "添加玩家后将允许其加入服务器。",
"access_whitelist_remove": "将 {{player}} 移出白名单", "access_whitelist_remove": "将 {{player}} 移出白名单",
"access_remove": "移除", "access_remove": "移除",
"access_whitelist_load_error": "无法加载白名单。", "access_whitelist_load_error": "无法加载白名单。",
@@ -108,19 +108,19 @@
"access_ban_confirm": "确认封禁 {{player}}?此玩家将被踢出并无法再进入。", "access_ban_confirm": "确认封禁 {{player}}?此玩家将被踢出并无法再进入。",
"access_ban_confirm_yes": "确认封禁", "access_ban_confirm_yes": "确认封禁",
"access_ban_empty": "暂无封禁玩家。", "access_ban_empty": "暂无封禁玩家。",
"access_ban_empty_hint": "被封禁的玩家会显示在这里,可随时一键解封。", "access_ban_empty_hint": "封禁名单将列出被封禁的玩家,并提供解封操作。",
"access_ban_load_error": "无法加载封禁名单。", "access_ban_load_error": "无法加载封禁名单。",
"access_ban_raw": "查看服务器原始返回", "access_ban_raw": "查看服务器原始返回",
"access_cancel": "取消", "access_cancel": "取消",
"access_banned": "已封禁 {{player}}。", "access_banned": "已封禁 {{player}}。",
"access_pardoned": "已解封 {{player}}。", "access_pardoned": "已解封 {{player}}。",
"wake_list_title": "唤醒名单", "wake_list_title": "唤醒名单",
"wake_list_desc": "自动启动策略设为「唤醒名单」时,服务器休眠后只有你、管理员和这里能唤醒的玩家可以把它启动。玩家第一次进服会自动加入;取消某人的唤醒权限后,他再进服也不会恢复,需要时在这里重新允许。服务器换主人时名单清空。", "wake_list_desc": "自动启动策略为「唤醒名单」时,仅服务器所有者、管理员和具有唤醒权限的玩家可启动休眠服务器。玩家首次加入后自动列入名单;撤销权限后,再次加入不会恢复权限,须手动重新授予。服务器所有者变更后名单清空。",
"wake_list_inactive_owner": "当前自动启动策略是「仅所有者」,休眠时只有你和管理员能启动,这份名单暂不生效。在控制台的「编辑服务器配置」里改成「唤醒名单」后生效。", "wake_list_inactive_owner": "当前策略为「仅所有者」,仅服务器所有者和管理员可启动休眠服务器,唤醒名单不生效。可在「编辑服务器配置」中将策略改为「唤醒名单」。",
"wake_list_inactive_public": "当前自动启动策略是「公开」,任何玩家进服都能启动,这份名单暂不生效。在控制台的「编辑服务器配置」里改成「唤醒名单」后,只有名单里能唤醒的玩家可以启动。", "wake_list_inactive_public": "当前策略为「公开」,任意玩家加入均可触发启动,唤醒名单不生效。改为「唤醒名单」后,仅服务器所有者、管理员和具有唤醒权限的玩家可启动。",
"wake_list_load_error": "无法加载唤醒名单。", "wake_list_load_error": "无法加载唤醒名单。",
"wake_list_empty": "还没有玩家进过这台服务器。", "wake_list_empty": "暂无玩家加入记录。",
"wake_list_empty_hint": "玩家第一次进服后会出现在这里。", "wake_list_empty_hint": "玩家首次加入后将自动列入名单。",
"wake_list_unlinked": "未绑定账号的玩家", "wake_list_unlinked": "未绑定账号的玩家",
"wake_list_joined": "{{when}}首次进服", "wake_list_joined": "{{when}}首次进服",
"wake_list_revoked_badge": "已取消", "wake_list_revoked_badge": "已取消",
@@ -132,12 +132,12 @@
"wake_list_restored": "已重新允许 {{player}} 唤醒服务器。", "wake_list_restored": "已重新允许 {{player}} 唤醒服务器。",
"create_server_btn": "新建服务器", "create_server_btn": "新建服务器",
"create_server_title": "创建服务器", "create_server_title": "创建服务器",
"create_server_desc": "从白名单镜像及规格中选择,平台自动处理其余配置。不暴露原始集群配置。", "create_server_desc": "选择白名单镜像及资源规格。平台自动完成服务器配置,不提供原始集群配置编辑。",
"create_server_name": "名称", "create_server_name": "名称",
"create_server_name_placeholder": "survival", "create_server_name_placeholder": "survival",
"create_server_name_hint": "3–32 位小写字母、数字或连字符。", "create_server_name_hint": "3–32 位小写字母、数字或连字符。",
"create_server_name_invalid": "请用 3–32 位小写字母、数字或连字符,首尾不能是连字符。", "create_server_name_invalid": "请用 3–32 位小写字母、数字或连字符,首尾不能是连字符。",
"create_server_name_reserved": "“{{name}}” 是平台保留的名字,请换一个。", "create_server_name_reserved": "「{{name}}」为平台保留名称,请使用其他名称。",
"create_server_subdomain": "子域名", "create_server_subdomain": "子域名",
"create_server_subdomain_placeholder": "survival", "create_server_subdomain_placeholder": "survival",
"create_server_subdomain_hint": "访问地址:{{host}}", "create_server_subdomain_hint": "访问地址:{{host}}",
@@ -146,7 +146,7 @@
"create_server_image": "镜像", "create_server_image": "镜像",
"create_server_image_loading": "加载镜像列表中…", "create_server_image_loading": "加载镜像列表中…",
"create_server_image_choose": "选择一个镜像", "create_server_image_choose": "选择一个镜像",
"create_server_image_unavailable": "镜像列表未读到", "create_server_image_unavailable": "镜像列表不可用",
"create_server_image_failed": "镜像列表读取失败:{{reason}}", "create_server_image_failed": "镜像列表读取失败:{{reason}}",
"create_server_image_none": "暂无白名单镜像", "create_server_image_none": "暂无白名单镜像",
"create_server_memory": "内存", "create_server_memory": "内存",
@@ -154,7 +154,7 @@
"create_server_policy": "自动启动策略", "create_server_policy": "自动启动策略",
"create_server_policy_owner": "仅所有者——通过面板启动", "create_server_policy_owner": "仅所有者——通过面板启动",
"create_server_policy_public": "公开——任意玩家加入即启动", "create_server_policy_public": "公开——任意玩家加入即启动",
"create_server_policy_allowlist": "唤醒名单——进过服、未被取消唤醒的玩家可启动", "create_server_policy_allowlist": "唤醒名单——已加入且具有唤醒权限的玩家可启动",
"create_server_cancel": "取消", "create_server_cancel": "取消",
"create_server_submit": "创建", "create_server_submit": "创建",
"edit_server_title": "编辑服务器配置", "edit_server_title": "编辑服务器配置",
@@ -162,8 +162,8 @@
"edit_server_desc_long": "正在修改服务器的 spec 配置。未修改的项将保持原样。", "edit_server_desc_long": "正在修改服务器的 spec 配置。未修改的项将保持原样。",
"edit_server_submit": "保存配置", "edit_server_submit": "保存配置",
"edit_server_image_build": "构建 {{id}}", "edit_server_image_build": "构建 {{id}}",
"edit_server_image_pinned_hint": "已锁定在构建 {{id}}:镜像标签以后指向新构建(比如重跑安装器)不会影响这台服务器,只有在这里更换镜像才会。", "edit_server_image_pinned_hint": "服务器已固定至构建 {{id}}。镜像标签指向其他构建时不会自动更新;更换镜像配置后才会使用新构建。",
"edit_server_image_warning": "保存后,服务器会换用这个镜像标签现在指向的构建;重新选择原来的标签,拿到的也是它最新的构建。如果 Minecraft 版本变了,世界会在下次启动时升级,升级过的区块无法再用旧版本打开。", "edit_server_image_warning": "保存后将使用镜像标签当前指向的构建,重新选择同一标签也将采用其最新构建。Minecraft 版本变更可能在下次启动时升级世界;升级后的区块不支持旧版本。",
"edit_server_image_confirm": "我已备份世界,确认更换镜像", "edit_server_image_confirm": "我已备份世界,确认更换镜像",
"luckperms_group_name": "用户组名称", "luckperms_group_name": "用户组名称",
"luckperms_node": "权限节点", "luckperms_node": "权限节点",
@@ -183,9 +183,9 @@
"luckperms_remove_group_btn": "移除用户组", "luckperms_remove_group_btn": "移除用户组",
"luckperms_remove_perm_btn": "移除权限", "luckperms_remove_perm_btn": "移除权限",
"luckperms_perm_form_title": "添加或移除权限", "luckperms_perm_form_title": "添加或移除权限",
"luckperms_remove_perm_hint": "移除会清掉该世界范围里的这个节点,授予和拒绝都一样清掉;权限值只对添加生效。", "luckperms_remove_perm_hint": "移除操作将删除指定世界范围内的权限节点,无论其值为允许或拒绝。权限值仅用于添加操作。",
"luckperms_groups_unread": "读不到这名玩家现有的用户组。要移除某个组,在下面填上组名后点「移除用户组」。", "luckperms_groups_unread": "无法读取玩家现有用户组。可填写用户组名称并执行「移除用户组」。",
"luckperms_perms_unread": "读不到这名玩家现有的权限节点。要移除某个节点,在下面填上节点名(按世界设置的再填世界)后点「移除权限」。", "luckperms_perms_unread": "无法读取玩家现有权限节点。可填写节点名称及适用的世界范围,并执行「移除权限」。",
"luckperms_groups_title": "所属用户组", "luckperms_groups_title": "所属用户组",
"luckperms_permissions_title": "拥有权限节点", "luckperms_permissions_title": "拥有权限节点",
"luckperms_node_column": "权限节点", "luckperms_node_column": "权限节点",
@@ -198,16 +198,16 @@
"luckperms_recent_actions": "最近操作历史", "luckperms_recent_actions": "最近操作历史",
"luckperms_no_recent_actions": "暂无最近操作历史。", "luckperms_no_recent_actions": "暂无最近操作历史。",
"luckperms_revert": "撤销", "luckperms_revert": "撤销",
"luckperms_revert_to_set": "撤销会把 {{node}} 改回 {{value}}", "luckperms_revert_to_set": "撤销后将 {{node}} 恢复为 {{value}}",
"luckperms_revert_to_unset": "撤销会移除 {{node}}", "luckperms_revert_to_unset": "撤销会移除 {{node}}",
"luckperms_revert_unknown": "无法撤销:原值未知", "luckperms_revert_unknown": "无法撤销:原值未知",
"luckperms_remove_group": "移除权限组 {{group}}", "luckperms_remove_group": "移除权限组 {{group}}",
"luckperms_remove_perm": "移除权限 {{node}}", "luckperms_remove_perm": "移除权限 {{node}}",
"luckperms_revert_failed": "撤销失败:{{reason}}", "luckperms_revert_failed": "撤销失败:{{reason}}",
"luckperms_revert_success": "已成功撤销该操作!", "luckperms_revert_success": "操作已撤销。",
"luckperms_presets": "预设", "luckperms_presets": "预设",
"luckperms_no_parent_groups": "未分配任何父组", "luckperms_no_parent_groups": "未分配任何父组",
"luckperms_no_reply": "这台服上的 LuckPerms 不通过 RCON 回话,所以显示不出玩家现有的用户组和权限节点。添加和移除照常生效:在下面填上组名或节点名即可。按玩家名的操作只对「自 LuckPerms 安装以来进过本服」的玩家可靠,对没进过服的玩家名可能静默不生效。", "luckperms_no_reply": "LuckPerms 未通过 RCON 返回查询结果,无法显示现有用户组和权限节点。添加及移除操作仍可执行。按玩家名操作仅对安装 LuckPerms 后已加入过服务器的玩家可靠;未加入过的玩家可能无法应用更改,且不返回错误。",
"luckperms_no_output": "(服务器未返回输出,无法确认结果)", "luckperms_no_output": "(服务器未返回输出,无法确认结果)",
"luckperms_global": "全局", "luckperms_global": "全局",
"luckperms_no_perms": "尚未分配任何权限节点", "luckperms_no_perms": "尚未分配任何权限节点",
@@ -215,37 +215,37 @@
"luckperms_clear_history": "清除历史记录", "luckperms_clear_history": "清除历史记录",
"edit_server_cpu": "CPU 限制", "edit_server_cpu": "CPU 限制",
"edit_server_cpu_placeholder": "例如 1、1.5、500m,留空不限制", "edit_server_cpu_placeholder": "例如 1、1.5、500m,留空不限制",
"edit_server_cpu_invalid": "填核数(1、1.5)或毫核(500m),要大于 0。", "edit_server_cpu_invalid": "CPU 限制须为大于 0 的核数(如 1、1.5)或毫核(如 500m)。",
"owned_filter_mine": "我", "owned_filter_mine": "我",
"edit_server_idle": "空闲自动停服", "edit_server_idle": "空闲自动停服",
"edit_server_idle_hint": "没人在线达到这个时长就自动停服,省下内存和 CPU;玩家下次进服时自动唤醒。", "edit_server_idle_hint": "在线人数为零且达到配置时长后,服务器将自动停止并释放内存及 CPU。后续玩家加入时,按自动启动策略处理唤醒。",
"edit_server_idle_unknown": "现在读不到在线人数,空闲停服暂停中,直到能读到为止。", "edit_server_idle_unknown": "在线人数读取失败,空闲停服已暂停。读取恢复后将继续判断。",
"idle_stop_never": "从不", "idle_stop_never": "从不",
"idle_stop_seconds": "{{count}} 秒", "idle_stop_seconds": "{{count}} 秒",
"idle_stop_minutes": "{{count}} 分钟", "idle_stop_minutes": "{{count}} 分钟",
"idle_stop_hours": "{{count}} 小时", "idle_stop_hours": "{{count}} 小时",
"stop_confirm_players": "{{count}} 名玩家在线。停服会先在游戏里提醒他们,30 秒后断开。确定停止?", "stop_confirm_players": "当前有 {{count}} 名玩家在线。停止操作将先发送游戏内通知,30 秒后断开玩家连接。确认停止?",
"stop_confirm_unknown": "读不到在线人数,可能有玩家在线。确定停止?", "stop_confirm_unknown": "在线人数读取失败,可能仍有玩家在线。确认停止?",
"retire_card_title": "放弃服务器", "retire_card_title": "放弃服务器",
"retire_card_title_admin": "放弃或删除服务器", "retire_card_title_admin": "放弃或删除服务器",
"retire_card_desc": "不再需要时交还给平台:世界归档后,服务器释放给其他人认领。", "retire_card_desc": "放弃服务器后,世界将归档,服务器将释放供其他账户认领。",
"retire_card_desc_admin": "放弃会归档世界并释放服务器;删除会归档世界并移除服务器,名称和子域名可以重新使用。", "retire_card_desc_admin": "放弃会归档世界并释放服务器;删除会归档世界并移除服务器,名称和子域名可以重新使用。",
"retire_release": "放弃服务器", "retire_release": "放弃服务器",
"retire_delete": "删除服务器", "retire_delete": "删除服务器",
"retire_release_title": "放弃「{{name}}」?", "retire_release_title": "放弃「{{name}}」?",
"retire_delete_title": "删除「{{name}}」?", "retire_delete_title": "删除「{{name}}」?",
"retire_step_stop": "服务器会立即停止,处理完之前不能启动。", "retire_step_stop": "服务器将进入停止流程,回收处理完成或请求撤销前无法启动。",
"retire_step_release": "下一次每日回收运行时,世界先归档(默认保留 90 天),然后删除世界存储,服务器释放给其他人认领。", "retire_step_release": "下一次每日回收运行时,世界先归档(默认保留 90 天),然后删除世界存储,服务器释放给其他人认领。",
"retire_step_delete": "下一次每日回收运行时,世界先归档(默认保留 90 天),然后删除世界存储和这台服务器,名称和子域名释放出来。", "retire_step_delete": "下一次每日回收时将归档世界(默认保留 90 天),再删除世界存储及服务器记录,释放名称和子域名。",
"retire_step_cancel_owner": "回收运行之前可以在控制台撤销;在此之前它仍计入你的配额。", "retire_step_cancel_owner": "回收执行前可在控制台撤销请求。完成回收前,服务器仍计入账户配额。",
"retire_step_cancel_admin": "回收运行之前管理员可以在控制台撤销。", "retire_step_cancel_admin": "回收运行之前管理员可以在控制台撤销。",
"retire_confirm_label": "输入服务器名称以确认:", "retire_confirm_label": "输入服务器名称以确认:",
"retire_confirm_release": "放弃服务器", "retire_confirm_release": "放弃服务器",
"retire_confirm_delete": "删除服务器", "retire_confirm_delete": "删除服务器",
"retiring_title_release": "这台服务器已被放弃", "retiring_title_release": "服务器待回收",
"retiring_title_delete": "这台服务器即将被删除", "retiring_title_delete": "服务器待删除",
"retiring_body_release": "{{when}}提出。下一次每日回收运行时会归档世界,然后删除世界存储并释放服务器。在此之前它保持停止、不能启动,仍计入配额。", "retiring_body_release": "请求提交于 {{when}}。下一次每日回收时将归档世界、删除世界存储并释放服务器。处理完成前服务器保持停止,无法启动,仍计入账户配额。",
"retiring_body_delete": "{{when}}提出。下一次每日回收运行时会归档世界,然后删除世界存储和这台服务器。在此之前它保持停止、不能启动。", "retiring_body_delete": "请求提交于 {{when}}。下一次每日回收时将归档世界、删除世界存储及服务器记录。处理完成前服务器保持停止,无法启动。",
"retiring_cancel": "撤销", "retiring_cancel": "撤销",
"retiring_cancel_admin_only": "删除由管理员提出,只有管理员可以撤销。", "retiring_cancel_admin_only": "删除由管理员提出,只有管理员可以撤销。",
"retiring_badge_release": "等待回收", "retiring_badge_release": "等待回收",
@@ -278,7 +278,7 @@
"startup_booting": "正在启动 Minecraft 并检查就绪状态", "startup_booting": "正在启动 Minecraft 并检查就绪状态",
"startup_elapsed": "本次启动已等待 {{seconds}} 秒", "startup_elapsed": "本次启动已等待 {{seconds}} 秒",
"startup_memory": "可调度内存不足。请降低此服务器的内存配置、停止其他服务器,或增加节点容量。Java 尚未启动。", "startup_memory": "可调度内存不足。请降低此服务器的内存配置、停止其他服务器,或增加节点容量。Java 尚未启动。",
"startup_hint": "启动状态会自动刷新。资源调度、文件准备与游戏启动是不同阶段,没有日志输出并不代表程序卡死。", "startup_hint": "启动分为资源调度、文件准备及游戏初始化阶段,状态将自动刷新。无日志输出时,请依据当前阶段及诊断信息判断进度。",
"status_stale": "状态已过期", "status_stale": "状态已过期",
"status_stale_detail": "无法确认服务器当前状态。最后成功读取:{{time}}。正在自动重试。{{error}}", "status_stale_detail": "无法确认服务器当前状态。最后成功读取:{{time}}。正在自动重试。{{error}}",
"startup_failed": "启动受阻或容器异常", "startup_failed": "启动受阻或容器异常",
@@ -288,10 +288,10 @@
"startup_waiting_resources": "等待资源", "startup_waiting_resources": "等待资源",
"startup_failed_hint": "请根据报错检查镜像、配置与内存。修正问题后再重新启动服务器。", "startup_failed_hint": "请根据报错检查镜像、配置与内存。修正问题后再重新启动服务器。",
"emergency_stop": "应急停止", "emergency_stop": "应急停止",
"emergency_stop_warning": "普通停止无响应时使用。直接将工作负载缩到零,绕过游戏进程和 Operator,保留正常退出时间;不会删除世界,但无法保证未保存的内容。需要重新验证身份。", "emergency_stop_warning": "普通停止无响应时,可直接将工作负载副本数设为零,绕过游戏进程及 Operator,并保留正常终止宽限期。世界数据不删除,但未保存的更改可能丢失。执行前须重新验证身份。",
"emergency_stop_accepted": "停止意图已保存,工作负载已缩到零。仍需等待 Pod 退出;上方状态会继续检查,不能将请求受理视为已经停止。", "emergency_stop_accepted": "停止意图已保存,工作负载副本数已设为零。系统将继续检查 Pod 退出状态;请求受理不代表进程已停止。",
"emergency_stop_unknown": "操作结果尚未确认。停止意图可能已经保存,请重新读取状态;若控制 API 不可达,请使用主机侧恢复步骤。", "emergency_stop_unknown": "操作结果尚未确认。停止意图可能已经保存,请重新读取状态;若控制 API 不可达,请使用主机侧恢复步骤。",
"host_recovery": "控制 API 不可达?查看主机侧应急停止步骤", "host_recovery": "主机侧应急停止步骤",
"host_recovery_hint": "面板无法绕过已经离线的控制 API。仅在部署主机上执行下列命令:先保存停止意图,再缩到零,最后检查游戏 Pod。", "host_recovery_hint": "控制 API 离线时,面板无法执行集群操作。请在部署主机依次执行以下命令:保存停止意图、将副本数设为零、检查游戏 Pod。命名空间不是 minecraft 时须替换命令中的命名空间。",
"host_recovery_verify": "如果 Pod 仍在退出或节点不可达,不能确认进程已停止。检查对应节点和容器运行时;不要强制删除 Pod 来冒充停止成功。" "host_recovery_verify": "Pod 仍在终止或节点不可达时,无法确认进程已停止。请检查对应节点及容器运行时;强制删除 Pod 不能作为进程退出的依据。"
} }
@@ -27,7 +27,7 @@
"build_status_failed": "构建失败", "build_status_failed": "构建失败",
"build_status_cancelled": "已取消", "build_status_cancelled": "已取消",
"no_submissions_title": "暂无提交记录", "no_submissions_title": "暂无提交记录",
"no_submissions_hint": "您还没有提交过任何模组包。", "no_submissions_hint": "当前账户暂无模组包提交记录。",
"search_no_results": "无匹配结果", "search_no_results": "无匹配结果",
"search_no_results_hint": "尝试更换搜索词或筛选条件。", "search_no_results_hint": "尝试更换搜索词或筛选条件。",
"search_placeholder": "搜索提交记录...", "search_placeholder": "搜索提交记录...",
@@ -38,17 +38,17 @@
"field_context_ref": "构建上下文引用", "field_context_ref": "构建上下文引用",
"field_image_ref": "目标镜像引用", "field_image_ref": "目标镜像引用",
"field_context_sha256": "上下文 SHA-256", "field_context_sha256": "上下文 SHA-256",
"field_context_sha256_hint": "平台收到的文件摘要,可用 sha256sum 与本地文件核对。审核通过的就是这份内容。", "field_context_sha256_hint": "服务器已接收内容的摘要。可使用 sha256sum 与本地文件核对;审核通过后将固定此内容。",
"clear_btn": "清除", "clear_btn": "清除",
"file_hint": "支持 .tar.gz 格式,最大 {{limit}}", "file_hint": "支持 .tar.gz 格式,最大 {{limit}}",
"withdraw_btn": "撤回提交", "withdraw_btn": "撤回提交",
"withdraw_hint": "撤回会删除该提交及其已上传的构建上下文,并释放你的待审核名额与存储配额。", "withdraw_hint": "撤回将删除提交及已上传的构建上下文,并释放对应的待审核名额与存储配额。",
"withdraw_confirm": "确认撤回", "withdraw_confirm": "确认撤回",
"withdraw_cancel": "取消", "withdraw_cancel": "取消",
"upload_progress_label": "上传进度", "upload_progress_label": "上传进度",
"upload_progress": "{{sent}} / {{total}}", "upload_progress": "{{sent}} / {{total}}",
"pause_btn": "暂停", "pause_btn": "暂停",
"resume_btn": "继续上传", "resume_btn": "继续上传",
"resume_hint": "“{{name}}”已保存,构建上下文已有 {{sent}} 传到服务器。再次提交即可从这里接着传。", "resume_hint": "「{{name}}」已保存,服务器已接收 {{sent}} 构建上下文。再次提交将从已确认的位置继续上传。",
"restart_hint": "“{{name}}”已保存。新选的文件会从头上传。" "restart_hint": "「{{name}}」已保存。重新选择文件后将从起始位置上传。"
} }
+42 -42
View File
@@ -114,7 +114,7 @@ describe("session auth wire shapes", () => {
it("maps the auth error codes to stable human copy", async () => { it("maps the auth error codes to stable human copy", async () => {
const { humanizeError } = await import("./api"); const { humanizeError } = await import("./api");
expect(humanizeError({ code: "local_auth_disabled" })).toMatch(/turned off/i); expect(humanizeError({ code: "local_auth_disabled" })).toMatch(/disabled/i);
expect(humanizeError({ code: "staff_account" })).toMatch(/operator/i); expect(humanizeError({ code: "staff_account" })).toMatch(/operator/i);
}); });
@@ -227,7 +227,7 @@ describe("account migration wire shapes", () => {
it("says a redeem over quota can be retried with the same code once the quota fits", () => { it("says a redeem over quota can be retried with the same code once the quota fits", () => {
expect(humanizeError({ status: 403, code: "migrate_quota_exceeded" })).toBe( expect(humanizeError({ status: 403, code: "migrate_quota_exceeded" })).toBe(
"The servers this migration brings over don't fit your quota. Ask an admin to raise it, then redeem the same code again before it expires.", "The migration exceeds the current account’s server quota. Request a quota adjustment from an administrator and redeem the same code again before it expires.",
); );
}); });
}); });
@@ -430,36 +430,36 @@ describe("api access-control wire shapes", () => {
it("says a server name is held by a world volume left behind", async () => { it("says a server name is held by a world volume left behind", async () => {
const { humanizeError } = await import("./api"); const { humanizeError } = await import("./api");
expect(humanizeError({ status: 409, code: "world_volume_exists" })).toBe( expect(humanizeError({ status: 409, code: "world_volume_exists" })).toBe(
"An earlier server with that name left its world volume behind. Choose another name, or ask the operator to delete the old volume.", "A world volume from a server of the same name still exists. Use a different name or request an operator to verify and process the existing volume.",
); );
}); });
it("says why a user change was refused for the caller's own or the owner account", async () => { it("says why a user change was refused for the caller's own or the owner account", async () => {
const { humanizeError } = await import("./api"); const { humanizeError } = await import("./api");
expect(humanizeError({ status: 403, code: "self_protected" })).toBe( expect(humanizeError({ status: 403, code: "self_protected" })).toBe(
"You can't do that to the account you're signed in with.", "This operation is not permitted on the signed-in account.",
); );
expect(humanizeError({ status: 403, code: "owner_protected" })).toBe( expect(humanizeError({ status: 403, code: "owner_protected" })).toBe(
"The owner account can't be demoted, disabled or deleted from the panel. Only the host's break-glass console (sudo felis breakGlass) manages it.", "The Owner account cannot be demoted, disabled, or deleted from the panel. Manage it through the host recovery console (sudo felis breakGlass).",
); );
}); });
it("says what a Minecraft UUID looks like when the typed one is not", () => { it("says what a Minecraft UUID looks like when the typed one is not", () => {
expect(humanizeError({ status: 400, code: "bad_mc_uuid" })).toBe( expect(humanizeError({ status: 400, code: "bad_mc_uuid" })).toBe(
"That isn't a Minecraft UUID. It looks like 069a79f4-44e9-4726-a5be-fca90e38aaf5, with or without the dashes.", "Invalid Minecraft UUID format. Example: 069a79f4-44e9-4726-a5be-fca90e38aaf5. Hyphens are optional.",
); );
}); });
it("says what a display name may hold when the server refuses one", () => { it("says what a display name may hold when the server refuses one", () => {
expect(humanizeError({ status: 400, code: "bad_display_name" })).toBe( expect(humanizeError({ status: 400, code: "bad_display_name" })).toBe(
"That display name is invalid — use at most 64 characters, all visible ones or spaces, with no line breaks, tabs or invisible control characters.", "Invalid display name. Use at most 64 visible characters or spaces, excluding line breaks, tabs, and control characters.",
); );
}); });
it("says email codes are off when the install has no mail relay", async () => { it("says email codes are off when the install has no mail relay", async () => {
const { humanizeError } = await import("./api"); const { humanizeError } = await import("./api");
expect(humanizeError({ status: 503, code: "mail_unavailable" })).toBe( expect(humanizeError({ status: 503, code: "mail_unavailable" })).toBe(
"This server can't send email codes because no mail relay is set up. Sign in with a passkey, or ask the server operator to configure email.", "Mail delivery is not configured, so verification codes cannot be sent. Sign in with a Passkey or request mail configuration from an administrator.",
); );
}); });
@@ -673,7 +673,7 @@ describe("image whitelist and builds wire shapes", () => {
it("says an uploads store that did not answer is worth another try", async () => { it("says an uploads store that did not answer is worth another try", async () => {
const { humanizeError } = await import("./api"); const { humanizeError } = await import("./api");
expect(humanizeError({ status: 503, code: "uploads_store_unavailable" })).toBe( expect(humanizeError({ status: 503, code: "uploads_store_unavailable" })).toBe(
"The uploads store did not answer — try again in a moment; what was already sent is kept.", "Upload storage did not respond. Received content is retained. Retry later.",
); );
}); });
@@ -876,7 +876,7 @@ describe("image whitelist and builds wire shapes", () => {
fakeFetch({ path: "world/level.dat", content, sha256: "a".repeat(64), content_sha256: hexOf(new Uint8Array([0, 1, 2])) }), fakeFetch({ path: "world/level.dat", content, sha256: "a".repeat(64), content_sha256: hexOf(new Uint8Array([0, 1, 2])) }),
); );
await expect(api.readServerFile("survival", "world/level.dat")).rejects.toMatchObject({ code: "read_damaged" }); await expect(api.readServerFile("survival", "world/level.dat")).rejects.toMatchObject({ code: "read_damaged" });
expect(humanizeError({ code: "read_damaged" })).toMatch(/arrived damaged/); expect(humanizeError({ code: "read_damaged" })).toMatch(/File transfer verification failed/);
}); });
it("writeServerFile PUTs {content} — an explicit \"\" is a deliberate truncate, not an omitted field", async () => { it("writeServerFile PUTs {content} — an explicit \"\" is a deliberate truncate, not an omitted field", async () => {
@@ -992,7 +992,7 @@ describe("path parameters", () => {
await expect(api.deleteUser(bad)).rejects.toMatchObject({ code: "bad_path_param" }); await expect(api.deleteUser(bad)).rejects.toMatchObject({ code: "bad_path_param" });
} }
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(humanizeError({ status: 0, code: "bad_path_param" })).toMatch(/not valid/i); expect(humanizeError({ status: 0, code: "bad_path_param" })).toMatch(/invalid/i);
}); });
it("does not encode a value twice", async () => { it("does not encode a value twice", async () => {
@@ -1041,7 +1041,7 @@ describe("the caller's own sessions", () => {
}); });
it("says a session that is already gone has ended", () => { it("says a session that is already gone has ended", () => {
expect(humanizeError({ status: 404, code: "session_not_found" })).toBe("That session has already ended."); expect(humanizeError({ status: 404, code: "session_not_found" })).toBe("The session has ended or does not exist.");
}); });
}); });
@@ -1091,11 +1091,11 @@ describe("responses that are not the API's JSON", () => {
message: "this build's scan kept no sbom: it was too large to keep, or the step that writes it failed", message: "this build's scan kept no sbom: it was too large to keep, or the step that writes it failed",
}; };
expect(humanizeError(err)).toBe( expect(humanizeError(err)).toBe(
"This build kept no copy of that scan file: it was too large to keep, or the step that writes it failed.", "The scan document was not retained for this build. The file may have exceeded the size limit or its generation step may have failed.",
); );
await i18next.changeLanguage("zh-CN"); await i18next.changeLanguage("zh-CN");
try { try {
expect(humanizeError(err)).toBe("这次构建没有留下这份扫描文件:文件太大没有保存,或者生成它的步骤失败了。"); expect(humanizeError(err)).toBe("构建未留存此扫描文件,可能由于文件过大或生成步骤失败。");
} finally { } finally {
await i18next.changeLanguage("en-US"); await i18next.changeLanguage("en-US");
} }
@@ -1103,7 +1103,7 @@ describe("responses that are not the API's JSON", () => {
it("maps an unknown 5xx code to the unavailable line", () => { it("maps an unknown 5xx code to the unavailable line", () => {
expect(humanizeError({ status: 500, code: "unknown", message: "internal error" })).toMatch(/unavailable/i); expect(humanizeError({ status: 500, code: "unknown", message: "internal error" })).toMatch(/unavailable/i);
expect(humanizeError({ status: 413, code: "error", message: "Payload Too Large" })).toMatch(/larger/i); expect(humanizeError({ status: 413, code: "error", message: "Payload Too Large" })).toMatch(/exceeds/i);
}); });
it("keeps an internal error's request ID without exposing backend details", async () => { it("keeps an internal error's request ID without exposing backend details", async () => {
@@ -1381,13 +1381,13 @@ describe("server file manager wire shapes", () => {
code: "file_unreadable", code: "file_unreadable",
}); });
expect(FakeXHR.last).toBeUndefined(); expect(FakeXHR.last).toBeUndefined();
expect(humanizeError({ code: "file_unreadable" })).toMatch(/changed, moved or deleted/); expect(humanizeError({ code: "file_unreadable" })).toMatch(/modified, moved, or deleted/);
}); });
it("the upload refusals over a checksum read as what to do next", () => { it("the upload refusals over a checksum read as what to do next", () => {
expect(humanizeError({ code: "digest_mismatch" })).toMatch(/changed on its way/); expect(humanizeError({ code: "digest_mismatch" })).toMatch(/File transfer verification failed/);
expect(humanizeError({ code: "digest_required" })).toMatch(/without a checksum/); expect(humanizeError({ code: "digest_required" })).toMatch(/omitted its checksum/);
expect(humanizeError({ code: "bad_digest" })).toMatch(/checksum was malformed/); expect(humanizeError({ code: "bad_digest" })).toMatch(/checksum format is invalid/);
}); });
it("createServerFile PUTs the content with create_only, so nothing already there is replaced", async () => { it("createServerFile PUTs the content with create_only, so nothing already there is replaced", async () => {
@@ -1580,9 +1580,9 @@ describe("server file manager wire shapes", () => {
}); });
it("words the upload session codes in the panel's own copy", () => { it("words the upload session codes in the panel's own copy", () => {
expect(humanizeError({ status: 404, code: "upload_not_found", message: "raw" })).toMatch(/^This upload is gone/); expect(humanizeError({ status: 404, code: "upload_not_found", message: "raw" })).toMatch(/^The multipart upload record is unavailable/);
expect(humanizeError({ status: 429, code: "too_many_uploads", message: "raw" })).toMatch(/4 large uploads in progress/); expect(humanizeError({ status: 429, code: "too_many_uploads", message: "raw" })).toMatch(/four concurrent large-file uploads/);
expect(humanizeError({ status: 0, code: "op_lost", message: "" })).toMatch(/progress can no longer be read/); expect(humanizeError({ status: 0, code: "op_lost", message: "" })).toMatch(/Operation progress is unavailable/);
}); });
}); });
@@ -1679,10 +1679,10 @@ describe("scheduled task wire shapes", () => {
// "invalid request"); the panel words them itself so a Chinese UI never shows it. // "invalid request"); the panel words them itself so a Chinese UI never shows it.
describe("copy for the generic server codes", () => { describe("copy for the generic server codes", () => {
it("names a missing record, a lost race and a refused format in the UI's words", () => { it("names a missing record, a lost race and a refused format in the UI's words", () => {
expect(humanizeError({ status: 404, code: "not_found", message: "user not found" })).toMatch(/no longer exists/); expect(humanizeError({ status: 404, code: "not_found", message: "user not found" })).toMatch(/does not exist/);
expect(humanizeError({ status: 409, code: "conflict", message: "conflict" })).toMatch(/changed in the meantime/); expect(humanizeError({ status: 409, code: "conflict", message: "conflict" })).toMatch(/changed during the operation/);
expect(humanizeError({ status: 409, code: "restore_in_progress", message: "restore running" })).toMatch( expect(humanizeError({ status: 409, code: "restore_in_progress", message: "restore running" })).toMatch(
/still being restored/, /being restored to the world/,
); );
expect(humanizeError({ status: 415, code: "unsupported_media_type", message: "json only" })).toMatch( expect(humanizeError({ status: 415, code: "unsupported_media_type", message: "json only" })).toMatch(
/format the server does not accept/, /format the server does not accept/,
@@ -1693,35 +1693,35 @@ describe("copy for the generic server codes", () => {
expect(humanizeError({ status: 400, code: "bad_request", message: "mc_uuid is required" })).toBe( expect(humanizeError({ status: 400, code: "bad_request", message: "mc_uuid is required" })).toBe(
"The request was not accepted: mc_uuid is required", "The request was not accepted: mc_uuid is required",
); );
expect(humanizeError({ status: 400, code: "bad_request", message: "" })).toBe("Something went wrong."); expect(humanizeError({ status: 400, code: "bad_request", message: "" })).toBe("The operation failed. Retry later.");
}); });
it("words the file manager's refusals itself", () => { it("words the file manager's refusals itself", () => {
expect(humanizeError({ status: 409, code: "file_exists", message: "something is already at a.txt" })).toBe( expect(humanizeError({ status: 409, code: "file_exists", message: "something is already at a.txt" })).toBe(
"Something with that name is already there. Pick another name, or rename or delete the one that is there first.", "A file or folder with this name already exists in the current directory. Use a different name, or rename or delete the existing entry.",
); );
expect(humanizeError({ status: 507, code: "upload_staging_full", message: "raw" })).toMatch(/upload space is nearly full/); expect(humanizeError({ status: 507, code: "upload_staging_full", message: "raw" })).toMatch(/Upload staging space is insufficient/);
expect(humanizeError({ status: 400, code: "upload_incomplete", message: "raw" })).toMatch(/stopped before the whole file arrived/); expect(humanizeError({ status: 400, code: "upload_incomplete", message: "raw" })).toMatch(/interrupted before the complete file was transferred/);
expect(humanizeError({ status: 411, code: "length_required", message: "raw" })).toMatch(/did not say how large it is/); expect(humanizeError({ status: 411, code: "length_required", message: "raw" })).toMatch(/omitted the file size/);
}); });
it("words the scheduled tasks' refusals itself, and keeps the reason a schedule was refused", () => { it("words the scheduled tasks' refusals itself, and keeps the reason a schedule was refused", () => {
expect(humanizeError({ status: 503, code: "schedules_unavailable", message: "raw" })).toBe( expect(humanizeError({ status: 503, code: "schedules_unavailable", message: "raw" })).toBe(
"Scheduled tasks aren't available right now.", "Scheduled tasks are currently unavailable.",
); );
expect(humanizeError({ status: 409, code: "schedule_limit", message: "raw" })).toBe( expect(humanizeError({ status: 409, code: "schedule_limit", message: "raw" })).toBe(
"This server already has as many scheduled tasks as it can hold. Delete one first.", "The server’s scheduled-task limit has been reached. Remove an existing task before retrying.",
); );
expect(humanizeError({ status: 409, code: "schedule_running", message: "raw" })).toBe( expect(humanizeError({ status: 409, code: "schedule_running", message: "raw" })).toBe(
"This task is running right now. Try again once the run finishes.", "The task is running. Retry after the current execution completes.",
); );
expect(humanizeError({ status: 409, code: "schedule_stale", message: "raw" })).toBe( expect(humanizeError({ status: 409, code: "schedule_stale", message: "raw" })).toBe(
"The server has a new owner since this task was saved. Save the task again before running it.", "The server owner changed after the task was saved. Save the task again before execution.",
); );
expect( expect(
humanizeError({ status: 400, code: "bad_schedule", message: "a restart can repeat at most every 60 minutes" }), humanizeError({ status: 400, code: "bad_schedule", message: "a restart can repeat at most every 60 minutes" }),
).toBe("The task was not saved: a restart can repeat at most every 60 minutes"); ).toBe("The task was not saved: a restart can repeat at most every 60 minutes");
expect(humanizeError({ status: 400, code: "bad_schedule", message: "" })).toBe("Something went wrong."); expect(humanizeError({ status: 400, code: "bad_schedule", message: "" })).toBe("The operation failed. Retry later.");
}); });
it("reads a full upload store as full, not as an outage", () => { it("reads a full upload store as full, not as an outage", () => {
@@ -1731,18 +1731,18 @@ describe("copy for the generic server codes", () => {
it("words a failure the panel caught itself from its code", () => { it("words a failure the panel caught itself from its code", () => {
const err = clientError("passkey_no_credential"); const err = clientError("passkey_no_credential");
expect(err.status).toBe(0); expect(err.status).toBe(0);
expect(humanizeError(err)).toBe("The browser returned no passkey. Try again."); expect(humanizeError(err)).toBe("The browser returned no Passkey credential. Retry.");
}); });
it("words the re-authentication refusals", () => { it("words the re-authentication refusals", () => {
expect(humanizeError({ status: 403, code: "reauth_required", message: "raw" })).toBe( expect(humanizeError({ status: 403, code: "reauth_required", message: "raw" })).toBe(
"Confirm it's you first: this change needs your Passkey or an email code from the last few minutes.", "This operation requires recent identity verification. Verify identity again using a Passkey or email code.",
); );
expect(humanizeError({ status: 403, code: "staff_reauth", message: "raw" })).toBe( expect(humanizeError({ status: 403, code: "staff_reauth", message: "raw" })).toBe(
"Operator accounts confirm with a Passkey or by signing in again.", "Operator accounts confirm with a Passkey or by signing in again.",
); );
expect(humanizeError({ status: 400, code: "no_session", message: "raw" })).toBe( expect(humanizeError({ status: 400, code: "no_session", message: "raw" })).toBe(
"This only works in a browser signed in to Felis.", "This operation requires an active Felis browser session.",
); );
}); });
}); });
@@ -1800,16 +1800,16 @@ describe("world export wire shapes", () => {
it("words the export refusals itself", () => { it("words the export refusals itself", () => {
expect(humanizeError({ status: 429, code: "export_busy", message: "raw" })).toBe( expect(humanizeError({ status: 429, code: "export_busy", message: "raw" })).toBe(
"Too many exports are being prepared: one at a time per person, two at a time across the platform, and six an hour per person. Try again in a few minutes.", "Export limits have been reached: one concurrent request per account, two across the platform, and six per account per hour. Retry after existing tasks complete or rate limits reset.",
); );
expect(humanizeError({ status: 410, code: "export_expired", message: "raw" })).toBe( expect(humanizeError({ status: 410, code: "export_expired", message: "raw" })).toBe(
"This download has expired or was already used — start the export again.", "The download link has expired or has already been used. Export the content again.",
); );
expect(humanizeError({ status: 409, code: "export_not_ready", message: "raw" })).toBe( expect(humanizeError({ status: 409, code: "export_not_ready", message: "raw" })).toBe(
"The download isn't ready yet — wait a moment and try again.", "Download preparation is incomplete. Retry later.",
); );
expect(humanizeError({ status: 503, code: "export_unavailable", message: "raw" })).toBe( expect(humanizeError({ status: 503, code: "export_unavailable", message: "raw" })).toBe(
"Downloading worlds and backups isn't set up on this deployment — ask an administrator.", "World and backup downloads are not configured in this deployment. Contact an administrator.",
); );
}); });
}); });
+1 -1
View File
@@ -178,7 +178,7 @@ describe("Account passkey delete", () => {
expect(await screen.findByText("Firefox on Windows")).toBeTruthy(); expect(await screen.findByText("Firefox on Windows")).toBeTruthy();
await userEvent.click(await screen.findByRole("button", deleteButton("Laptop"))); await userEvent.click(await screen.findByRole("button", deleteButton("Laptop")));
expect(within(screen.getByRole("dialog")).getByText(/Your other devices are signed out too\./)).toBeTruthy(); expect(within(screen.getByRole("dialog")).getByText(/signs out sessions on other devices/)).toBeTruthy();
await userEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: t("account:passkey_delete_confirm") })); await userEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: t("account:passkey_delete_confirm") }));
await waitFor(() => expect(screen.queryByText("Firefox on Windows")).toBeNull()); await waitFor(() => expect(screen.queryByText("Firefox on Windows")).toBeNull());
+7 -7
View File
@@ -82,11 +82,11 @@ function renderAccount() {
); );
} }
const checkDialog = () => screen.findByRole("dialog", { name: "Confirm it's you" }); const checkDialog = () => screen.findByRole("dialog", { name: "Account identity verification" });
async function askToDeleteLaptop() { async function askToDeleteLaptop() {
await userEvent.click(await screen.findByRole("button", { name: "Delete passkey “Laptop”" })); await userEvent.click(await screen.findByRole("button", { name: "Delete passkey “Laptop”" }));
const confirmDelete = screen.getByRole("dialog", { name: "Delete this passkey?" }); const confirmDelete = screen.getByRole("dialog", { name: "Delete this Passkey?" });
await userEvent.click(within(confirmDelete).getByRole("button", { name: "Delete" })); await userEvent.click(within(confirmDelete).getByRole("button", { name: "Delete" }));
} }
@@ -140,8 +140,8 @@ describe("Account re-authentication", () => {
const dialog = await checkDialog(); const dialog = await checkDialog();
await userEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); await userEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Confirm it's you" })).toBeNull()); await waitFor(() => expect(screen.queryByRole("dialog", { name: "Account identity verification" })).toBeNull());
const confirmDelete = screen.getByRole("dialog", { name: "Delete this passkey?" }); const confirmDelete = screen.getByRole("dialog", { name: "Delete this Passkey?" });
expect(within(confirmDelete).queryByRole("alert")).toBeNull(); expect(within(confirmDelete).queryByRole("alert")).toBeNull();
expect(mocks.passkeyDelete).toHaveBeenCalledTimes(1); expect(mocks.passkeyDelete).toHaveBeenCalledTimes(1);
expect(mocks.passkeyList).toHaveBeenCalledTimes(1); expect(mocks.passkeyList).toHaveBeenCalledTimes(1);
@@ -159,7 +159,7 @@ describe("Account re-authentication", () => {
await proveByEmail(dialog); await proveByEmail(dialog);
const alert = await within(dialog).findByRole("alert"); const alert = await within(dialog).findByRole("alert");
expect(alert.textContent).toBe("That code is invalid or expired — request a fresh one and try again."); expect(alert.textContent).toBe("The binding code is invalid or expired. Obtain a new code.");
expect(mocks.passkeyDelete).toHaveBeenCalledTimes(1); expect(mocks.passkeyDelete).toHaveBeenCalledTimes(1);
}); });
@@ -249,7 +249,7 @@ describe("Account re-authentication", () => {
await proveByEmail(await checkDialog()); await proveByEmail(await checkDialog());
const status = await within(screen.getByRole("dialog", { name: "Add Passkey" })).findByRole("status"); const status = await within(screen.getByRole("dialog", { name: "Add Passkey" })).findByRole("status");
expect(status.textContent).toBe("Confirmed. Press Continue to add the passkey."); expect(status.textContent).toBe("Identity verified. Select Continue to add a Passkey.");
expect(mocks.passkeyRegisterBegin).toHaveBeenCalledTimes(1); expect(mocks.passkeyRegisterBegin).toHaveBeenCalledTimes(1);
expect(mocks.credentialsCreate).not.toHaveBeenCalled(); expect(mocks.credentialsCreate).not.toHaveBeenCalled();
expect(within(screen.getByRole("dialog", { name: "Add Passkey" })).getByRole("button", { name: "Continue" })).toHaveProperty( expect(within(screen.getByRole("dialog", { name: "Add Passkey" })).getByRole("button", { name: "Continue" })).toHaveProperty(
@@ -268,7 +268,7 @@ describe("Account re-authentication", () => {
await userEvent.click(await screen.findByRole("button", { name: "Change email" })); await userEvent.click(await screen.findByRole("button", { name: "Change email" }));
expect( expect(
screen.getByText( screen.getByText(
"Enter the new address and we'll send it a code. Once it's verified, sign-in codes go there, the old address gets a notice, and your other devices are signed out.", "Enter the new email address to receive a verification code. After verification, sign-in codes will be sent to the new address, a change notification will be sent to the previous address, and sessions on other devices will be signed out.",
), ),
).toBeTruthy(); ).toBeTruthy();
const address = screen.getByPlaceholderText("[email protected]"); const address = screen.getByPlaceholderText("[email protected]");
+10 -10
View File
@@ -89,18 +89,18 @@ describe("AccountSessionsCard", () => {
expect(within(phone).queryByText("This device")).toBeNull(); expect(within(phone).queryByText("This device")).toBeNull();
expect(within(phone).getByText("Active 20 hours ago")).toBeTruthy(); expect(within(phone).getByText("Active 20 hours ago")).toBeTruthy();
expect(within(phone).getByText("203.0.113.24")).toBeTruthy(); expect(within(phone).getByText("203.0.113.24")).toBeTruthy();
expect(within(phone).getByRole("button", { name: "Sign out Safari on iPhone" })).toBeTruthy(); expect(within(phone).getByRole("button", { name: "Sign out the session on Safari on iPhone" })).toBeTruthy();
// Seen a minute ago: the server records activity once a minute, so that is now. // Seen a minute ago: the server records activity once a minute, so that is now.
expect(within(row("Edge on Windows")).getByText("Active now")).toBeTruthy(); expect(within(row("Edge on Windows")).getByText("Active now")).toBeTruthy();
expect(screen.queryByText("Operator sessions sign out on their own after 30 minutes without activity.")).toBeNull(); expect(screen.queryByText("Operations account sessions expire after 30 minutes of inactivity.")).toBeNull();
}); });
it("tells an operator that their sessions idle out", async () => { it("tells an operator that their sessions idle out", async () => {
mocks.listMySessions.mockResolvedValue([mac]); mocks.listMySessions.mockResolvedValue([mac]);
renderCard({ staff: true }); renderCard({ staff: true });
expect( expect(
await screen.findByText("Operator sessions sign out on their own after 30 minutes without activity."), await screen.findByText("Operations account sessions expire after 30 minutes of inactivity."),
).toBeTruthy(); ).toBeTruthy();
}); });
@@ -109,7 +109,7 @@ describe("AccountSessionsCard", () => {
mocks.revokeMySession.mockResolvedValue({ ok: true, signed_out: false }); mocks.revokeMySession.mockResolvedValue({ ok: true, signed_out: false });
renderCard(); renderCard();
await userEvent.click(await screen.findByRole("button", { name: "Sign out Safari on iPhone" })); await userEvent.click(await screen.findByRole("button", { name: "Sign out the session on Safari on iPhone" }));
expect(mocks.revokeMySession).toHaveBeenCalledWith("h-iphone"); expect(mocks.revokeMySession).toHaveBeenCalledWith("h-iphone");
expect((await screen.findByRole("status")).textContent).toBe("Signed out Safari on iPhone."); expect((await screen.findByRole("status")).textContent).toBe("Signed out Safari on iPhone.");
@@ -122,7 +122,7 @@ describe("AccountSessionsCard", () => {
mocks.revokeMySession.mockRejectedValue({ status: 404, code: "session_not_found", message: "gone" }); mocks.revokeMySession.mockRejectedValue({ status: 404, code: "session_not_found", message: "gone" });
renderCard(); renderCard();
await userEvent.click(await screen.findByRole("button", { name: "Sign out Safari on iPhone" })); await userEvent.click(await screen.findByRole("button", { name: "Sign out the session on Safari on iPhone" }));
expect((await screen.findByRole("status")).textContent).toBe("Signed out Safari on iPhone."); expect((await screen.findByRole("status")).textContent).toBe("Signed out Safari on iPhone.");
expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByRole("alert")).toBeNull();
@@ -134,10 +134,10 @@ describe("AccountSessionsCard", () => {
mocks.revokeMySession.mockRejectedValue({ status: 403, code: "self_protected", message: "no" }); mocks.revokeMySession.mockRejectedValue({ status: 403, code: "self_protected", message: "no" });
renderCard(); renderCard();
await userEvent.click(await screen.findByRole("button", { name: "Sign out Safari on iPhone" })); await userEvent.click(await screen.findByRole("button", { name: "Sign out the session on Safari on iPhone" }));
expect((await screen.findByRole("alert")).textContent).toBe( expect((await screen.findByRole("alert")).textContent).toBe(
"You can't do that to the account you're signed in with.", "This operation is not permitted on the signed-in account.",
); );
expect(screen.queryByRole("status")).toBeNull(); expect(screen.queryByRole("status")).toBeNull();
await waitFor(() => expect(mocks.listMySessions).toHaveBeenCalledTimes(2)); await waitFor(() => expect(mocks.listMySessions).toHaveBeenCalledTimes(2));
@@ -151,13 +151,13 @@ describe("AccountSessionsCard", () => {
await userEvent.click(await screen.findByRole("button", { name: "Sign out other devices" })); await userEvent.click(await screen.findByRole("button", { name: "Sign out other devices" }));
const dialog = screen.getByRole("dialog"); const dialog = screen.getByRole("dialog");
expect(within(dialog).getByText("Sign out every other device?")).toBeTruthy(); expect(within(dialog).getByText("Sign out all other device sessions?")).toBeTruthy();
expect(mocks.revokeMyOtherSessions).not.toHaveBeenCalled(); expect(mocks.revokeMyOtherSessions).not.toHaveBeenCalled();
await userEvent.click(within(dialog).getByRole("button", { name: "Sign out other devices" })); await userEvent.click(within(dialog).getByRole("button", { name: "Sign out other devices" }));
expect(mocks.revokeMyOtherSessions).toHaveBeenCalledTimes(1); expect(mocks.revokeMyOtherSessions).toHaveBeenCalledTimes(1);
expect((await screen.findByRole("status")).textContent).toBe("Signed out 2 other devices."); expect((await screen.findByRole("status")).textContent).toBe("Sessions on 2 other devices have been signed out.");
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
await waitFor(() => expect(screen.queryByText("Safari on iPhone")).toBeNull()); await waitFor(() => expect(screen.queryByText("Safari on iPhone")).toBeNull());
expect(screen.getByText("Chrome on macOS")).toBeTruthy(); expect(screen.getByText("Chrome on macOS")).toBeTruthy();
@@ -171,7 +171,7 @@ describe("AccountSessionsCard", () => {
await userEvent.click(await screen.findByRole("button", { name: "Sign out other devices" })); await userEvent.click(await screen.findByRole("button", { name: "Sign out other devices" }));
await userEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Sign out other devices" })); await userEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Sign out other devices" }));
expect((await screen.findByRole("status")).textContent).toBe("Signed out 1 other device."); expect((await screen.findByRole("status")).textContent).toBe("The session on 1 other device has been signed out.");
}); });
it("has nothing to sign out elsewhere when this is the only device", async () => { it("has nothing to sign out elsewhere when this is the only device", async () => {
+2 -2
View File
@@ -112,13 +112,13 @@ describe("Login", () => {
await renderLogin(); await renderLogin();
await userEvent.click(screen.getByRole("button", { name: t("auth:passkey_btn") })); await userEvent.click(screen.getByRole("button", { name: t("auth:passkey_btn") }));
expect((await screen.findByRole("alert")).textContent).toBe("The browser returned no passkey. Try again."); expect((await screen.findByRole("alert")).textContent).toBe("The browser returned no Passkey credential. Retry.");
expect(calls.authPasskeyDiscoverableFinish).not.toHaveBeenCalled(); expect(calls.authPasskeyDiscoverableFinish).not.toHaveBeenCalled();
await userEvent.type(screen.getByLabelText(t("auth:email_address")), "[email protected]"); await userEvent.type(screen.getByLabelText(t("auth:email_address")), "[email protected]");
await userEvent.click(screen.getByRole("button", { name: t("auth:passkey_btn") })); await userEvent.click(screen.getByRole("button", { name: t("auth:passkey_btn") }));
await vi.waitFor(() => expect(calls.authPasskeyLoginBegin).toHaveBeenCalledWith("[email protected]")); await vi.waitFor(() => expect(calls.authPasskeyLoginBegin).toHaveBeenCalledWith("[email protected]"));
expect((await screen.findByRole("alert")).textContent).toBe("The browser returned no passkey. Try again."); expect((await screen.findByRole("alert")).textContent).toBe("The browser returned no Passkey credential. Retry.");
expect(calls.authPasskeyLoginFinish).not.toHaveBeenCalled(); expect(calls.authPasskeyLoginFinish).not.toHaveBeenCalled();
}); });
}); });
+4 -4
View File
@@ -162,12 +162,12 @@ describe("MySubmissionsPage", () => {
expect( expect(
await screen.findByText( await screen.findByText(
"Can't reach Felis: the network is down, or your Cloudflare Access sign-in expired. Reload the page to sign in again.", "Unable to connect to Felis. The network may be unavailable or the Cloudflare Access session may have expired. Check the network. If the Access session has expired, reload the page and sign in again.",
), ),
).toBeTruthy(); ).toBeTruthy();
expect( expect(
screen.getByText( screen.getByText(
"“Skyblock Pack” is saved and 2 KB of its build context is on the server. Submit again to continue from there.", "“Skyblock Pack” is saved and the server has received 2 KB of its build context. Resubmission resumes from the confirmed offset.",
), ),
).toBeTruthy(); ).toBeTruthy();
expect((screen.getByLabelText(/Display Name/) as HTMLInputElement).disabled).toBe(true); expect((screen.getByLabelText(/Display Name/) as HTMLInputElement).disabled).toBe(true);
@@ -197,7 +197,7 @@ describe("MySubmissionsPage", () => {
const other = new File(["\x1f\x8bother"], "other.tar.gz", { type: "application/gzip" }); const other = new File(["\x1f\x8bother"], "other.tar.gz", { type: "application/gzip" });
await userEvent.upload(screen.getByLabelText(/Build Context/), other); await userEvent.upload(screen.getByLabelText(/Build Context/), other);
expect(screen.getByText("“Skyblock Pack” is saved. The file you picked uploads from the start.")).toBeTruthy(); expect(screen.getByText("“Skyblock Pack” is saved. A newly selected file is uploaded from the beginning.")).toBeTruthy();
upload.uploadContext.mockResolvedValueOnce(undefined); upload.uploadContext.mockResolvedValueOnce(undefined);
await userEvent.click(screen.getByRole("button", { name: "Submit" })); await userEvent.click(screen.getByRole("button", { name: "Submit" }));
await vi.waitFor(() => expect(upload.uploadContext).toHaveBeenCalledTimes(2)); await vi.waitFor(() => expect(upload.uploadContext).toHaveBeenCalledTimes(2));
@@ -222,7 +222,7 @@ describe("MySubmissionsPage", () => {
expect( expect(
await screen.findByText( await screen.findByText(
"“Skyblock Pack” is saved and 1 KB of its build context is on the server. Submit again to continue from there.", "“Skyblock Pack” is saved and the server has received 1 KB of its build context. Resubmission resumes from the confirmed offset.",
), ),
).toBeTruthy(); ).toBeTruthy();
expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByRole("alert")).toBeNull();
+1 -1
View File
@@ -228,7 +228,7 @@ describe("ServerBackups back up now", () => {
renderPage(); renderPage();
await screen.findByText("Page 1 of 3"); await screen.findByText("Page 1 of 3");
await vi.waitFor(() => expect(backUp().disabled).toBe(true)); await vi.waitFor(() => expect(backUp().disabled).toBe(true));
expect(backUp().title).toBe("A backup, restore or world export is already running; back up once it finishes."); expect(backUp().title).toBe("A backup, restoration, or world export is in progress. Wait for the current operation to complete before starting a backup.");
}); });
it("keeps the page when a reread fails, and says the status may be stale", async () => { it("keeps the page when a reread fails, and says the status may be stale", async () => {
+4 -4
View File
@@ -59,7 +59,7 @@ describe("ServerConsole host recovery", () => {
tier.isOwner = true; tier.isOwner = true;
calls.status.mockRejectedValue(new Error("API offline")); calls.status.mockRejectedValue(new Error("API offline"));
renderConsole(); renderConsole();
expect(await screen.findByText("Control API unreachable? Show host recovery steps")).toBeTruthy(); expect(await screen.findByText("Host emergency shutdown procedure")).toBeTruthy();
expect(screen.getByText(/kubectl patch minecraftserver survival/)).toBeTruthy(); expect(screen.getByText(/kubectl patch minecraftserver survival/)).toBeTruthy();
expect(screen.queryByRole("button", { name: "Stop" })).toBeNull(); expect(screen.queryByRole("button", { name: "Stop" })).toBeNull();
tier.isOwner = false; tier.isOwner = false;
@@ -235,7 +235,7 @@ describe("ServerConsole retirement", () => {
calls.status.mockResolvedValue(stopped({ retiring: { requested_at: new Date().toISOString(), delete: false } })); calls.status.mockResolvedValue(stopped({ retiring: { requested_at: new Date().toISOString(), delete: false } }));
renderConsole(); renderConsole();
expect(await screen.findByText("This server has been given up")).toBeTruthy(); expect(await screen.findByText("Server pending reclamation")).toBeTruthy();
expect(screen.getByRole("button", { name: "Cancel request" })).toBeTruthy(); expect(screen.getByRole("button", { name: "Cancel request" })).toBeTruthy();
expect(screen.getByText("Given up")).toBeTruthy(); expect(screen.getByText("Given up")).toBeTruthy();
expect(screen.queryByRole("button", { name: "Wake" })).toBeNull(); expect(screen.queryByRole("button", { name: "Wake" })).toBeNull();
@@ -350,13 +350,13 @@ describe("startup diagnostics and stale state", () => {
expect(await screen.findByText("Startup issue")).toBeTruthy(); expect(await screen.findByText("Startup issue")).toBeTruthy();
expect(screen.getByText(/OOMKilled.*137/)).toBeTruthy(); expect(screen.getByText(/OOMKilled.*137/)).toBeTruthy();
expect(screen.queryByText("Starting", { exact: true })).toBeNull(); expect(screen.queryByText("Starting", { exact: true })).toBeNull();
expect(screen.getByText("Logs are not available yet")).toBeTruthy(); expect(screen.getByText("Logs unavailable")).toBeTruthy();
}); });
it("explains scheduling failure before logs exist", async () => { it("explains scheduling failure before logs exist", async () => {
calls.status.mockResolvedValue(status({ phase: "Starting", desiredState: "Running", startup: { stage: "scheduling", reason: "Unschedulable", message: "0/1 nodes: Insufficient memory", startedAt: new Date().toISOString(), logsAvailable: false } })); calls.status.mockResolvedValue(status({ phase: "Starting", desiredState: "Running", startup: { stage: "scheduling", reason: "Unschedulable", message: "0/1 nodes: Insufficient memory", startedAt: new Date().toISOString(), logsAvailable: false } }));
renderConsole(); renderConsole();
expect(await screen.findByText("Waiting for resources and scheduling")).toBeTruthy(); expect(await screen.findByText("Waiting for resources and scheduling")).toBeTruthy();
expect(screen.getByText(/Not enough schedulable memory/)).toBeTruthy(); expect(screen.getByText(/Insufficient schedulable memory/)).toBeTruthy();
expect(screen.queryByTestId("log-stream")).toBeNull(); expect(screen.queryByTestId("log-stream")).toBeNull();
expect(screen.queryByRole("button", { name: "Delete server" })).toBeNull(); expect(screen.queryByRole("button", { name: "Delete server" })).toBeNull();
expect(screen.getByRole("button", { name: "More actions" })).toBeTruthy(); expect(screen.getByRole("button", { name: "More actions" })).toBeTruthy();
+10 -10
View File
@@ -66,7 +66,7 @@ describe("ServerLuckPerms without LuckPerms", () => {
await userEvent.type(await screen.findByPlaceholderText("Steve"), "Alex{Enter}"); await userEvent.type(await screen.findByPlaceholderText("Steve"), "Alex{Enter}");
const alert = await screen.findByRole("alert"); const alert = await screen.findByRole("alert");
expect(alert.textContent).toMatch(/LuckPerms isn't installed/); expect(alert.textContent).toMatch(/LuckPerms is not installed/);
expect(calls.accessLuckPermsInfo).toHaveBeenCalledWith("lobby", "Alex"); expect(calls.accessLuckPermsInfo).toHaveBeenCalledWith("lobby", "Alex");
await userEvent.type(screen.getByLabelText("Group Name"), "vip"); await userEvent.type(screen.getByLabelText("Group Name"), "vip");
@@ -74,7 +74,7 @@ describe("ServerLuckPerms without LuckPerms", () => {
expect(calls.accessGroup).toHaveBeenCalledWith("lobby", "add", "Alex", "vip"); expect(calls.accessGroup).toHaveBeenCalledWith("lobby", "add", "Alex", "vip");
const alerts = await screen.findAllByRole("alert"); const alerts = await screen.findAllByRole("alert");
expect(alerts).toHaveLength(2); expect(alerts).toHaveLength(2);
expect(alerts.every((a) => /LuckPerms isn't installed/.test(a.textContent ?? ""))).toBe(true); expect(alerts.every((a) => /LuckPerms is not installed/.test(a.textContent ?? ""))).toBe(true);
expect(screen.queryByText(/success/i)).toBeNull(); expect(screen.queryByText(/success/i)).toBeNull();
}); });
}); });
@@ -126,12 +126,12 @@ describe("ServerLuckPerms when LuckPerms does not answer", () => {
async function lookUpAlex() { async function lookUpAlex() {
renderPage(); renderPage();
await userEvent.type(await screen.findByPlaceholderText("Steve"), "Alex{Enter}"); await userEvent.type(await screen.findByPlaceholderText("Steve"), "Alex{Enter}");
await screen.findByText(/Couldn't read this player's groups/); await screen.findByText(/The player’s current groups could not be read/);
} }
it("says the lists are unread, never that they are empty", async () => { it("says the lists are unread, never that they are empty", async () => {
await lookUpAlex(); await lookUpAlex();
expect(screen.getByText(/Couldn't read this player's permission nodes/)).toBeTruthy(); expect(screen.getByText(/The player’s current permission nodes could not be read/)).toBeTruthy();
expect(screen.queryByText("No parent groups assigned")).toBeNull(); expect(screen.queryByText("No parent groups assigned")).toBeNull();
expect(screen.queryByText("No explicit permission nodes assigned")).toBeNull(); expect(screen.queryByText("No explicit permission nodes assigned")).toBeNull();
}); });
@@ -169,7 +169,7 @@ describe("ServerLuckPerms when LuckPerms does not answer", () => {
await lookUpAlex(); await lookUpAlex();
await userEvent.type(screen.getByLabelText("Permission Node"), "essentials.fly"); await userEvent.type(screen.getByLabelText("Permission Node"), "essentials.fly");
await userEvent.click(screen.getByRole("button", { name: /^remove permission$/i })); await userEvent.click(screen.getByRole("button", { name: /^remove permission$/i }));
expect((await screen.findByRole("alert")).textContent).toMatch(/LuckPerms isn't installed/); expect((await screen.findByRole("alert")).textContent).toMatch(/LuckPerms is not installed/);
expect((screen.getByLabelText("Permission Node") as HTMLInputElement).value).toBe("essentials.fly"); expect((screen.getByLabelText("Permission Node") as HTMLInputElement).value).toBe("essentials.fly");
}); });
@@ -179,7 +179,7 @@ describe("ServerLuckPerms when LuckPerms does not answer", () => {
await userEvent.type(await screen.findByPlaceholderText("Steve"), "Alex{Enter}"); await userEvent.type(await screen.findByPlaceholderText("Steve"), "Alex{Enter}");
expect(await screen.findByText("No parent groups assigned")).toBeTruthy(); expect(await screen.findByText("No parent groups assigned")).toBeTruthy();
expect(screen.getByText("No explicit permission nodes assigned")).toBeTruthy(); expect(screen.getByText("No explicit permission nodes assigned")).toBeTruthy();
expect(screen.queryByText(/Couldn't read this player's/)).toBeNull(); expect(screen.queryByText(/The player’s current/)).toBeNull();
}); });
}); });
@@ -259,17 +259,17 @@ describe("ServerLuckPerms reverting a permission change", () => {
reason: humanizeError({ status: 502, code: "rcon_unavailable", message: "console down" }), reason: humanizeError({ status: 502, code: "rcon_unavailable", message: "console down" }),
}), }),
); );
expect(alert.textContent?.startsWith("Couldn't revert: ")).toBe(true); expect(alert.textContent?.startsWith("Unable to revert: ")).toBe(true);
expect(screen.getByRole("button", { name: "Revert" })).toBeTruthy(); expect(screen.getByRole("button", { name: "Revert" })).toBeTruthy();
}); });
it("offers no revert for a removal whose value was never read", async () => { it("offers no revert for a removal whose value was never read", async () => {
calls.accessLuckPermsInfo.mockResolvedValue({ player: "Alex", groups: [], permissions: [], output: "" }); calls.accessLuckPermsInfo.mockResolvedValue({ player: "Alex", groups: [], permissions: [], output: "" });
await lookUpAlex(); await lookUpAlex();
await screen.findByText(/Couldn't read this player's permission nodes/); await screen.findByText(/The player’s current permission nodes could not be read/);
await typeNode("essentials.fly", "world_nether"); await typeNode("essentials.fly", "world_nether");
await userEvent.click(screen.getByRole("button", { name: /^remove permission$/i })); await userEvent.click(screen.getByRole("button", { name: /^remove permission$/i }));
expect(await screen.findByText("Can't revert: value unknown")).toBeTruthy(); expect(await screen.findByText("Cannot revert: previous value unknown")).toBeTruthy();
expect(screen.getByText("- essentials.fly [world_nether]")).toBeTruthy(); expect(screen.getByText("- essentials.fly [world_nether]")).toBeTruthy();
expect(screen.queryByRole("button", { name: "Revert" })).toBeNull(); expect(screen.queryByRole("button", { name: "Revert" })).toBeNull();
@@ -290,7 +290,7 @@ describe("ServerLuckPerms reverting a permission change", () => {
await typeNode("essentials.fly", "world_nether"); await typeNode("essentials.fly", "world_nether");
await userEvent.click(screen.getByRole("button", { name: /^remove permission$/i })); await userEvent.click(screen.getByRole("button", { name: /^remove permission$/i }));
expect(calls.accessPermission).toHaveBeenLastCalledWith("lobby", "unset", "Alex", "essentials.fly", undefined, "world_nether"); expect(calls.accessPermission).toHaveBeenLastCalledWith("lobby", "unset", "Alex", "essentials.fly", undefined, "world_nether");
expect(await screen.findByText("Can't revert: value unknown")).toBeTruthy(); expect(await screen.findByText("Cannot revert: previous value unknown")).toBeTruthy();
expect(screen.getByText("- essentials.fly [world_nether]")).toBeTruthy(); expect(screen.getByText("- essentials.fly [world_nether]")).toBeTruthy();
} }
+8 -8
View File
@@ -169,9 +169,9 @@ describe("ServerSchedules", () => {
const backup = await rowOf("Backup"); const backup = await rowOf("Backup");
expect(within(backup).getByText("Mon, Wed, Fri at 05:30")).toBeTruthy(); expect(within(backup).getByText("Mon, Wed, Fri at 05:30")).toBeTruthy();
expect(within(backup).getByText("Off: it runs only when you press Run now")).toBeTruthy(); expect(within(backup).getByText("Disabled; runs only when Run now is selected")).toBeTruthy();
expect(within(backup).getByText("Off")).toBeTruthy(); expect(within(backup).getByText("Off")).toBeTruthy();
expect(within(backup).getByText("Hasn't run yet")).toBeTruthy(); expect(within(backup).getByText("Not executed")).toBeTruthy();
expect(screen.getByText("3 of 20 tasks")).toBeTruthy(); expect(screen.getByText("3 of 20 tasks")).toBeTruthy();
}); });
@@ -210,7 +210,7 @@ describe("ServerSchedules", () => {
expect(within(nightly).getByText("Skipped")).toBeTruthy(); expect(within(nightly).getByText("Skipped")).toBeTruthy();
expect( expect(
within(nightly).getByText( within(nightly).getByText(
"The server changed owner, so this task switched itself off. Check it, then save it or switch it on to use it again.", "The server owner has changed and the task was disabled automatically. Verify the configuration before saving or enabling it again.",
), ),
).toBeTruthy(); ).toBeTruthy();
expect(within(await rowOf("Who")).getByText("Reply: There are 0 of a max of 20 players online")).toBeTruthy(); expect(within(await rowOf("Who")).getByText("Reply: There are 0 of a max of 20 players online")).toBeTruthy();
@@ -359,7 +359,7 @@ describe("ServerSchedules", () => {
const dialog = await openCreate(); const dialog = await openCreate();
await userEvent.click(within(dialog).getByRole("button", { name: "Create" })); await userEvent.click(within(dialog).getByRole("button", { name: "Create" }));
expect((await within(dialog).findByRole("alert")).textContent).toBe( expect((await within(dialog).findByRole("alert")).textContent).toBe(
"This server already has as many scheduled tasks as it can hold. Delete one first.", "The server’s scheduled-task limit has been reached. Remove an existing task before retrying.",
); );
expect(screen.getByRole("dialog")).toBe(dialog); expect(screen.getByRole("dialog")).toBe(dialog);
expect((within(dialog).getByRole("button", { name: "Create" }) as HTMLButtonElement).disabled).toBe(false); expect((within(dialog).getByRole("button", { name: "Create" }) as HTMLButtonElement).disabled).toBe(false);
@@ -370,7 +370,7 @@ describe("ServerSchedules", () => {
renderPage(); renderPage();
await userEvent.click(await screen.findByRole("button", { name: "Back up every night at 05:00" })); await userEvent.click(await screen.findByRole("button", { name: "Back up every night at 05:00" }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText(/A backup of a running server stops it/)).toBeTruthy(); expect(within(dialog).getByText(/Backing up a running server first stops it/)).toBeTruthy();
await userEvent.click(within(dialog).getByRole("button", { name: "Create" })); await userEvent.click(within(dialog).getByRole("button", { name: "Create" }));
expect(calls.createSchedule).toHaveBeenCalledWith("survival", { expect(calls.createSchedule).toHaveBeenCalledWith("survival", {
label: "", label: "",
@@ -483,11 +483,11 @@ describe("ServerSchedules", () => {
const busy = await rowOf("Busy"); const busy = await rowOf("Busy");
expect(within(busy).getByText("Stopping the server…")).toBeTruthy(); expect(within(busy).getByText("Stopping the server…")).toBeTruthy();
expect(within(busy).getByText("Started 5 minutes ago")).toBeTruthy(); expect(within(busy).getByText("Started 5 minutes ago")).toBeTruthy();
expect(within(busy).queryByText("Hasn't run yet")).toBeNull(); expect(within(busy).queryByText("Not executed")).toBeNull();
for (const name of ["Run now", "Edit", "Delete"]) { for (const name of ["Run now", "Edit", "Delete"]) {
const button = within(busy).getByRole("button", { name }) as HTMLButtonElement; const button = within(busy).getByRole("button", { name }) as HTMLButtonElement;
expect(button.disabled).toBe(true); expect(button.disabled).toBe(true);
expect(button.parentElement?.title).toBe("This task is running. Wait for the run to finish."); expect(button.parentElement?.title).toBe("The task is running. Wait for the current execution to complete.");
} }
expect((within(busy).getByRole("switch") as HTMLButtonElement).disabled).toBe(true); expect((within(busy).getByRole("switch") as HTMLButtonElement).disabled).toBe(true);
@@ -504,7 +504,7 @@ describe("ServerSchedules", () => {
const add = (await screen.findByRole("button", { name: "New task" })) as HTMLButtonElement; const add = (await screen.findByRole("button", { name: "New task" })) as HTMLButtonElement;
await rowOf("One"); await rowOf("One");
expect(add.disabled).toBe(true); expect(add.disabled).toBe(true);
expect(add.parentElement?.title).toBe("A server holds at most 2 tasks. Delete one to add another."); expect(add.parentElement?.title).toBe("The scheduled-task limit of 2 has been reached. Remove an existing task before adding another.");
}); });
it("says plainly when felis-api has no schedule store, and treats other failures as errors", async () => { it("says plainly when felis-api has no schedule store, and treats other failures as errors", async () => {
@@ -129,7 +129,7 @@ describe("AuthSourcesPage", () => {
calls.getAuthSources.mockRejectedValueOnce({ status: 503, code: "auth_sources_unavailable" }); calls.getAuthSources.mockRejectedValueOnce({ status: 503, code: "auth_sources_unavailable" });
page(); await screen.findByRole("alert"); page(); await screen.findByRole("alert");
expect(field("Permanent source ID").disabled).toBe(true); expect(field("Permanent source ID").disabled).toBe(true);
await userEvent.click(button("Try again")); await userEvent.click(button("Retry"));
await ready(); await ready();
expect(field("Permanent source ID").value).toBe("littleskin"); expect(field("Permanent source ID").value).toBe("littleskin");
}); });
@@ -100,7 +100,7 @@ describe("BuildScanPanel", () => {
"Listed in [registry] scan_accept as a known risk, so it never blocks.", "Listed in [registry] scan_accept as a known risk, so it never blocks.",
); );
const unfixed = within(panel).getByText("CVE-2024-0002").closest("li")!; const unfixed = within(panel).getByText("CVE-2024-0002").closest("li")!;
expect(unfixed.textContent).toBe("CVE-2024-0002HIGHopenssl3.0.13 → no fix yetusr/lib/libssl.so.3"); expect(unfixed.textContent).toBe("CVE-2024-0002HIGHopenssl3.0.13 → No fix availableusr/lib/libssl.so.3");
expect(within(panel).getByText("Showing 4 of 6 findings. The full report lists every one.")).toBeTruthy(); expect(within(panel).getByText("Showing 4 of 6 findings. The full report lists every one.")).toBeTruthy();
}); });
@@ -112,7 +112,7 @@ describe("BuildScanPanel", () => {
const sbom = within(panel).getByRole("button", { name: "SBOM" }) as HTMLButtonElement; const sbom = within(panel).getByRole("button", { name: "SBOM" }) as HTMLButtonElement;
expect(sbom.disabled).toBe(true); expect(sbom.disabled).toBe(true);
expect(sbom.getAttribute("title")).toBe( expect(sbom.getAttribute("title")).toBe(
"Not kept with this build: the file was too large, or the step that writes it failed.", "The file was not retained for this build. It may have exceeded the size limit or its generation step may have failed.",
); );
await userEvent.click(within(panel).getByRole("button", { name: "Trivy report" })); await userEvent.click(within(panel).getByRole("button", { name: "Trivy report" }));
expect(calls.downloadBuildScanDocument).toHaveBeenCalledWith("bld-7", "report"); expect(calls.downloadBuildScanDocument).toHaveBeenCalledWith("bld-7", "report");
@@ -124,7 +124,7 @@ describe("BuildScanPanel", () => {
}); });
await userEvent.click(within(panel).getByRole("button", { name: "Trivy report" })); await userEvent.click(within(panel).getByRole("button", { name: "Trivy report" }));
expect((await within(panel).findByRole("alert")).textContent).toBe( expect((await within(panel).findByRole("alert")).textContent).toBe(
"Couldn't download: This build kept no copy of that scan file: it was too large to keep, or the step that writes it failed.", "Unable to download: The scan document was not retained for this build. The file may have exceeded the size limit or its generation step may have failed.",
); );
}); });
@@ -135,7 +135,7 @@ describe("BuildScanPanel", () => {
expect(within(panel).getByText("Passed")).toBeTruthy(); expect(within(panel).getByText("Passed")).toBeTruthy();
expect(panel.textContent).toContain("· 1 package · Blocks on HIGH, including vulnerabilities with no fixed release"); expect(panel.textContent).toContain("· 1 package · Blocks on HIGH, including vulnerabilities with no fixed release");
expect(panel.textContent).not.toContain("accepted"); expect(panel.textContent).not.toContain("accepted");
expect(within(panel).getByText("Trivy found nothing to report in this image.")).toBeTruthy(); expect(within(panel).getByText("Trivy detected no findings in this image.")).toBeTruthy();
expect(within(panel).queryByText("Found in")).toBeNull(); expect(within(panel).queryByText("Found in")).toBeNull();
calls.downloadBuildScanDocument.mockResolvedValue(undefined); calls.downloadBuildScanDocument.mockResolvedValue(undefined);
await userEvent.click(within(panel).getByRole("button", { name: "SBOM" })); await userEvent.click(within(panel).getByRole("button", { name: "SBOM" }));
@@ -147,7 +147,7 @@ describe("BuildScanPanel", () => {
const { unmount } = render(<BuildScanPanel build={FAILED} />); const { unmount } = render(<BuildScanPanel build={FAILED} />);
expect( expect(
await screen.findByText( await screen.findByText(
"No security scan was kept for this build. It stopped before the scan step, or it ran before builds kept their scans.", "This build has no retained security scan record. It may have ended before scanning or been created before scan retention was enabled.",
), ),
).toBeTruthy(); ).toBeTruthy();
unmount(); unmount();
@@ -156,9 +156,9 @@ describe("BuildScanPanel", () => {
calls.getBuildScan.mockResolvedValueOnce(BLOCKED); calls.getBuildScan.mockResolvedValueOnce(BLOCKED);
render(<BuildScanPanel build={FAILED} />); render(<BuildScanPanel build={FAILED} />);
expect((await screen.findByRole("alert")).textContent).toBe( expect((await screen.findByRole("alert")).textContent).toBe(
"Couldn't load the security scan: The operation failed because of an internal server error. Ask an admin to check the logs.", "Unable to load the security scan: The operation failed because of an internal server error. Ask an admin to check the logs.",
); );
await userEvent.click(screen.getByRole("button", { name: "Try again" })); await userEvent.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByRole("region", { name: "Security scan" })).toBeTruthy(); expect(await screen.findByRole("region", { name: "Security scan" })).toBeTruthy();
expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByRole("alert")).toBeNull();
}); });
+5 -5
View File
@@ -54,7 +54,7 @@ describe("DBBackupCard", () => {
expect(await screen.findByText("Incomplete")).toBeTruthy(); expect(await screen.findByText("Incomplete")).toBeTruthy();
expect(screen.queryByText("Healthy")).toBeNull(); expect(screen.queryByText("Healthy")).toBeNull();
expect(screen.getByText("The newest backup lacks the server definitions")).toBeTruthy(); expect(screen.getByText("The newest backup lacks the server definitions")).toBeTruthy();
expect(screen.getByText(/Restoring from it brings back accounts and the database, but no servers/)).toBeTruthy(); expect(screen.getByText(/Restoring the database alone cannot recreate servers/)).toBeTruthy();
expect(screen.getByText(REASON)).toBeTruthy(); expect(screen.getByText(REASON)).toBeTruthy();
expect(screen.getByTitle("sudo k3s kubectl get minecraftservers -A")).toBeTruthy(); expect(screen.getByTitle("sudo k3s kubectl get minecraftservers -A")).toBeTruthy();
expect(screen.getByTitle("sudo felis db backup")).toBeTruthy(); expect(screen.getByTitle("sudo felis db backup")).toBeTruthy();
@@ -74,7 +74,7 @@ describe("DBBackupCard", () => {
render(<DBBackupCard />); render(<DBBackupCard />);
expect(await screen.findByText("Overdue")).toBeTruthy(); expect(await screen.findByText("Overdue")).toBeTruthy();
expect(screen.getByText("The newest backup is more than 26 hours old")).toBeTruthy(); expect(screen.getByText("The newest backup is more than 26 hours old")).toBeTruthy();
expect(screen.getByText(/find out why the timer missed it:$/)).toBeTruthy(); expect(screen.getByText(/inspect the timer logs:$/)).toBeTruthy();
expect(screen.getByText("yesterday").className).toBe("text-destructive"); expect(screen.getByText("yesterday").className).toBe("text-destructive");
expect(screen.getByTitle("sudo systemctl start felis-db-backup.service")).toBeTruthy(); expect(screen.getByTitle("sudo systemctl start felis-db-backup.service")).toBeTruthy();
}); });
@@ -87,7 +87,7 @@ describe("DBBackupCard", () => {
render(<DBBackupCard />); render(<DBBackupCard />);
expect(await screen.findByText("Overdue")).toBeTruthy(); expect(await screen.findByText("Overdue")).toBeTruthy();
expect(screen.getByText("The daily backup last completed 2 days ago, past the 26-hour limit")).toBeTruthy(); expect(screen.getByText("The daily backup last completed 2 days ago, past the 26-hour limit")).toBeTruthy();
expect(screen.getByText(/^The newer backup above was taken by hand or by another job and can be restored from/)).toBeTruthy(); expect(screen.getByText(/^The newer backup was created manually or by another task and is available for restoration/)).toBeTruthy();
// The manual bundle itself is fresh: its age stays out of the alarm. // The manual bundle itself is fresh: its age stays out of the alarm.
expect(screen.getByText("3 hours ago").className).toBe(""); expect(screen.getByText("3 hours ago").className).toBe("");
expect(screen.getByTitle("sudo systemctl start felis-db-backup.service")).toBeTruthy(); expect(screen.getByTitle("sudo systemctl start felis-db-backup.service")).toBeTruthy();
@@ -96,8 +96,8 @@ describe("DBBackupCard", () => {
it("says the daily timer never completed when only other kinds exist", async () => { it("says the daily timer never completed when only other kinds exist", async () => {
calls.getDBBackup.mockResolvedValue(status({ label: "pre-migrate", name: "felis-db-20260926T101500Z-pre-migrate.tar" }, true)); calls.getDBBackup.mockResolvedValue(status({ label: "pre-migrate", name: "felis-db-20260926T101500Z-pre-migrate.tar" }, true));
render(<DBBackupCard />); render(<DBBackupCard />);
expect(await screen.findByText("The daily timer has not completed a backup yet")).toBeTruthy(); expect(await screen.findByText("The daily timer has not completed a backup")).toBeTruthy();
expect(screen.getByText(/^The newer backup above was taken by hand/)).toBeTruthy(); expect(screen.getByText(/^The newer backup was created manually/)).toBeTruthy();
}); });
it("names an off-site copy snapshot by its kind", async () => { it("names an off-site copy snapshot by its kind", async () => {
@@ -229,7 +229,7 @@ describe("ImageBuildPage cancel", () => {
calls.listBuilds.mockRejectedValue({ status: 409, code: "test", message: "database is away" }); calls.listBuilds.mockRejectedValue({ status: 409, code: "test", message: "database is away" });
await userEvent.click(within(dialog).getByRole("button", { name: t("admin:cancel_build_confirm") })); await userEvent.click(within(dialog).getByRole("button", { name: t("admin:cancel_build_confirm") }));
expect((await screen.findByRole("alert")).textContent).toBe("Couldn't refresh the build list: database is away"); expect((await screen.findByRole("alert")).textContent).toBe("Unable to refresh the build list: database is away");
expect(screen.getByText(RUNNING.image_ref)).toBeTruthy(); expect(screen.getByText(RUNNING.image_ref)).toBeTruthy();
expect(screen.getByText(MINE.image_ref)).toBeTruthy(); expect(screen.getByText(MINE.image_ref)).toBeTruthy();
}); });
+1 -1
View File
@@ -137,7 +137,7 @@ describe("LobbyPage", () => {
await userEvent.click(screen.getByRole("button", { name: "Save settings" })); await userEvent.click(screen.getByRole("button", { name: "Save settings" }));
expect(calls.writeServerFile).not.toHaveBeenCalled(); expect(calls.writeServerFile).not.toHaveBeenCalled();
calls.readServerFile.mockResolvedValue({ content: btoa('{"menuTitleEn":"Recovered title"}'), sha256: "hash" }); calls.readServerFile.mockResolvedValue({ content: btoa('{"menuTitleEn":"Recovered title"}'), sha256: "hash" });
await userEvent.click(screen.getByRole("button", { name: "Try again" })); await userEvent.click(screen.getByRole("button", { name: "Retry" }));
expect((await readyField("English menu title")).value).toBe("Recovered title"); expect((await readyField("English menu title")).value).toBe("Recovered title");
expect(screen.queryByRole("alert")).toBeNull(); expect(screen.queryByRole("alert")).toBeNull();
}); });
@@ -54,7 +54,7 @@ describe("UpdateReportCard", () => {
expect(rowText("k3s")).toBe("k3sInstalled: v1.36.2+k3s1Newer release: —Up to date"); expect(rowText("k3s")).toBe("k3sInstalled: v1.36.2+k3s1Newer release: —Up to date");
expect(rowText("jre")).toBe("jreInstalled: 21.0.8+9Newer release: —Feed unreachableadoptium: context deadline exceeded"); expect(rowText("jre")).toBe("jreInstalled: 21.0.8+9Newer release: —Feed unreachableadoptium: context deadline exceeded");
expect(rowText("postgresql")).toBe( expect(rowText("postgresql")).toBe(
"postgresqlInstalled: —Newer release: —Version unreadablepsql: not foundPostgreSQL 13 is past its end of life", "postgresqlInstalled: —Newer release: —Version read failedpsql: not foundPostgreSQL 13 is past its end of life",
); );
expect(screen.getByTitle("sudo felis update --panel --cloudflared").textContent).toBe("sudo felis update --panel --cloudflared"); expect(screen.getByTitle("sudo felis update --panel --cloudflared").textContent).toBe("sudo felis update --panel --cloudflared");
expect(screen.queryByText("sudo felis update --record")).toBeNull(); expect(screen.queryByText("sudo felis update --record")).toBeNull();
@@ -76,7 +76,7 @@ describe("UpdateReportCard", () => {
calls.getUpdateReport.mockResolvedValue({ report: null, stale: true, max_age_seconds: 93600 }); calls.getUpdateReport.mockResolvedValue({ report: null, stale: true, max_age_seconds: 93600 });
render(<UpdateReportCard />); render(<UpdateReportCard />);
expect(await screen.findByText("Never checked")).toBeTruthy(); expect(await screen.findByText("Never checked")).toBeTruthy();
expect(screen.getByText("No version check has been recorded yet")).toBeTruthy(); expect(screen.getByText("No version check records")).toBeTruthy();
expect(screen.getByTitle("sudo felis update --record")).toBeTruthy(); expect(screen.getByTitle("sudo felis update --record")).toBeTruthy();
expect(screen.getByTitle("journalctl -u felis-update-check -n 50 --no-pager")).toBeTruthy(); expect(screen.getByTitle("journalctl -u felis-update-check -n 50 --no-pager")).toBeTruthy();
}); });
@@ -140,7 +140,7 @@ describe("UserDetailPage", () => {
expect(calls.revokeUserSession).toHaveBeenCalledWith("u-1", "h-phone"); expect(calls.revokeUserSession).toHaveBeenCalledWith("u-1", "h-phone");
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
await waitFor(() => expect(screen.queryByText("Chrome on Android")).toBeNull()); await waitFor(() => expect(screen.queryByText("Chrome on Android")).toBeNull());
expect(screen.queryByText("That session has already ended.")).toBeNull(); expect(screen.queryByText("The session has ended or does not exist.")).toBeNull();
}); });
it("shows why a revoke failed for any other reason", async () => { it("shows why a revoke failed for any other reason", async () => {
@@ -178,9 +178,9 @@ describe("UserDetailPage", () => {
}); });
describe("danger zone for accounts the server protects", () => { describe("danger zone for accounts the server protects", () => {
const SELF_REASON = "You can't disable or delete the account you're signed in with."; const SELF_REASON = "The signed-in account cannot be disabled or deleted.";
const OWNER_REASON = const OWNER_REASON =
"The owner account can't be disabled or deleted from the panel. Only the host's break-glass console (sudo felis breakGlass) manages it."; "The Owner account cannot be disabled or deleted from the panel. Manage it through the host recovery console (sudo felis breakGlass).";
const button = (name: string) => screen.getByRole("button", { name }) as HTMLButtonElement; const button = (name: string) => screen.getByRole("button", { name }) as HTMLButtonElement;
const describedBy = (b: HTMLElement) => const describedBy = (b: HTMLElement) =>
@@ -197,7 +197,7 @@ describe("UserDetailPage", () => {
expect(button(name).parentElement?.getAttribute("title")).toBe(SELF_REASON); expect(button(name).parentElement?.getAttribute("title")).toBe(SELF_REASON);
} }
expect(button("Unbind passkeys").disabled).toBe(false); expect(button("Unbind passkeys").disabled).toBe(false);
expect(screen.getByText("You can't change your own role.")).toBeTruthy(); expect(screen.getByText("The current account’s role cannot be changed.")).toBeTruthy();
expect(screen.queryByRole("combobox")).toBeNull(); expect(screen.queryByRole("combobox")).toBeNull();
}); });
@@ -212,7 +212,7 @@ describe("UserDetailPage", () => {
expect(describedBy(button("Delete User"))).toBe(OWNER_REASON); expect(describedBy(button("Delete User"))).toBe(OWNER_REASON);
expect( expect(
screen.getByText( screen.getByText(
"The owner's role is fixed. Only the host's break-glass console (sudo felis breakGlass) manages the owner.", "The Owner role cannot be changed. Manage it through the host recovery console (sudo felis breakGlass).",
), ),
).toBeTruthy(); ).toBeTruthy();
expect(screen.queryByRole("combobox")).toBeNull(); expect(screen.queryByRole("combobox")).toBeNull();
+2 -2
View File
@@ -324,7 +324,7 @@ describe("ServersPage order and paging", () => {
await sortBy(user, "Sort by status"); await sortBy(user, "Sort by status");
expect(await tableOrder()).toEqual(["skyblock", "survival", "Arcade", "node-2", "node-10", "creative"]); expect(await tableOrder()).toEqual(["skyblock", "survival", "Arcade", "node-2", "node-10", "creative"]);
await sortBy(user, "Most players first"); await sortBy(user, "Online players, descending");
expect(await tableOrder()).toEqual(["skyblock", "survival", "Arcade", "creative", "node-2", "node-10"]); expect(await tableOrder()).toEqual(["skyblock", "survival", "Arcade", "creative", "node-2", "node-10"]);
}); });
@@ -340,7 +340,7 @@ describe("ServersPage order and paging", () => {
// "surv" opens both survival names and sits inside my-surv, which ranks it below. // "surv" opens both survival names and sits inside my-surv, which ranks it below.
expect(await tableOrder()).toEqual(["survival-a", "survival-b", "my-surv"]); expect(await tableOrder()).toEqual(["survival-a", "survival-b", "my-surv"]);
await sortBy(user, "Most players first"); await sortBy(user, "Online players, descending");
expect(await tableOrder()).toEqual(["survival-b", "survival-a", "my-surv"]); expect(await tableOrder()).toEqual(["survival-b", "survival-a", "my-surv"]);
await user.clear(screen.getByPlaceholderText(/Search/)); await user.clear(screen.getByPlaceholderText(/Search/));
expect(await tableOrder()).toEqual(["my-surv", "survival-b", "survival-a"]); expect(await tableOrder()).toEqual(["my-surv", "survival-b", "survival-a"]);