mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-19 13:01:44 +10:00
refactor(stylesheet): move Semantic CSS to the browser (#3329)
This commit is contained in:
@@ -52,7 +52,6 @@
|
||||
"@reactive-resume/db": "workspace:*",
|
||||
"@reactive-resume/env": "workspace:*",
|
||||
"@reactive-resume/mcp": "workspace:*",
|
||||
"@reactive-resume/pdf": "workspace:*",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@reactive-resume/utils": "workspace:*",
|
||||
"@sindresorhus/slugify": "^3.0.0",
|
||||
@@ -61,7 +60,6 @@
|
||||
"ai": "^7.0.58",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.6.26",
|
||||
"canonicalize": "^3.0.0",
|
||||
"cjk-regex": "^3.4.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
@@ -74,7 +72,6 @@
|
||||
"node-html-parser": "^9.0.1",
|
||||
"nodemailer": "^9.0.5",
|
||||
"ollama-ai-provider-v2": "^4.0.1",
|
||||
"pdfjs-dist": "^6.2.108",
|
||||
"pg": "^8.23.0",
|
||||
"phosphor-icons-react-pdf": "^0.1.3",
|
||||
"react": "^19.2.8",
|
||||
|
||||
@@ -131,10 +131,10 @@ describe("createApp", () => {
|
||||
it("uses the transport address for public PDF fallback despite rotated forwarding headers", async () => {
|
||||
const { createApp } = await import("./app");
|
||||
const app = createApp();
|
||||
const first = new Request("http://localhost:3001/api/resumes/jane/resume/pdf?reason=render-data-hash", {
|
||||
const first = new Request("http://localhost:3001/api/resumes/jane/resume/pdf", {
|
||||
headers: { "x-forwarded-for": "198.51.100.1" },
|
||||
});
|
||||
const rotated = new Request("http://localhost:3001/api/resumes/jane/resume/pdf?reason=render-data-hash", {
|
||||
const rotated = new Request("http://localhost:3001/api/resumes/jane/resume/pdf", {
|
||||
headers: { "x-forwarded-for": "198.51.100.2" },
|
||||
});
|
||||
const env = transportEnv("203.0.113.9");
|
||||
@@ -159,8 +159,8 @@ describe("createApp", () => {
|
||||
const unknownRpcRequest = new Request("http://localhost:3001/api/rpc", {
|
||||
headers: { "cf-connecting-ip": "198.51.100.2" },
|
||||
});
|
||||
const trustedOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume/style-projection");
|
||||
const unknownOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume/style-projection");
|
||||
const trustedOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume");
|
||||
const unknownOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume");
|
||||
|
||||
await app.fetch(trustedRpcRequest, transportEnv("203.0.113.9"));
|
||||
await app.fetch(unknownRpcRequest);
|
||||
|
||||
@@ -6,16 +6,6 @@ const mocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("@reactive-resume/api/features/resume/public-pdf", () => ({
|
||||
createPublicResumePdf: mocks.createPublicResumePdf,
|
||||
PUBLIC_RESUME_PDF_MISMATCH_REASONS: [
|
||||
"missing-projection",
|
||||
"format-version",
|
||||
"language-version",
|
||||
"semantic-tree-version",
|
||||
"registry-fingerprint",
|
||||
"adapter-fingerprint",
|
||||
"render-data-hash",
|
||||
"invalid-projection",
|
||||
],
|
||||
}));
|
||||
|
||||
const { handlePublicResumePdf } = await import("./public-resume-pdf");
|
||||
@@ -24,18 +14,15 @@ const trustedClient = "203.0.113.9";
|
||||
describe("handlePublicResumePdf", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns the authorized fallback PDF with strict mismatch metadata and cache policy", async () => {
|
||||
it("returns the authorized on-demand PDF without forwarding compatibility metadata", async () => {
|
||||
const body = new File(["%PDF"], "Ada_Lovelace.pdf", { type: "text/plain" });
|
||||
mocks.createPublicResumePdf.mockResolvedValueOnce({
|
||||
body,
|
||||
filename: "Ada_Lovelace.pdf",
|
||||
});
|
||||
const registry = "0".repeat(64);
|
||||
const adapter = "1".repeat(64);
|
||||
const request = new Request(
|
||||
`https://example.com/api/resumes/jane/resume/pdf?reason=render-data-hash®istryFingerprint=${registry}&adapterFingerprint=${adapter}`,
|
||||
{ headers: { "x-forwarded-for": "203.0.113.7" } },
|
||||
);
|
||||
const request = new Request("https://example.com/api/resumes/jane/resume/pdf?ignored=true", {
|
||||
headers: { "x-forwarded-for": "203.0.113.7" },
|
||||
});
|
||||
|
||||
const response = await handlePublicResumePdf(request, "jane", "resume", trustedClient);
|
||||
|
||||
@@ -50,13 +37,10 @@ describe("handlePublicResumePdf", () => {
|
||||
slug: "resume",
|
||||
requestHeaders: request.headers,
|
||||
trustedClient,
|
||||
mismatchReason: "render-data-hash",
|
||||
clientRegistryFingerprint: registry,
|
||||
clientAdapterFingerprint: adapter,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults a missing mismatch reason and keeps password/private responses uncacheable", async () => {
|
||||
it("keeps password and private responses uncacheable", async () => {
|
||||
mocks.createPublicResumePdf.mockResolvedValueOnce({
|
||||
body: new File(["%PDF"], "resume.pdf", { type: "application/pdf" }),
|
||||
filename: "resume.pdf",
|
||||
@@ -66,13 +50,15 @@ describe("handlePublicResumePdf", () => {
|
||||
const response = await handlePublicResumePdf(request, "jane", "resume", trustedClient);
|
||||
|
||||
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
expect(mocks.createPublicResumePdf).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ mismatchReason: "missing-projection" }),
|
||||
);
|
||||
expect(mocks.createPublicResumePdf).toHaveBeenCalledWith({
|
||||
username: "jane",
|
||||
slug: "resume",
|
||||
requestHeaders: request.headers,
|
||||
trustedClient,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ code: "BAD_REQUEST" }, 400],
|
||||
[{ code: "NEED_PASSWORD" }, 401],
|
||||
[{ code: "NOT_FOUND" }, 404],
|
||||
[{ code: "RATE_LIMIT_EXCEEDED" }, 429],
|
||||
@@ -90,20 +76,4 @@ describe("handlePublicResumePdf", () => {
|
||||
expect(response.status).toBe(status);
|
||||
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
});
|
||||
|
||||
it.each(["?reason=private-source", "?registryFingerprint=unsafe", "?adapterFingerprint=unsafe"])(
|
||||
"rejects invalid fallback metadata before the API service",
|
||||
async (search) => {
|
||||
const response = await handlePublicResumePdf(
|
||||
new Request(`https://example.com/api/resumes/jane/resume/pdf${search}`),
|
||||
"jane",
|
||||
"resume",
|
||||
trustedClient,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
expect(mocks.createPublicResumePdf).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,57 +1,28 @@
|
||||
import type { PublicResumePdfMismatchReason } from "@reactive-resume/api/features/resume/public-pdf";
|
||||
import {
|
||||
createPublicResumePdf,
|
||||
PUBLIC_RESUME_PDF_MISMATCH_REASONS,
|
||||
} from "@reactive-resume/api/features/resume/public-pdf";
|
||||
import { createPublicResumePdf } from "@reactive-resume/api/features/resume/public-pdf";
|
||||
|
||||
const noStoreResponse = (body: string, status: number) =>
|
||||
new Response(body, { status, headers: { "Cache-Control": "private, no-store" } });
|
||||
|
||||
const errorStatus = (error: unknown): number => {
|
||||
const code = typeof error === "object" && error && "code" in error ? (error as { code?: unknown }).code : undefined;
|
||||
if (code === "BAD_REQUEST") return 400;
|
||||
if (code === "NEED_PASSWORD") return 401;
|
||||
if (code === "NOT_FOUND") return 404;
|
||||
if (code === "RATE_LIMIT_EXCEEDED") return 429;
|
||||
return 500;
|
||||
};
|
||||
|
||||
const fingerprint = (value: string | null): string | undefined => {
|
||||
if (value === null) return;
|
||||
return /^[a-f0-9]{64}$/.test(value) ? value : undefined;
|
||||
};
|
||||
|
||||
export async function handlePublicResumePdf(
|
||||
request: Request,
|
||||
username: string,
|
||||
slug: string,
|
||||
trustedClient = "unknown",
|
||||
): Promise<Response> {
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
const rawReason = searchParams.get("reason") ?? "missing-projection";
|
||||
if (!PUBLIC_RESUME_PDF_MISMATCH_REASONS.includes(rawReason as PublicResumePdfMismatchReason)) {
|
||||
return noStoreResponse("Invalid fallback metadata", 400);
|
||||
}
|
||||
const rawRegistryFingerprint = searchParams.get("registryFingerprint");
|
||||
const rawAdapterFingerprint = searchParams.get("adapterFingerprint");
|
||||
const clientRegistryFingerprint = fingerprint(rawRegistryFingerprint);
|
||||
const clientAdapterFingerprint = fingerprint(rawAdapterFingerprint);
|
||||
if (
|
||||
(rawRegistryFingerprint !== null && clientRegistryFingerprint === undefined) ||
|
||||
(rawAdapterFingerprint !== null && clientAdapterFingerprint === undefined)
|
||||
) {
|
||||
return noStoreResponse("Invalid fallback metadata", 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createPublicResumePdf({
|
||||
username,
|
||||
slug,
|
||||
requestHeaders: request.headers,
|
||||
trustedClient,
|
||||
mismatchReason: rawReason as PublicResumePdfMismatchReason,
|
||||
...(clientRegistryFingerprint ? { clientRegistryFingerprint } : {}),
|
||||
...(clientAdapterFingerprint ? { clientAdapterFingerprint } : {}),
|
||||
});
|
||||
|
||||
return new Response(result.body, {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { pathToFileURL } from "node:url";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { env } from "@reactive-resume/env/server";
|
||||
import { createApp } from "./http/app";
|
||||
import { stylesheetPreflightRunner } from "./services/stylesheet-preflight";
|
||||
import { runStartupChecks } from "./startup/checks";
|
||||
|
||||
export { createApp } from "./http/app";
|
||||
@@ -32,10 +31,6 @@ async function main() {
|
||||
console.info(`🚀 Up and running on http://localhost:${info.port}`);
|
||||
},
|
||||
);
|
||||
|
||||
// Load the heavy PDF preflight runtime once, now, so the first semantic-CSS edit
|
||||
// does not pay (and time out on) the cold worker start.
|
||||
stylesheetPreflightRunner.warmup();
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { RPCHandler } from "@orpc/server/fetch";
|
||||
import { BatchHandlerPlugin, RequestHeadersPlugin, StrictGetMethodPlugin } from "@orpc/server/plugins";
|
||||
import router from "@reactive-resume/api/routers";
|
||||
import { mergeResponseHeaders } from "../http/headers";
|
||||
import { stylesheetPreflightRunner } from "../services/stylesheet-preflight";
|
||||
import { getRequestLocale } from "./locale";
|
||||
|
||||
const rpcHandler = new RPCHandler(router, {
|
||||
@@ -24,7 +23,6 @@ export async function handleRpc(request: Request, trustedClient = "unknown") {
|
||||
reqHeaders: request.headers,
|
||||
resHeaders,
|
||||
trustedClient,
|
||||
stylesheetPreflightRunner,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { createStylesheetPreflightRunner, STYLESHEET_PREFLIGHT_LIMITS } from "./stylesheet-preflight";
|
||||
|
||||
const validStylesheet = {
|
||||
languageVersion: 1,
|
||||
text: "@version 1;",
|
||||
} as const;
|
||||
|
||||
const input = {
|
||||
data: defaultResumeData,
|
||||
template: defaultResumeData.metadata.template,
|
||||
stylesheet: validStylesheet,
|
||||
} as const;
|
||||
|
||||
// Runners now own a long-lived, reused worker; destroy them so no worker thread
|
||||
// outlives its test.
|
||||
const runners: Array<{ destroy(): Promise<void> }> = [];
|
||||
const track = <T extends { destroy(): Promise<void> }>(runner: T): T => {
|
||||
runners.push(runner);
|
||||
return runner;
|
||||
};
|
||||
afterEach(async () => {
|
||||
await Promise.all(runners.splice(0).map((runner) => runner.destroy()));
|
||||
});
|
||||
|
||||
const memoryExhaustionWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
const retained = [];
|
||||
while (true) {
|
||||
const batch = Array.from({ length: 100_000 }, (_, index) => ({ batch: retained.length, index }));
|
||||
retained.push(batch);
|
||||
}
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const failedWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent('throw new Error("sensitive worker details");')}`,
|
||||
);
|
||||
|
||||
// Counts how many preflights this single worker instance served so a reuse test
|
||||
// can prove the worker is not respawned per request.
|
||||
const reuseCountingWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
let served = 0;
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
served += 1;
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: { ok: true, pageCount: 1, byteCount: served, diagnostics: [] },
|
||||
});
|
||||
});
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const delayedSuccessfulWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
setTimeout(() => {
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: {
|
||||
ok: true,
|
||||
pageCount: 1,
|
||||
byteCount: Number(message.input.data.basics.name),
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const delayedReadyWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
setTimeout(() => {
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [] },
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
setTimeout(() => parentPort.postMessage({ type: "ready" }), 50);
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const neverCompletesWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
setInterval(() => {}, 1_000);
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const synchronousFailureWorker = new URL("https://example.com/stylesheet-preflight.mjs");
|
||||
|
||||
const numberedInput = (number: number) => ({
|
||||
...input,
|
||||
data: {
|
||||
...input.data,
|
||||
basics: {
|
||||
...input.data.basics,
|
||||
name: String(number),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const invalidInput = () => {
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.customSections = [
|
||||
{
|
||||
id: "custom-experience",
|
||||
type: "experience",
|
||||
title: "Experience",
|
||||
icon: "",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
items: [{ id: "summary-shaped-item", hidden: false, content: "<p>Missing company</p>" }],
|
||||
} as never,
|
||||
];
|
||||
return { ...input, data };
|
||||
};
|
||||
|
||||
describe("stylesheet PDF preflight worker", () => {
|
||||
it("keeps the production resource policy fixed and immutable", () => {
|
||||
expect(STYLESHEET_PREFLIGHT_LIMITS).toEqual({
|
||||
timeoutMs: 30_000,
|
||||
maxPages: 20,
|
||||
maxBytes: 10_000_000,
|
||||
maxPageWidthPt: 2_000,
|
||||
maxPageHeightPt: 20_000,
|
||||
maxPageAreaPt2: 20_000_000,
|
||||
maxOldGenerationMb: 256,
|
||||
maxConcurrentWorkers: 1,
|
||||
maxQueuedRequests: 32,
|
||||
});
|
||||
expect(Object.isFrozen(STYLESHEET_PREFLIGHT_LIMITS)).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts a bounded candidate render in an isolated worker", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 15_000 }));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
ok: true,
|
||||
pageCount: 1,
|
||||
byteCount: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
if (!result.ok) throw new Error(`Expected successful preflight, received ${result.code}.`);
|
||||
expect(result.byteCount).toBeGreaterThan(0);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
}, 45_000);
|
||||
|
||||
it("reuses one warm worker across sequential requests instead of cold-starting each one", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({}, reuseCountingWorker));
|
||||
|
||||
const first = await runner.run(input);
|
||||
const second = await runner.run(input);
|
||||
const third = await runner.run(input);
|
||||
|
||||
// A single reused worker increments its per-instance counter; a per-request
|
||||
// worker would report byteCount 1 every time.
|
||||
expect([first, second, third].map((result) => (result.ok ? result.byteCount : -1))).toEqual([1, 2, 3]);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves structured resume-data failures across the worker boundary", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 15_000 }));
|
||||
|
||||
const result = runner.run(invalidInput());
|
||||
|
||||
await expect(result).rejects.toMatchObject({
|
||||
name: "ZodError",
|
||||
issues: expect.arrayContaining([expect.objectContaining({ path: ["customSections", 0, "items", 0, "company"] })]),
|
||||
});
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
}, 20_000);
|
||||
|
||||
it("terminates a worker when the render exceeds its deadline", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 1 }, neverCompletesWorker));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_TIMEOUT" }));
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
|
||||
it("starts the authored render deadline after the worker runtime is ready", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 20 }, delayedReadyWorker));
|
||||
|
||||
await expect(runner.run(input)).resolves.toEqual(expect.objectContaining({ ok: true, pageCount: 1 }));
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
});
|
||||
|
||||
it("returns deterministic output byte and page limit codes", async () => {
|
||||
const byteRunner = track(createStylesheetPreflightRunner({ maxBytes: 16 }));
|
||||
const pageRunner = track(createStylesheetPreflightRunner({ maxPages: 0 }));
|
||||
|
||||
await expect(byteRunner.run(input)).resolves.toEqual(
|
||||
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_BYTE_LIMIT" }),
|
||||
);
|
||||
await expect(pageRunner.run(input)).resolves.toEqual(
|
||||
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_PAGE_LIMIT" }),
|
||||
);
|
||||
expect(byteRunner.activeWorkerCount).toBe(0);
|
||||
expect(pageRunner.activeWorkerCount).toBe(0);
|
||||
}, 30_000);
|
||||
|
||||
it("maps worker heap exhaustion to a controlled memory-limit result", async () => {
|
||||
const runner = track(
|
||||
createStylesheetPreflightRunner({ maxOldGenerationMb: 8, timeoutMs: 10_000 }, memoryExhaustionWorker),
|
||||
);
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_MEMORY_LIMIT" }));
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
}, 15_000);
|
||||
|
||||
it("does not expose internal errors from a failed worker bootstrap", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({}, failedWorker));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed.",
|
||||
diagnostics: [],
|
||||
});
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
|
||||
it("surfaces sanitized render failures from inside the worker catch path", async () => {
|
||||
const throwingWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed. (Error: Canvas is already closed)",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
`)}`,
|
||||
);
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 5_000 }, throwingWorker));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed. (Error: Canvas is already closed)",
|
||||
diagnostics: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds concurrent workers and queued requests without charging queue time to the worker deadline", async () => {
|
||||
const runner = track(
|
||||
createStylesheetPreflightRunner(
|
||||
{
|
||||
timeoutMs: 500,
|
||||
maxConcurrentWorkers: 1,
|
||||
maxQueuedRequests: 2,
|
||||
},
|
||||
delayedSuccessfulWorker,
|
||||
),
|
||||
);
|
||||
const completionOrder: number[] = [];
|
||||
const accepted = [1, 2, 3].map((number) =>
|
||||
runner.run(numberedInput(number)).then((result) => {
|
||||
if (result.ok) completionOrder.push(result.byteCount);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
const rejected = runner.run(numberedInput(4));
|
||||
|
||||
expect(runner.activeWorkerCount).toBe(1);
|
||||
expect(runner.queuedPreflightCount).toBe(2);
|
||||
await expect(rejected).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight queue is full.",
|
||||
}),
|
||||
);
|
||||
const results = await Promise.all(accepted);
|
||||
|
||||
expect(results.every((result) => result.ok)).toBe(true);
|
||||
expect(completionOrder).toEqual([1, 2, 3]);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
}, 5_000);
|
||||
|
||||
it("does not leak a slot when the worker constructor throws synchronously", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({}, synchronousFailureWorker));
|
||||
|
||||
await expect(runner.run(input)).resolves.toEqual(
|
||||
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_WORKER_FAILED" }),
|
||||
);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,362 +0,0 @@
|
||||
import type {
|
||||
PdfPreflightFailure,
|
||||
PdfPreflightResult,
|
||||
StylesheetPreflightInput,
|
||||
StylesheetPreflightRunner,
|
||||
} from "@reactive-resume/pdf/server";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { STYLESHEET_PREFLIGHT_LIMITS } from "@reactive-resume/pdf/preflight-reference";
|
||||
|
||||
export { STYLESHEET_PREFLIGHT_LIMITS } from "@reactive-resume/pdf/preflight-reference";
|
||||
|
||||
type StylesheetPreflightLimits = {
|
||||
timeoutMs: number;
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
maxPageWidthPt: number;
|
||||
maxPageHeightPt: number;
|
||||
maxPageAreaPt2: number;
|
||||
maxOldGenerationMb: number;
|
||||
maxConcurrentWorkers: number;
|
||||
maxQueuedRequests: number;
|
||||
};
|
||||
|
||||
const SOURCE_WORKER_LOADER_HEAP_MB = 256;
|
||||
// The worker is warmed once and reused, so the one-time cold load (which is
|
||||
// super-linear in available CPU — measured ~9s at 0.5 vCPU, ~53s at 0.35, ~93s at
|
||||
// 0.25) is paid at startup, not per request. This ceiling must exceed that cold
|
||||
// load or warmup is killed mid-bootstrap and never completes on a throttled box;
|
||||
// it only bounds a genuinely stuck bootstrap and is off the per-edit path.
|
||||
const WORKER_READINESS_TIMEOUT_MS = 120_000;
|
||||
|
||||
type SerializedPreflightCause = {
|
||||
name: string;
|
||||
message: string;
|
||||
issues: readonly unknown[];
|
||||
};
|
||||
|
||||
type StylesheetPreflightWorkerMessage =
|
||||
| { type: "ready" }
|
||||
| { type: "load_error"; message: string }
|
||||
| { type: "result"; requestId: number; result: PdfPreflightResult }
|
||||
| { type: "preflight_error"; requestId: number; cause: SerializedPreflightCause };
|
||||
|
||||
export type NodeStylesheetPreflightRunner = StylesheetPreflightRunner & {
|
||||
readonly activeWorkerCount: number;
|
||||
readonly queuedPreflightCount: number;
|
||||
warmup(): void;
|
||||
destroy(): Promise<void>;
|
||||
};
|
||||
|
||||
const failure = (code: PdfPreflightFailure["code"], message: string): PdfPreflightFailure => ({
|
||||
ok: false,
|
||||
code,
|
||||
message,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const workerFailure = (error: Error): PdfPreflightFailure => {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return code === "ERR_WORKER_OUT_OF_MEMORY" || /heap out of memory/i.test(error.message)
|
||||
? failure("STYLESHEET_PREFLIGHT_MEMORY_LIMIT", "The PDF preflight worker exceeded its memory limit.")
|
||||
: failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed.");
|
||||
};
|
||||
|
||||
const sourceWorkerExecArgv = () => {
|
||||
const importIndex = process.execArgv.findIndex(
|
||||
(argument, index, arguments_) =>
|
||||
(argument === "--import" && arguments_[index + 1]?.includes("tsx")) ||
|
||||
(argument.startsWith("--import=") && argument.includes("tsx")),
|
||||
);
|
||||
const inherited = process.execArgv[importIndex];
|
||||
if (inherited?.startsWith("--import=")) return [inherited];
|
||||
if (inherited === "--import") return [inherited, process.execArgv[importIndex + 1] as string];
|
||||
return ["--import", import.meta.resolve("tsx")];
|
||||
};
|
||||
|
||||
const workerLocation = () => {
|
||||
const source = import.meta.url.endsWith(".ts");
|
||||
return {
|
||||
source,
|
||||
url: source
|
||||
? new URL("../workers/stylesheet-preflight.ts", import.meta.url)
|
||||
: new URL("./stylesheet-preflight-worker.mjs", import.meta.url),
|
||||
// Production must not inherit parent execArgv (e.g. --import/--input-type); source workers need tsx.
|
||||
execArgv: source ? sourceWorkerExecArgv() : [],
|
||||
...(source
|
||||
? {
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: fileURLToPath(new URL("../../tsconfig.json", import.meta.url)),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
type PendingRequest = {
|
||||
resolve: (result: PdfPreflightResult) => void;
|
||||
reject: (cause: unknown) => void;
|
||||
renderTimer?: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
type Readiness = {
|
||||
promise: Promise<Worker>;
|
||||
resolve: (worker: Worker) => void;
|
||||
reject: (error: Error) => void;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
export function createStylesheetPreflightRunner(
|
||||
overrides: Partial<StylesheetPreflightLimits> = {},
|
||||
testWorkerUrl?: URL,
|
||||
): NodeStylesheetPreflightRunner {
|
||||
const limits = Object.freeze({ ...STYLESHEET_PREFLIGHT_LIMITS, ...overrides });
|
||||
|
||||
let worker: Worker | undefined;
|
||||
let ready = false;
|
||||
let readiness: Readiness | undefined;
|
||||
let destroyed = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
const pending = new Map<number, PendingRequest>();
|
||||
// ponytail: process-local, bounded admission; upgrade to a pooled/distributed queue only for multi-process coordination.
|
||||
const queue: Array<{
|
||||
input: StylesheetPreflightInput;
|
||||
resolve: PendingRequest["resolve"];
|
||||
reject: PendingRequest["reject"];
|
||||
}> = [];
|
||||
|
||||
const teardownWorker = () => {
|
||||
const dead = worker;
|
||||
worker = undefined;
|
||||
ready = false;
|
||||
if (readiness) {
|
||||
clearTimeout(readiness.timer);
|
||||
readiness.reject(new Error("Stylesheet preflight worker did not become ready."));
|
||||
readiness = undefined;
|
||||
}
|
||||
if (!dead) return;
|
||||
dead.off("message", onMessage);
|
||||
dead.off("error", onError);
|
||||
dead.off("exit", onExit);
|
||||
void dead.terminate().catch(() => undefined);
|
||||
};
|
||||
|
||||
const clearPending = (requestId: number): PendingRequest | undefined => {
|
||||
const entry = pending.get(requestId);
|
||||
if (!entry) return undefined;
|
||||
if (entry.renderTimer) clearTimeout(entry.renderTimer);
|
||||
pending.delete(requestId);
|
||||
return entry;
|
||||
};
|
||||
|
||||
const drainQueue = () => {
|
||||
while (!destroyed && pending.size < limits.maxConcurrentWorkers && queue.length > 0) {
|
||||
const next = queue.shift();
|
||||
if (!next) return;
|
||||
dispatch(next.input, next.resolve, next.reject);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveRequest = (requestId: number, result: PdfPreflightResult) => {
|
||||
const entry = clearPending(requestId);
|
||||
if (!entry) return;
|
||||
entry.resolve(result);
|
||||
drainQueue();
|
||||
};
|
||||
|
||||
const rejectRequest = (requestId: number, cause: unknown) => {
|
||||
const entry = clearPending(requestId);
|
||||
if (!entry) return;
|
||||
entry.reject(cause);
|
||||
drainQueue();
|
||||
};
|
||||
|
||||
// A crashed/exited/timed-out worker is torn down and its in-flight requests are
|
||||
// failed; the next dispatch (including any queued requests) spawns a fresh one,
|
||||
// so a poisoned render never lingers across requests.
|
||||
const failWorker = (result: PdfPreflightFailure) => {
|
||||
teardownWorker();
|
||||
const stale = [...pending.keys()];
|
||||
for (const requestId of stale) resolveRequest(requestId, result);
|
||||
drainQueue();
|
||||
};
|
||||
|
||||
function onMessage(message: StylesheetPreflightWorkerMessage) {
|
||||
if (message.type === "ready") {
|
||||
if (readiness) {
|
||||
clearTimeout(readiness.timer);
|
||||
ready = true;
|
||||
const resolveReady = readiness.resolve;
|
||||
const readyWorker = worker;
|
||||
readiness = undefined;
|
||||
if (readyWorker) resolveReady(readyWorker);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.type === "load_error") {
|
||||
failWorker(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", message.message));
|
||||
return;
|
||||
}
|
||||
if (message.type === "result") {
|
||||
resolveRequest(message.requestId, message.result);
|
||||
return;
|
||||
}
|
||||
if (message.type === "preflight_error") {
|
||||
rejectRequest(
|
||||
message.requestId,
|
||||
Object.assign(new Error(message.cause.message), { name: message.cause.name, issues: message.cause.issues }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
console.warn("[stylesheet-preflight] worker error:", error.message);
|
||||
failWorker(workerFailure(error));
|
||||
}
|
||||
|
||||
function onExit(code: number) {
|
||||
if (destroyed || (!worker && pending.size === 0)) return;
|
||||
console.warn(`[stylesheet-preflight] worker exited unexpectedly (code ${code})`);
|
||||
failWorker(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed."));
|
||||
}
|
||||
|
||||
const startWorker = (): Promise<Worker> => {
|
||||
const location = testWorkerUrl ? { source: false, url: testWorkerUrl, execArgv: [] as string[] } : workerLocation();
|
||||
let spawned: Worker;
|
||||
try {
|
||||
spawned = new Worker(location.url, {
|
||||
name: "stylesheet-preflight",
|
||||
resourceLimits: {
|
||||
// The source-only tsx compiler heap is outside the production render budget.
|
||||
maxOldGenerationSizeMb: limits.maxOldGenerationMb + (location.source ? SOURCE_WORKER_LOADER_HEAP_MB : 0),
|
||||
},
|
||||
execArgv: location.execArgv,
|
||||
...("env" in location ? { env: location.env } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject(error instanceof Error ? error : new Error("Failed to start PDF preflight worker."));
|
||||
}
|
||||
|
||||
worker = spawned;
|
||||
ready = false;
|
||||
// The idle reused worker must not keep the process (or a test run) alive; active
|
||||
// requests stay alive through their ref'd readiness/render timers.
|
||||
spawned.unref();
|
||||
spawned.on("message", onMessage);
|
||||
spawned.once("error", onError);
|
||||
spawned.once("exit", onExit);
|
||||
|
||||
let resolve!: (value: Worker) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<Worker>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(`[stylesheet-preflight] worker did not become ready within ${WORKER_READINESS_TIMEOUT_MS}ms`);
|
||||
teardownWorker();
|
||||
}, WORKER_READINESS_TIMEOUT_MS);
|
||||
readiness = { promise, resolve, reject, timer };
|
||||
return promise;
|
||||
};
|
||||
|
||||
const getReadyWorker = (): Promise<Worker> => {
|
||||
if (destroyed) return Promise.reject(new Error("Stylesheet preflight worker was terminated."));
|
||||
if (worker && ready) return Promise.resolve(worker);
|
||||
if (readiness) return readiness.promise;
|
||||
return startWorker();
|
||||
};
|
||||
|
||||
// One retry: a worker that failed to become ready is torn down; a second
|
||||
// attempt spawns a fresh worker before the request gives up.
|
||||
const waitUntilReady = async (): Promise<Worker> => {
|
||||
try {
|
||||
return await getReadyWorker();
|
||||
} catch {
|
||||
return await getReadyWorker();
|
||||
}
|
||||
};
|
||||
|
||||
function dispatch(
|
||||
input: StylesheetPreflightInput,
|
||||
resolve: PendingRequest["resolve"],
|
||||
reject: PendingRequest["reject"],
|
||||
) {
|
||||
const requestId = ++requestSeq;
|
||||
pending.set(requestId, { resolve, reject });
|
||||
void waitUntilReady()
|
||||
.then((readyWorker) => {
|
||||
const entry = pending.get(requestId);
|
||||
if (!entry) return;
|
||||
entry.renderTimer = setTimeout(() => {
|
||||
// A stuck render poisons the reused worker: tear it down and respawn — but
|
||||
// only the worker this request actually ran on, so a stale timer can never
|
||||
// kill a newer worker already serving other requests.
|
||||
clearPending(requestId);
|
||||
if (worker === readyWorker) teardownWorker();
|
||||
resolve(failure("STYLESHEET_PREFLIGHT_TIMEOUT", "The PDF preflight exceeded its deadline."));
|
||||
drainQueue();
|
||||
}, limits.timeoutMs);
|
||||
readyWorker.postMessage({ type: "preflight", requestId, input, limits });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
resolveRequest(
|
||||
requestId,
|
||||
workerFailure(error instanceof Error ? error : new Error("Failed to start PDF preflight worker.")),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
get activeWorkerCount() {
|
||||
return pending.size;
|
||||
},
|
||||
get queuedPreflightCount() {
|
||||
return queue.length;
|
||||
},
|
||||
|
||||
warmup() {
|
||||
void waitUntilReady().catch(() => undefined);
|
||||
},
|
||||
|
||||
run(input: StylesheetPreflightInput): Promise<PdfPreflightResult> {
|
||||
return new Promise<PdfPreflightResult>((resolve, reject) => {
|
||||
if (destroyed) {
|
||||
resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."));
|
||||
return;
|
||||
}
|
||||
if (pending.size < limits.maxConcurrentWorkers) {
|
||||
dispatch(input, resolve, reject);
|
||||
return;
|
||||
}
|
||||
if (queue.length >= limits.maxQueuedRequests) {
|
||||
resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight queue is full."));
|
||||
return;
|
||||
}
|
||||
queue.push({ input, resolve, reject });
|
||||
});
|
||||
},
|
||||
|
||||
async destroy() {
|
||||
destroyed = true;
|
||||
const dead = worker;
|
||||
teardownWorker();
|
||||
await dead?.terminate().catch(() => undefined);
|
||||
for (const requestId of [...pending.keys()]) {
|
||||
resolveRequest(
|
||||
requestId,
|
||||
failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."),
|
||||
);
|
||||
}
|
||||
while (queue.length > 0) {
|
||||
const next = queue.shift();
|
||||
next?.resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const stylesheetPreflightRunner = createStylesheetPreflightRunner();
|
||||
@@ -1,78 +0,0 @@
|
||||
import type {
|
||||
PdfPreflightPageLimits,
|
||||
PdfPreflightResult,
|
||||
RenderPreflightPdfResult,
|
||||
} from "@reactive-resume/pdf/preflight";
|
||||
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
|
||||
type StylesheetPreflightInspectionLimits = PdfPreflightPageLimits & {
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
};
|
||||
|
||||
type PdfLoadingTask = {
|
||||
promise: PromiseLike<{ numPages: number }>;
|
||||
destroy(): Promise<void>;
|
||||
};
|
||||
|
||||
type LoadPdf = (options: { data: Uint8Array }) => PdfLoadingTask;
|
||||
|
||||
export async function inspectPreflightPdf(
|
||||
rendered: Extract<RenderPreflightPdfResult, { ok: true }>,
|
||||
limits: StylesheetPreflightInspectionLimits,
|
||||
loadPdf: LoadPdf = getDocument,
|
||||
): Promise<PdfPreflightResult> {
|
||||
if (rendered.bytes.byteLength > limits.maxBytes) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_BYTE_LIMIT",
|
||||
message: "The rendered PDF exceeds the preflight byte limit.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
const byteCount = rendered.bytes.byteLength;
|
||||
let loadingTask: PdfLoadingTask;
|
||||
try {
|
||||
loadingTask = loadPdf({ data: rendered.bytes });
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let document: { numPages: number };
|
||||
try {
|
||||
document = await loadingTask.promise;
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
if (document.numPages > limits.maxPages) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PAGE_LIMIT",
|
||||
message: "The rendered PDF exceeds the preflight page limit.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
pageCount: document.numPages,
|
||||
byteCount,
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
} finally {
|
||||
await loadingTask.destroy().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { inspectPreflightPdf } from "./stylesheet-preflight-inspection";
|
||||
|
||||
const warning = {
|
||||
code: "EXTREME_VALUE",
|
||||
severity: "warning",
|
||||
message: "The authored value is unusually large.",
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 10, offset: 9 },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const rendered = {
|
||||
ok: true as const,
|
||||
bytes: new TextEncoder().encode("%PDF-1.7"),
|
||||
diagnostics: [warning],
|
||||
};
|
||||
|
||||
const limits = {
|
||||
maxPages: 20,
|
||||
maxBytes: 10_000_000,
|
||||
maxPageWidthPt: 2_000,
|
||||
maxPageHeightPt: 20_000,
|
||||
maxPageAreaPt2: 20_000_000,
|
||||
};
|
||||
|
||||
describe("stylesheet preflight PDF inspection", () => {
|
||||
it("preserves compiler warnings and hides parser exceptions", async () => {
|
||||
const destroy = vi.fn(async () => undefined);
|
||||
|
||||
const result = await inspectPreflightPdf(rendered, limits, () => ({
|
||||
promise: Promise.reject(new Error("sensitive parser details")),
|
||||
destroy,
|
||||
}));
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: [warning],
|
||||
});
|
||||
expect(destroy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves compiler warnings when PDF.js fails before returning a loading task", async () => {
|
||||
const result = await inspectPreflightPdf(rendered, limits, () => {
|
||||
throw new Error("sensitive parser setup details");
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: [warning],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import type {
|
||||
PdfPreflightPageLimits,
|
||||
PdfPreflightResult,
|
||||
StylesheetPreflightInput,
|
||||
} from "@reactive-resume/pdf/preflight";
|
||||
import { parentPort } from "node:worker_threads";
|
||||
import * as React from "react";
|
||||
import { inspectPreflightPdf } from "./stylesheet-preflight-inspection";
|
||||
|
||||
(globalThis as typeof globalThis & { React: typeof React }).React = React;
|
||||
|
||||
type StylesheetPreflightWorkerLimits = PdfPreflightPageLimits & {
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
};
|
||||
|
||||
// The worker is long-lived and reused across requests: the heavy PDF runtime is
|
||||
// loaded once at startup and each preflight arrives as a message (input can no
|
||||
// longer come from `workerData`, which is fixed at construction time).
|
||||
type StylesheetPreflightWorkerRequest = {
|
||||
type: "preflight";
|
||||
requestId: number;
|
||||
input: StylesheetPreflightInput;
|
||||
limits: StylesheetPreflightWorkerLimits;
|
||||
};
|
||||
|
||||
type SerializedPreflightCause = {
|
||||
name: string;
|
||||
message: string;
|
||||
issues: readonly unknown[];
|
||||
};
|
||||
|
||||
const sanitizeWorkerCause = (cause: unknown): string => {
|
||||
if (!(cause instanceof Error)) return "The PDF preflight worker failed.";
|
||||
const detail = cause.message.replace(/\s+/g, " ").trim().slice(0, 200);
|
||||
if (!detail) return "The PDF preflight worker failed.";
|
||||
return `The PDF preflight worker failed. (${cause.name}: ${detail})`;
|
||||
};
|
||||
|
||||
const serializeZodCause = (cause: unknown): SerializedPreflightCause | undefined => {
|
||||
if (!(cause instanceof Error) || cause.name !== "ZodError" || !("issues" in cause) || !Array.isArray(cause.issues)) {
|
||||
return;
|
||||
}
|
||||
return { name: cause.name, message: cause.message, issues: cause.issues };
|
||||
};
|
||||
|
||||
// Load the heavy PDF runtime once. A rejection here (missing/broken dependency in
|
||||
// a pruned production install) is reported explicitly instead of becoming an
|
||||
// unhandled rejection that silently kills the worker and surfaces as an opaque
|
||||
// runner-side failure.
|
||||
const initialization = import("@reactive-resume/pdf/preflight");
|
||||
|
||||
void initialization.then(
|
||||
() => parentPort?.postMessage({ type: "ready" }),
|
||||
(cause: unknown) => {
|
||||
console.error("[stylesheet-preflight] worker runtime failed to load", cause);
|
||||
parentPort?.postMessage({ type: "load_error", message: sanitizeWorkerCause(cause) });
|
||||
},
|
||||
);
|
||||
|
||||
const handle = async (request: StylesheetPreflightWorkerRequest): Promise<void> => {
|
||||
try {
|
||||
const { renderPreflightPdf } = await initialization;
|
||||
const rendered = await renderPreflightPdf(request.input, request.limits);
|
||||
const result: PdfPreflightResult = rendered.ok ? await inspectPreflightPdf(rendered, request.limits) : rendered;
|
||||
parentPort?.postMessage({ type: "result", requestId: request.requestId, result });
|
||||
} catch (cause) {
|
||||
const serializedCause = serializeZodCause(cause);
|
||||
if (serializedCause) {
|
||||
parentPort?.postMessage({ type: "preflight_error", requestId: request.requestId, cause: serializedCause });
|
||||
return;
|
||||
}
|
||||
console.error("[stylesheet-preflight]", cause);
|
||||
parentPort?.postMessage({
|
||||
type: "result",
|
||||
requestId: request.requestId,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: sanitizeWorkerCause(cause),
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
parentPort?.on("message", (message: StylesheetPreflightWorkerRequest) => {
|
||||
if (message.type !== "preflight") return;
|
||||
void handle(message);
|
||||
});
|
||||
@@ -33,10 +33,7 @@ const promptAssetsPlugin: TsdownPlugin = {
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
"stylesheet-preflight-worker": "src/workers/stylesheet-preflight.ts",
|
||||
},
|
||||
entry: { index: "src/index.ts" },
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node24",
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Aansoeke per week gestuur (laaste 8 weke)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Aansoek gedoen"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Aansoek gedoen"
|
||||
msgid "Applied on"
|
||||
msgstr "Toegepas op"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Toegepas met waarskuwings"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabies"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Voer jou wagwoord in om die opstel van twee-faktor-magtiging te bevestig
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Voer jou wagwoord in om twee-faktor-magtiging te deaktiveer. Jou rekening sal minder veilig wees sonder 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fout"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Misluk om jou rugsteunkodeverifikasie te voltooi. Probeer asseblief weer
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Misluk om jou kode te verifieer. Probeer asseblief weer."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funksies"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Voorskou"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Voorskou en uitvoer gebruik die laaste geldige weergawe."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lees…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Gereed om te aktiveer"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Gereed om te aktiveer met waarskuwings"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Herstel"
|
||||
msgid "Reset Password"
|
||||
msgstr "Herstel wagwoord"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Stel terug na toegepaste stylblad"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Herstel jou wagwoord"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Sommige aansoekstelsels vereis een voordat jy kan indien."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Sommige ontleders hanteer beelde verkeerd, en foto's word in sommige streke ontmoedig."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Iets het verkeerd geloop"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stylbladredigeerder"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stylblad bevat foute"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Gebruikers"
|
||||
msgid "Uzbek"
|
||||
msgstr "Oezbeeks"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Geldige URL's moet met http:// of https:// begin."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel KI-toegangspoort"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zoem uit"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zoeloe"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "በሳምንት የተላኩ ማመልከቻዎች (ያለፉት 8 ሳምንታት)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "ተመልክቷል"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "ተመልክቷል"
|
||||
msgid "Applied on"
|
||||
msgstr "የተተገበረው በ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "ከማስጠንቀቂያዎች ጋር ተተግብሯል"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "ዓረብኛ"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "ሁለት-ደረጃ ማረጋገጫን ስትቆጣጠሩ የሚሆን
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "ሁለት-ደረጃ ማረጋገጫን ለማቦዘን የይለፍ ቃልዎን ያስገቡ። 2FA ባቦዘነ ጊዜ መለያዎ ያነሰ ደህንነት ይኖረዋል።"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "ስህተት"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "የመጠባበቂያ ኮድዎን ማረጋገጥ አልተሳካም።
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "ኮድዎን ማረጋገጥ አልተሳካም። እባክዎ እንደገና ይሞክሩ።"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "መለያዎች"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "ቅድመ እይታ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "ቅድመ እይታ እና ወደ ውጭ መላክ የመጨረሻውን ትክክለኛ ስሪት ይጠቀሙ።"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "ንባብ…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "ለማግበር ዝግጁ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "ከማስጠንቀቂያዎች ጋር ለማግበር ዝግጁ"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "ዳግም ለመጀመር"
|
||||
msgid "Reset Password"
|
||||
msgstr "የይለፍ ቃል ዳግም አስጀምር"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "ወደ ተተገበረ የቅጥ ሉህ ዳግም አስጀምር"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "የይለፍ ቃልዎን ዳግም ያስጀምሩ"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "ከማቅረብዎ በፊት አንዳንድ የመተግበሪያ ስ
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "አንዳንድ ተንታኞች ምስሎችን ያታልላሉ፣ እና ፎቶዎች በአንዳንድ ክልሎች ተስፋ ይቆርጣሉ።"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "የሆነ ችግር ተፈጥሯል"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "የቅጥ ሉህ አርታኢ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "የቅጥ ሉህ ስህተቶች አሉት"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "ተጠቃሚዎች"
|
||||
msgid "Uzbek"
|
||||
msgstr "ኡዝቤክኛ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "ትክክለኛ URL በ http:// ወይም https:// መጀመር አለባቸው።"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "አጉር"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ዙሉ"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "الطلبات المرسلة أسبوعيًا (آخر 8 أسابيع)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "تم التقديم"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "تم التقديم"
|
||||
msgid "Applied on"
|
||||
msgstr "تم التقديم في"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "تم استخدامه مع التحذيرات"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "العربية"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "أدخِل كلمة المرور الخاصة بك لتأكيد إعدا
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "أدخِل كلمة المرور الخاصة بك لتعطيل المصادقة الثنائية. سيكون حسابك أقل أمانًا بدون تمكين المصادقة الثنائية (2FA)."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "خطأ"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "فشل التحقق من رمز النسخ الاحتياطي الخاص
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "فشل التحقق من الرمز الخاص بك. يرجى المحاولة مرة أخرى."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "الميزات"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "معاينة"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "استخدم آخر نسخة صالحة للمعاينة والتصدير."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "قراءة…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "جاهز للتفعيل"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "جاهز للتفعيل مع التحذيرات"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "إعادة تعيين"
|
||||
msgid "Reset Password"
|
||||
msgstr "إعادة تعيين كلمة المرور"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "إعادة ضبط نمط التنسيق المُطبق"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "إعادة تعيين كلمة المرور"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "تتطلب بعض أنظمة التقديم واحدًا قبل أن ت
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "يسيء بعض المحللين التعامل مع الصور، ولا يُنصح باستخدام الصور في بعض المناطق."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "حدث خطأ ما"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "محرر أنماط CSS"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "يحتوي ملف الأنماط على أخطاء"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "المستخدمون"
|
||||
msgid "Uzbek"
|
||||
msgstr "الأوزبكية"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "يجب أن تبدأ عناوين URL الصالحة بـ http:// أو https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "بوابة فيركل للذكاء الاصطناعي"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "تصغير"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "الزولو"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Həftəlik göndərilən müraciətlər (son 8 həftə)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Müraciət edildi"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Müraciət edildi"
|
||||
msgid "Applied on"
|
||||
msgstr "Tətbiq edildiyi tarix"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Xəbərdarlıqlarla tətbiq olunur"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Ərəb"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "İki mərhələli autentifikasiyanı qurmağı təsdiqləmək üçün pa
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "İki mərhələli autentifikasiyanı deaktiv etmək üçün parolunuzu daxil edin. 2FA deaktiv edildikdə, hesabınız daha az təhlükəsiz olacaq."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Xəta"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Ehtiyat kodunuzu təsdiqləmək mümkün olmadı. Zəhmət olmasa yenid
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Kodunuzu təsdiqləmək mümkün olmadı. Zəhmət olmasa yenidən cəhd edin."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Xüsusiyyətlər"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Önizləmə"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Önizləmə və ixrac üçün son etibarlı versiyadan istifadə edin."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "… oxunur"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Aktivləşdirməyə hazırdır"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Xəbərdarlıqlarla aktivləşdirməyə hazırdır"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Sıfırla"
|
||||
msgid "Reset Password"
|
||||
msgstr "Parolu Sıfırla"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Tətbiq olunmuş stil cədvəlinə sıfırlayın"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Parolunuzu sıfırlayın"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Bəzi proqram sistemləri təqdim etməzdən əvvəl birini tələb edir
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Bəzi təhlilçilər şəkilləri səhv idarə edir və bəzi bölgələrdə fotoşəkillər tövsiyə edilmir."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Nəsə səhv getdi"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stil cədvəli redaktoru"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stil cədvəlində səhvlər var"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "İstifadəçilər"
|
||||
msgid "Uzbek"
|
||||
msgstr "Özbək"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Etibarlı URL‑lər http:// və ya https:// ilə başlamalıdır."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Qapısı"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Uzaqlaşdır"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Изпратени кандидатури на седмица (последните 8 седмици)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Кандидатствано"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Кандидатствано"
|
||||
msgid "Applied on"
|
||||
msgstr "Приложено на"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Прилага се с предупреждения"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Арабски"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Въведете паролата си, за да потвърдите
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Въведете паролата си, за да изключите двуфакторното удостоверяване. Вашият профил ще бъде по-малко защитен без активирано 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Грешка"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Не успяхте да проверите кода си за архи
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Не успяхме да проверим кода ви. Моля, опитайте отново."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Функции"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Преглед"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Прегледът и експортът използват последната валидна версия."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Четене…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Готов за активиране"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Готово за активиране с предупреждения"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Нулиране"
|
||||
msgid "Reset Password"
|
||||
msgstr "Нулиране на паролата"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Възстановяване към приложения стилов лист"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Нулирайте паролата си"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Някои системи за кандидатстване изискв
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Някои парсери обработват неправилно изображения и снимките не се препоръчват в някои региони."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Нещо се обърка"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Редактор на стилови листове"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Стилният лист съдържа грешки"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Потребители"
|
||||
msgid "Uzbek"
|
||||
msgstr "Узбекски"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Валидните URL адреси трябва да започват с http:// или https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Намаляване"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Зулуски"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "প্রতি সপ্তাহে পাঠানো আবেদন (শেষ 8 সপ্তাহ)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "আবেদন করা হয়েছে"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "আবেদন করা হয়েছে"
|
||||
msgid "Applied on"
|
||||
msgstr "প্রয়োগ করা হয়েছে"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "সতর্কতাসহ প্রয়োগ করা হয়েছে"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "আরবি"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "টু‑ফ্যাক্টর প্রমাণীকরণ সে
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "টু‑ফ্যাক্টর প্রমাণীকরণ বন্ধ করতে আপনার পাসওয়ার্ড লিখুন। 2FA বন্ধ করলে আপনার অ্যাকাউন্ট কম সুরক্ষিত থাকবে।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "ত্রুটি"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "আপনার ব্যাকআপ কোড যাচাই কর
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "আপনার কোড যাচাই করতে ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "বৈশিষ্ট্যসমূহ"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "প্রিভিউ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "প্রিভিউ এবং এক্সপোর্টের জন্য সর্বশেষ বৈধ সংস্করণটি ব্যবহার করা হয়।"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "পড়া…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "সক্রিয় করার জন্য প্রস্তুত"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "সতর্কতা সহ সক্রিয় করার জন্য প্রস্তুত"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "রিসেট"
|
||||
msgid "Reset Password"
|
||||
msgstr "পাসওয়ার্ড রিসেট করুন"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "প্রয়োগকৃত স্টাইলশীটে রিসেট করুন"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "আপনার পাসওয়ার্ড রিসেট করুন"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "আপনি জমা দেওয়ার আগে কিছু অ
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "কিছু বিশ্লেষক চিত্রগুলিকে ভুলভাবে পরিচালনা করে এবং কিছু অঞ্চলে ফটোগুলিকে নিরুৎসাহিত করা হয়৷"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "কিছু একটা ভুল হয়েছে"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "স্টাইলশিট সম্পাদক"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "স্টাইলশিটে ত্রুটি আছে"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "ব্যবহারকারী"
|
||||
msgid "Uzbek"
|
||||
msgstr "উজবেক"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "বৈধ URL অবশ্যই http:// বা https:// দিয়ে শুরু হতে হবে।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "ভার্সেল এআই গেটওয়ে"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "জুম আউট"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "জুলু"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Sol·licituds enviades per setmana (darreres 8 setmanes)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Sol·licitada"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Sol·licitada"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplicat a"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Aplicat amb avisos"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Àrab"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Introdueix la contrasenya per confirmar la configuració de l’autentic
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Introdueix la contrasenya per desactivar l’autenticació en dos passos. El compte serà menys segur sense l’A2F activada."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "No s'ha pogut verificar el vostre codi de recuperació. Torneu-ho a prov
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "S'ha fallat la verificació del vostre codi. Torneu-ho a provar."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funcions"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Vista prèvia"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "La vista prèvia i l'exportació utilitzen l'última versió vàlida."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Llegint…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Llest per activar"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Llest per activar amb avisos"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Restableix"
|
||||
msgid "Reset Password"
|
||||
msgstr "Restableix la contrasenya"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Restableix al full d'estils aplicat"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Restableix la contrasenya"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Alguns sistemes de sol·licitud en requereixen un abans de poder enviar-
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Alguns analitzadors analítics gestionen malament les imatges i, en algunes regions, no es recomanen les fotos."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Alguna cosa ha anat malament"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor de fulls d'estil"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "El full d'estil té errors"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Usuaris"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Les URL vàlides han de començar per http:// o https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Allunya"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulú"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Žádosti odeslané týdně (posledních 8 týdnů)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Podáno"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Podáno"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplikováno na"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Použito s varováním"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabština"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Zadejte své heslo pro potvrzení nastavení dvoufázového ověření.
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Zadejte své heslo pro deaktivaci dvoufázového ověření. Bez aktivního 2FA bude váš účet méně zabezpečený."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Chyba"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Nepodařilo se ověřit váš záložní kód. Zkuste to prosím znovu."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Nepodařilo se ověřit váš kód. Zkuste to prosím znovu."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkce"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Náhled"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Náhled a export použijí poslední platnou verzi."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Čtení…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Připraveno k aktivaci"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Připraveno k aktivaci s varováními"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Resetovat"
|
||||
msgid "Reset Password"
|
||||
msgstr "Obnovit heslo"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Obnovit použitý stylový list"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Obnovte své heslo"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Některé aplikační systémy vyžadují jeden před odesláním."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Některé analyzátory špatně zacházejí s obrázky a v některých oblastech se fotografie nedoporučuje."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Něco se pokazilo"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor stylů"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stylový list obsahuje chyby"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Uživatelé"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbečtina"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Platné URL adresy musí začínat http:// nebo https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Brána Vercel AI"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Oddálit"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Ansøgninger sendt pr. uge (sidste 8 uger)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Ansøgt"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Ansøgt"
|
||||
msgid "Applied on"
|
||||
msgstr "Anvendt den"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Anvendt med advarsler"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabisk"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Indtast din adgangskode for at bekræfte opsætning af to-faktor-godkend
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Indtast din adgangskode for at deaktivere to-faktor-godkendelse. Din konto vil være mindre sikker uden 2FA aktiveret."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fejl"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Det lykkedes ikke at bekræfte din backup-kode. Prøv venligst igen."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Det lykkedes ikke at bekræfte din kode. Prøv venligst igen."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funktioner"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Forhåndsvisning"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Forhåndsvisning og eksport med den seneste gyldige version."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Læsning…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Klar til aktivering"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Klar til aktivering med advarsler"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Nulstil"
|
||||
msgid "Reset Password"
|
||||
msgstr "Nulstil adgangskode"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Nulstil til anvendt stylesheet"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Nulstil din adgangskode"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Nogle ansøgningssystemer kræver et, før du kan indsende."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Nogle parsere mishandler billeder, og fotos frarådes i nogle regioner."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Noget gik galt"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stylesheet-editor"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stylesheet indeholder fejl"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Brugere"
|
||||
msgid "Uzbek"
|
||||
msgstr "Usbekisk"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Gyldige URL'er skal starte med http:// eller https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zoom ud"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Pro Woche gesendete Bewerbungen (letzte 8 Wochen)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Beworben"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Beworben"
|
||||
msgid "Applied on"
|
||||
msgstr "Beworben am"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Mit Warnungen angewendet"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabisch"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Geben Sie Ihr Passwort ein, um die Einrichtung der Zwei-Faktor-Authentif
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren. Ihr Konto ist ohne 2FA weniger sicher."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Ihr Sicherungscode konnte nicht verifiziert werden. Bitte versuchen Sie
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Ihr Code konnte nicht verifiziert werden. Bitte versuchen Sie es erneut."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funktionen"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Vorschau"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Vorschau und Export verwenden die letzte gültige Version."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Wird gelesen…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Bereit zur Aktivierung"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Bereit zur Aktivierung mit Warnungen"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Zurücksetzen"
|
||||
msgid "Reset Password"
|
||||
msgstr "Passwort zurücksetzen"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Auf angewendetes Stylesheet zurücksetzen"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Setzen Sie Ihr Passwort zurück"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Bei einigen Bewerbungssystemen ist ein Antrag erforderlich, bevor Sie ih
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Einige Parser verarbeiten Bilder falsch, und in einigen Regionen wird von Fotos abgeraten."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Etwas ist schiefgelaufen."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stylesheet-Editor"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Das Stylesheet enthält Fehler."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Nutzende"
|
||||
msgid "Uzbek"
|
||||
msgstr "Usbekisch"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Gültige URLs müssen mit http:// oder https:// beginnen."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Herauszoomen"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Αιτήσεις που στάλθηκαν ανά εβδομάδα (τελευταίες 8 εβδομάδες)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Υποβλήθηκε"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Υποβλήθηκε"
|
||||
msgid "Applied on"
|
||||
msgstr "Ημερομηνία αίτησης"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Εφαρμόζεται με προειδοποιήσεις"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Αραβικά"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Εισαγάγετε τον κωδικό πρόσβασής σας γι
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Εισαγάγετε τον κωδικό πρόσβασής σας για να απενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων. Ο λογαριασμός σας θα είναι λιγότερο ασφαλής χωρίς ενεργοποιημένο 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Σφάλμα"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Απέτυχε η επαλήθευση του κωδικού αντιγ
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Απέτυχε η επαλήθευση του κωδικού σας. Προσπαθήστε ξανά."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Λειτουργίες"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Πρεμιέρα"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Η προεπισκόπηση και η εξαγωγή χρησιμοποιούν την τελευταία έγκυρη έκδοση."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Ανάγνωση…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Έτοιμο για ενεργοποίηση"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Έτοιμο για ενεργοποίηση με προειδοποιήσεις"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Επαναφορά"
|
||||
msgid "Reset Password"
|
||||
msgstr "Επαναφορά κωδικού πρόσβασης"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Επαναφορά στο εφαρμοσμένο φύλλο στυλ"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Επαναφορά κωδικού πρόσβασης"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Ορισμένα συστήματα αιτήσεων απαιτούν έ
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Ορισμένοι αναλυτές χειρίζονται εσφαλμένα τις εικόνες και οι φωτογραφίες αποθαρρύνονται σε ορισμένες περιοχές."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Κάτι πήγε στραβά"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Επεξεργαστής φύλλου στυλ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Το φύλλο στυλ έχει σφάλματα"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Χρήστες"
|
||||
msgid "Uzbek"
|
||||
msgstr "Ουζμπεκικά"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Τα έγκυρα URLs πρέπει να ξεκινούν με http:// ή https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Πύλη Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Σμίκρυνση"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Ζουλού"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Applications sent per week (last 8 weeks)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Applied"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Applied"
|
||||
msgid "Applied on"
|
||||
msgstr "Applied on"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Applied with warnings"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabic"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Enter your password to confirm setting up two-factor authentication. Whe
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Failed to verify your backup code. Please try again."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Failed to verify your code. Please try again."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Features"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Preview"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Preview and export use the last valid version."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Reading…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Ready to activate"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Ready to activate with warnings"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Reset"
|
||||
msgid "Reset Password"
|
||||
msgstr "Reset Password"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Reset to applied stylesheet"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Reset your password"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Some application systems require one before you can submit."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Something went wrong"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stylesheet editor"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stylesheet has errors"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Users"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Valid URLs must start with http:// or https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zoom out"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-14
@@ -521,7 +521,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Applications sent per week (last 8 weeks)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Applied"
|
||||
|
||||
@@ -529,10 +528,6 @@ msgstr "Applied"
|
||||
msgid "Applied on"
|
||||
msgstr "Applied on"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Applied with warnings"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabic"
|
||||
@@ -1760,7 +1755,6 @@ msgstr "Enter your password to confirm setting up two-factor authentication. Whe
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
@@ -1998,6 +1992,10 @@ msgstr "Failed to verify your backup code. Please try again."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Failed to verify your code. Please try again."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr "Fatal error"
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Features"
|
||||
@@ -3415,8 +3413,12 @@ msgid "Preview"
|
||||
msgstr "Preview"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Preview and export use the last valid version."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr "Preview and export fall back to base styles."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr "Preview and export keep valid styles and ignore invalid styles."
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3537,6 +3539,10 @@ msgstr "Reading…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Ready to activate"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr "Ready to activate with errors"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Ready to activate with warnings"
|
||||
@@ -3658,10 +3664,6 @@ msgstr "Reset"
|
||||
msgid "Reset Password"
|
||||
msgstr "Reset Password"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Reset to applied stylesheet"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Reset your password"
|
||||
@@ -4190,6 +4192,10 @@ msgstr "Some application systems require one before you can submit."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr "Some styles were ignored"
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Something went wrong"
|
||||
@@ -4333,8 +4339,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stylesheet editor"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stylesheet has errors"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr "Stylesheet has fatal errors"
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5098,10 +5104,22 @@ msgstr "Users"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr "Valid"
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Valid URLs must start with http:// or https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr "Valid with errors"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr "Valid with warnings"
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Candidaturas enviadas por semana (últimas 8 semanas)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Enviada"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Enviada"
|
||||
msgid "Applied on"
|
||||
msgstr "Postulado el"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Aplicado con advertencias"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Árabe"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Introduce tu contraseña para confirmar la configuración de la autentic
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Introduce tu contraseña para deshabilitar la autenticación de doble factor. Tu cuenta será menos segura sin 2FA habilitada."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "No se ha podido verificar su código de copia de seguridad. Por favor, i
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "No se ha podido verificar su código. Por favor, inténtelo de nuevo."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funciones"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Avance"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "La vista previa y la exportación utilizan la última versión válida."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Leyendo…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Listo para activar"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Listo para activarse con advertencias"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Restablecer"
|
||||
msgid "Reset Password"
|
||||
msgstr "Restablecer contraseña"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Restablecer a la hoja de estilos aplicada"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Restablecer tu contraseña"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Algunos sistemas de solicitud requieren uno antes de poder enviarlo."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Algunos analizadores manejan mal las imágenes y en algunas regiones no se recomiendan las fotografías."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Algo salió mal"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor de hojas de estilo"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "La hoja de estilos tiene errores."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Usuarios"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbeko"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Las URL válidas deben comenzar con http:// o https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Pasarela Vercel AI"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Alejar"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulú"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "درخواستهای ارسالشده در هفته (۸ هفته گذشته)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "ارسالشده"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "ارسالشده"
|
||||
msgid "Applied on"
|
||||
msgstr "تاریخ درخواست"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "با هشدارها اعمال میشود"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "عربی"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "برای تأیید راهاندازی احراز هویت دو مر
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "برای غیرفعال کردن احراز هویت دو مرحلهای گذرواژهٔ خود را وارد کنید. بدون فعال بودن 2FA حساب شما امنیت کمتری خواهد داشت."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "خطا"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "کد پشتیبان شما تأیید نشد. لطفاً دوباره ت
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "تأیید کد شما ناموفق بود. لطفاً دوباره تلاش کنید."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "امکانات"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "پیشنمایش"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "پیشنمایش و خروجی گرفتن از آخرین نسخه معتبر."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "در حال خواندن…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "آماده فعال سازی"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "آماده فعال شدن با هشدارها"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "ریست"
|
||||
msgid "Reset Password"
|
||||
msgstr "تنظیم مجدد گذرواژه"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "تنظیم مجدد به شیوهنامه اعمال شده"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "تنظیم مجدد گذرواژهٔ شما"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "برخی سامانههای درخواست شغلی پیش از ار
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "برخی تجزیهگرها تصاویر را نادرست پردازش میکنند و در بعضی مناطق استفاده از عکس توصیه نمیشود."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "چیزی اشتباه پیش رفت"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "ویرایشگر استایلشیت"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "استایلشیت خطا دارد"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "کاربران"
|
||||
msgid "Uzbek"
|
||||
msgstr "ازبکی"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URLهای معتبر باید با http:// یا https:// شروع شوند."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "دروازه ورچل ایآی"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "کوچکنمایی"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "زولو"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Lähetetyt hakemukset viikoittain (viimeiset 8 viikkoa)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Haettu"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Haettu"
|
||||
msgid "Applied on"
|
||||
msgstr "Haettu"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Käytetty varoituksilla"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "arabia"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Syötä salasanasi vahvistaaksesi kaksivaiheisen todennuksen käyttöön
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Syötä salasanasi poistaaksesi kaksivaiheisen todennuksen käytöstä. Tilisi on vähemmän suojattu ilman 2FA:ta."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Virhe"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Varmuuskopiointikoodin vahvistaminen epäonnistui. Yritä uudelleen."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Koodiasi ei onnistuttu tarkistamaan. Yritä uudelleen."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Ominaisuudet"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Esikatselu"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Esikatsele ja vie käyttämällä viimeisintä kelvollista versiota."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Luetaan…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Valmis aktivoitavaksi"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Valmis aktivoitavaksi varoituksilla"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Palauta"
|
||||
msgid "Reset Password"
|
||||
msgstr "Palauta salasana"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Palauta käytettyyn tyylitiedostoon"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Palauta salasanasi"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Jotkin hakujärjestelmät vaativat sellaisen, ennen kuin voit lähettä
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Jotkut jäsentimet käsittelevät kuvia väärin, eikä valokuvia suositella joillain alueilla."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Jokin meni pieleen"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Tyylitiedostoeditori"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Tyylitiedostossa on virheitä"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Käyttäjät"
|
||||
msgid "Uzbek"
|
||||
msgstr "uzbekki"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Kelvollisten URL-osoitteiden on alettava http:// tai https:// ."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Loitonna"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Candidatures envoyées par semaine (8 dernières semaines)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Candidature envoyée"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Candidature envoyée"
|
||||
msgid "Applied on"
|
||||
msgstr "Postulé le"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Appliqué avec avertissements"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabe"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Saisissez votre mot de passe pour confirmer la configuration de l'authen
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Saisissez votre mot de passe pour désactiver l'authentification à deux facteurs. Votre compte sera moins sécurisé si l'authentification à deux facteurs n'est pas activée."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Erreur"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "La vérification de votre code de sauvegarde a échoué. Veuillez réess
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "La vérification de votre code a échoué. Veuillez réessayer."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Fonctionnalités"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Aperçu"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "L'aperçu et l'exportation utilisent la dernière version valide."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lecture…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Prêt à activer"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Prêt à être activé avec avertissements"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Réinitialiser"
|
||||
msgid "Reset Password"
|
||||
msgstr "Réinitialiser le mot de passe"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Réinitialiser la feuille de style appliquée"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Réinitialisez votre mot de passe"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Certains systèmes de candidature en exigent un avant que vous puissiez
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Certains analyseurs traitent mal les images et les photos sont déconseillées dans certaines régions."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Quelque chose s'est mal passé"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Éditeur de feuilles de style"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "La feuille de style contient des erreurs"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Utilisateurs"
|
||||
msgid "Uzbek"
|
||||
msgstr "Ouzbékistanais"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Les URL valides doivent commencer par http:// ou https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Passerelle Vercel AI"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zoom arrière"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zoulou"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "מועמדויות שנשלחו בשבוע (8 השבועות האחרונים)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "הוגש"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "הוגש"
|
||||
msgid "Applied on"
|
||||
msgstr "הוגש בתאריך"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "יושם עם אזהרות"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "ערבית"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "הזן את הסיסמה שלך כדי לאשר הגדרת אימות ד
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "הזן את הסיסמה שלך כדי להשבית אימות דו־שלבי. החשבון שלך יהיה פחות מאובטח ללא אימות דו־שלבי."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "שְׁגִיאָה"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "לא הצלחנו לאמת את קוד הגיבוי שלך. אנא נס
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "לא הצלחנו לאמת את הקוד שלך. אנא נסה שוב."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "יכולות"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "תצוגה מקדימה"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "תצוגה מקדימה וייצוא משתמשים בגרסה התקינה האחרונה."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "קורא…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "מוכן להפעלה"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "מוכן להפעלה עם אזהרות"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "איפוס"
|
||||
msgid "Reset Password"
|
||||
msgstr "איפוס סיסמה"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "איפוס לגיליון הסגנונות שהוחל"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "איפוס הסיסמה שלך"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "מערכות מועמדות מסוימות דורשות זאת לפני
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "מנתחים מסוימים מטפלים בתמונות באופן שגוי, ובאזורים מסוימים לא מומלץ להשתמש בתמונות."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "משהו השתבש"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "עורך גיליון סגנונות"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "גיליון הסגנונות מכיל שגיאות"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "משתמשים"
|
||||
msgid "Uzbek"
|
||||
msgstr "אוזבקית"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "כתובות URL חוקיות חייבות להתחיל ב־http:// או https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "התרחקות"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "זולו"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "प्रति सप्ताह भेजे गए आवेदन (पिछले 8 सप्ताह)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "आवेदन किया"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "आवेदन किया"
|
||||
msgid "Applied on"
|
||||
msgstr "आवेदन की तारीख"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "चेतावनी सहित लागू"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "अरबी"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "दो‑कारक प्रमाणीकरण सेटअप क
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "दो‑कारक प्रमाणीकरण अक्षम करने के लिए अपना पासवर्ड दर्ज करें। 2FA सक्षम न होने पर आपका खाता कम सुरक्षित रहेगा।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "गलती"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "आपके बैकअप कोड को सत्यापित
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "आपके कोड का सत्यापन विफल हो गया। कृपया फिर से प्रयास करें।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "विशेषताएँ"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "पूर्व दर्शन"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "पूर्वावलोकन और निर्यात के लिए अंतिम मान्य संस्करण का उपयोग करें।"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "पढ़ा जा रहा है…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "सक्रिय करने के लिए तैयार"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "चेतावनी सहित सक्रिय करने के लिए तैयार"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "रीसेट"
|
||||
msgid "Reset Password"
|
||||
msgstr "पासवर्ड रीसेट करें"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "लागू स्टाइलशीट पर रीसेट करें"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "अपना पासवर्ड रीसेट करें"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "आपके सबमिट करने से पहले कुछ
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "कुछ पार्सर्स छवियों को गलत तरीके से संभालते हैं, और कुछ क्षेत्रों में फ़ोटो को हतोत्साहित किया जाता है।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "कुछ गलत हो गया"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "स्टाइलशीट संपादक"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "स्टाइलशीट में त्रुटियाँ हैं"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "उपयोगकर्ता"
|
||||
msgid "Uzbek"
|
||||
msgstr "उज़्बेक"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "मान्य URL http:// या https:// से शुरू होना चाहिए।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "वर्सेल एआई गेटवे"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "ज़ूम आउट"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ज़ुलु"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Hetente elküldött jelentkezések (az elmúlt 8 hét)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Jelentkezve"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Jelentkezve"
|
||||
msgid "Applied on"
|
||||
msgstr "Jelentkezés dátuma"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Figyelmeztetésekkel alkalmazva"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "arab"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Add meg a jelszavadat a kétlépcsős hitelesítés beállításának me
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Add meg a jelszavadat a kétlépcsős hitelesítés kikapcsolásához. A fiókod kevésbé lesz biztonságos 2FA nélkül."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Hiba"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Nem sikerült ellenőrizni a biztonsági mentés kódját. Kérjük, pr
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Nem sikerült ellenőrizni a kódot. Kérjük, próbálja meg újra."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkciók"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Előnézet"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Az előnézet és az exportálás a legutóbbi érvényes verziót használja."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Olvasás…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Aktiválásra kész"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Aktiválásra kész, figyelmeztetésekkel"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Visszaállítás"
|
||||
msgid "Reset Password"
|
||||
msgstr "Jelszó visszaállítása"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Visszaállítás az alkalmazott stíluslapra"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Jelszó visszaállítása"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Egyes jelentkezési rendszereknek szüksége van egy ilyenre a benyújt
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Egyes elemzők rosszul kezelik a képeket, és egyes régiókban nem javasoljuk a fotók használatát."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Valami rosszul sült el"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stíluslap-szerkesztő"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "A stíluslap hibákat tartalmaz"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Felhasználók"
|
||||
msgid "Uzbek"
|
||||
msgstr "üzbég"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Az érvényes URL http:// vagy https:// előtaggal kezdődik."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Kicsinyítés"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Lamaran dikirim per minggu (8 minggu terakhir)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Dilamar"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Dilamar"
|
||||
msgid "Applied on"
|
||||
msgstr "Dilamar pada"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Diterapkan dengan peringatan"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arab"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Masukkan kata sandi Anda untuk mengonfirmasi penyiapan autentikasi dua f
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Masukkan kata sandi Anda untuk menonaktifkan autentikasi dua faktor. Akun Anda akan menjadi kurang aman tanpa 2FA diaktifkan."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Kesalahan"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Gagal memverifikasi kode cadangan Anda. Silakan coba lagi."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Gagal memverifikasi kode Anda. Silakan coba lagi."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Fitur"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Pratinjau"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Pratinjau dan ekspor menggunakan versi valid terakhir."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Membaca…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Siap diaktifkan"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Siap diaktifkan dengan peringatan"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Atur Ulang"
|
||||
msgid "Reset Password"
|
||||
msgstr "Atur Ulang Kata Sandi"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Atur ulang ke stylesheet yang diterapkan"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Atur ulang kata sandi Anda"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Beberapa sistem lamaran memerlukannya sebelum Anda dapat mengirimkannya.
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Beberapa parser salah menangani gambar, dan foto tidak disarankan di beberapa wilayah."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Terjadi kesalahan."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor lembar gaya"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stylesheet memiliki kesalahan"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Pengguna"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL yang valid harus dimulai dengan http:// atau https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Perkecil"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Candidature inviate a settimana (ultime 8 settimane)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Candidatura inviata"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Candidatura inviata"
|
||||
msgid "Applied on"
|
||||
msgstr "Candidatura inviata il"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Applicato con avvertenze"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabo"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Inserisci la tua password per confermare la configurazione dell'autentic
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Inserisci la tua password per disabilitare l'autenticazione a due fattori. Il tuo account sarà meno sicuro senza 2FA attiva."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Errore"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Impossibile verificare il suo codice di backup. Provi di nuovo."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Non è stato possibile verificare il suo codice. La preghiamo di riprovare."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funzionalità"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Anteprima"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "L'anteprima e l'esportazione utilizzano l'ultima versione valida."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lettura…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Pronto per l'attivazione"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Pronto per l'attivazione con avvisi"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Ripristina"
|
||||
msgid "Reset Password"
|
||||
msgstr "Reimposta password"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Ripristina il foglio di stile applicato"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Reimposta la tua password"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Alcuni sistemi di candidatura ne richiedono uno prima di poter inviare."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Alcuni parser gestiscono male le immagini e le foto sono sconsigliate in alcune regioni."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Qualcosa è andato storto"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor di fogli di stile"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Il foglio di stile presenta degli errori"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Utenti"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbeco"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Gli URL validi devono iniziare con http:// o https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Gateway Vercel AI"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Rimpicciolisci"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "週ごとの応募送信数(直近8週間)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "応募済み"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "応募済み"
|
||||
msgid "Applied on"
|
||||
msgstr "応募日"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "警告付きで適用"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "アラビア語"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "二要素認証を設定することを確定するために、パスワ
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "二要素認証を無効にするには、パスワードを入力してください。2FA を無効化すると、アカウントのセキュリティレベルは下がります。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "エラー"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "バックアップコードの検証に失敗しました。もう一度
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "コードの検証に失敗しました。もう一度やり直してください。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "機能"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "プレビュー"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "プレビューとエクスポートには、最新の有効なバージョンを使用してください。"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "読み込み中…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "起動準備完了"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "警告付きで有効化準備完了"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "リセット"
|
||||
msgid "Reset Password"
|
||||
msgstr "パスワードをリセット"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "適用済みのスタイルシートにリセットします"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "パスワードをリセット"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "一部のアプリケーション システムでは、送信する前
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "一部のパーサーは画像を誤って処理するため、一部の地域では写真の使用が推奨されません。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "何らかの問題が発生しました"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "スタイルシートエディタ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "スタイルシートにエラーがあります"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "ユーザー数"
|
||||
msgid "Uzbek"
|
||||
msgstr "ウズベク語"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "有効な URL は http:// または https:// で始まる必要があります。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "ズームアウト"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ズールー語"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "ពាក្យសុំបានផ្ញើក្នុងមួយសប្តាហ៍ (៨ សប្តាហ៍ចុងក្រោយ)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "បានដាក់ពាក្យ"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "បានដាក់ពាក្យ"
|
||||
msgid "Applied on"
|
||||
msgstr "បានដាក់ពាក្យនៅ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "អនុវត្តជាមួយការព្រមាន"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabic"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "បញ្ចូលពាក្យសម្ងាត់របស់
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "បញ្ចូលពាក្យសម្ងាត់របស់អ្នក ដើម្បីបិទការផ្ទៀងផ្ទាត់ពីរជាន់។ គណនីរបស់អ្នកនឹងកាន់តែមានហានិភ័យនៅពេលមិនបានបើក 2FA ទៀត។"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "កំហុស"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "បរាជ័យក្នុងការផ្ទៀងផ្ទាត
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "បរាជ័យក្នុងការផ្ទៀងផ្ទាត់កូដរបស់អ្នក។ សូមព្យាយាមម្ដងទៀត។"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "មុខងារ"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "មើលជាមុន"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "ការមើលជាមុន និងការនាំចេញប្រើប្រាស់កំណែដែលមានសុពលភាពចុងក្រោយ។"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "កំពុងអាន…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "រួចរាល់ដើម្បីធ្វើឱ្យសកម្ម"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "រួចរាល់ដើម្បីធ្វើឲ្យសកម្មជាមួយនឹងការព្រមាន"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "កំណត់ឡើងវិញ"
|
||||
msgid "Reset Password"
|
||||
msgstr "កំណត់ពាក្យសម្ងាត់ឡើងវិញ"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "កំណត់ឡើងវិញទៅសន្លឹករចនាប័ទ្មដែលបានអនុវត្ត"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "កំណត់ពាក្យសម្ងាត់របស់អ្នកឡើងវិញ"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "ប្រព័ន្ធកម្មវិធីមួយចំ
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "ឧបករណ៍ញែកមួយចំនួនធ្វើឱ្យរូបភាពមិនត្រឹមត្រូវ ហើយរូបថតត្រូវបានលើកទឹកចិត្តនៅក្នុងតំបន់មួយចំនួន។"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "មានអ្វីមួយខុសប្រក្រតី"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "កម្មវិធីនិពន្ធសន្លឹករចនាប័ទ្ម"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "សន្លឹករចនាប័ទ្មមានកំហុស"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "អ្នកប្រើ"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL ត្រឹមត្រូវ ត្រូវតែចាប់ផ្តើមដោយ http:// ឬ https://។"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "បង្រួម"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "ಪ್ರತಿ ವಾರ ಕಳುಹಿಸಿದ ಅರ್ಜಿಗಳು (ಕೊನೆಯ 8 ವಾರಗಳು)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "ಅರ್ಜಿಸಲಾಗಿದೆ"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "ಅರ್ಜಿಸಲಾಗಿದೆ"
|
||||
msgid "Applied on"
|
||||
msgstr "ಅರ್ಜಿ ಸಲ್ಲಿಸಿದ ದಿನಾಂಕ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "ಎಚ್ಚರಿಕೆಗಳೊಂದಿಗೆ ಅನ್ವಯಿಸಲಾಗಿದೆ"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "ಅರೇಬಿಕ್"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "ಎರಡು ಅಂಶಗಳ ದೃಢೀಕರಣವನ್ನು ಹೊ
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "ಎರಡು ಅಂಶಗಳ ದೃಢೀಕರಣವನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ನಿಮ್ಮ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ನಮೂದಿಸಿ. 2FA ಸಕ್ರಿಯಗೊಳಿಸದಿದ್ದರೆ ನಿಮ್ಮ ಖಾತೆ ಕಡಿಮೆ ಸುರಕ್ಷಿತವಾಗಿರುತ್ತದೆ."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "ದೋಷ"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "ನಿಮ್ಮ ಬ್ಯಾಕಪ್ ಕೋಡ್ ಅನ್ನು ಪ
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "ನಿಮ್ಮ ಕೋಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಲು ವಿಫಲವಾಗಿದೆ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "ವೈಶಿಷ್ಟ್ಯಗಳು"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "ಪೂರ್ವವೀಕ್ಷಣೆ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "ಕೊನೆಯ ಮಾನ್ಯ ಆವೃತ್ತಿಯನ್ನು ಬಳಸಿಕೊಂಡು ಪೂರ್ವವೀಕ್ಷಣೆ ಮತ್ತು ರಫ್ತು ಮಾಡಿ."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "ಓದುತ್ತಿದೆ…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "ಸಕ್ರಿಯಗೊಳಿಸಲು ಸಿದ್ಧವಾಗಿದೆ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "ಎಚ್ಚರಿಕೆಗಳೊಂದಿಗೆ ಸಕ್ರಿಯಗೊಳಿಸಲು ಸಿದ್ಧವಾಗಿದೆ"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "ಮರುಹೊಂದಿಸಿ"
|
||||
msgid "Reset Password"
|
||||
msgstr "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಮರುಹೊಂದಿಸಿ"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "ಅನ್ವಯಿಸಲಾದ ಸ್ಟೈಲ್ಶೀಟ್ಗೆ ಮರುಹೊಂದಿಸಿ"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "ನಿಮ್ಮ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಮರುಹೊಂದಿಸಿ"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "ನೀವು ಸಲ್ಲಿಸುವ ಮೊದಲು ಕೆಲವು
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "ಕೆಲವು ಪಾರ್ಸರ್ಗಳು ಚಿತ್ರಗಳನ್ನು ತಪ್ಪಾಗಿ ನಿರ್ವಹಿಸುತ್ತವೆ ಮತ್ತು ಕೆಲವು ಪ್ರದೇಶಗಳಲ್ಲಿ ಫೋಟೋಗಳನ್ನು ನಿರುತ್ಸಾಹಗೊಳಿಸಲಾಗುತ್ತದೆ."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "ಏನೋ ತಪ್ಪಾಗಿದೆ."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "ಸ್ಟೈಲ್ಶೀಟ್ ಸಂಪಾದಕ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "ಸ್ಟೈಲ್ಶೀಟ್ ದೋಷಗಳನ್ನು ಹೊಂದಿದೆ."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "ಬಳಕೆದಾರರು"
|
||||
msgid "Uzbek"
|
||||
msgstr "ಉಜ್ಬೇಕ್"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "ಮಾನ್ಯ URL ಗಳು http:// ಅಥವಾ https:// ನಿಂದ ಪ್ರಾರಂಭವಾಗಿರಬೇಕು."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "ಗಾತ್ರ ಕುಗ್ಗಿಸಿ"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ಜೂಲೂ"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "주당 보낸 지원서 (최근 8주)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "지원함"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "지원함"
|
||||
msgid "Applied on"
|
||||
msgstr "지원일"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "경고와 함께 적용됨"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "아랍어"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "2단계 인증을 설정하려면 비밀번호를 입력해 확인하세
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "2단계 인증을 비활성화하려면 비밀번호를 입력하세요. 2단계 인증을 사용하지 않으면 계정 보안이 낮아집니다."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "오류"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "백업 코드를 인증하지 못했습니다. 다시 시도해 주세
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "코드를 인증하지 못했습니다. 다시 시도해 주세요."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "기능"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "시사"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "미리보기 및 내보내기 시에는 마지막으로 유효한 버전을 사용합니다."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "읽는 중…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "활성화 준비 완료"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "경고 메시지와 함께 활성화할 준비가 되었습니다."
|
||||
@@ -3663,10 +3669,6 @@ msgstr "초기화"
|
||||
msgid "Reset Password"
|
||||
msgstr "비밀번호 재설정"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "적용된 스타일시트로 재설정"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "비밀번호 재설정"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "일부 지원 시스템에서는 제출하기 전에 하나의 정보가
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "일부 파서는 이미지를 잘못 처리하므로 일부 지역에서는 사진을 사용하지 않는 것이 좋습니다."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "뭔가 잘못됐어요"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "스타일시트 편집기"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "스타일시트에 오류가 있습니다"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "사용자 수"
|
||||
msgid "Uzbek"
|
||||
msgstr "우즈베크어"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "유효한 URL은 http:// 또는 https://로 시작해야 합니다."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "버셀 AI 게이트웨이"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "축소"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "줄루어"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Per savaitę išsiųstos paraiškos (paskutinės 8 savaitės)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Pateikta"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Pateikta"
|
||||
msgid "Applied on"
|
||||
msgstr "Pateikta"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Taikoma su įspėjimais"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabų"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Įveskite slaptažodį, kad patvirtintumėte dviejų veiksnių tapatumo
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Įveskite slaptažodį, kad išjungtumėte dviejų veiksnių tapatumo patvirtinimą. Be įjungto 2FA jūsų paskyra bus mažiau saugi."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Klaida"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Nepavyko patikrinti atsarginės kopijos kodo. Bandykite dar kartą."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Nepavyko patikrinti jūsų kodo. Bandykite dar kartą."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkcijos"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Peržiūra"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Peržiūrai ir eksportui naudokite paskutinę galiojančią versiją."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Skaitoma…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Paruošta aktyvuoti"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Paruošta aktyvuoti su įspėjimais"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Nustatyti iš naujo"
|
||||
msgid "Reset Password"
|
||||
msgstr "Atkurti slaptažodį"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Atkurti pritaikytą stiliaus lapą"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Atkurkite slaptažodį"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Kai kurioms paraiškų teikimo sistemoms reikia, kad galėtumėte pateik
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Kai kurie analizatoriai netinkamai tvarko vaizdus, o kai kuriuose regionuose nuotraukos nerekomenduojamos."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Kažkas nutiko ne taip"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stilių lapų redaktorius"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stiliaus lape yra klaidų"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Vartotojai"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbekų"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Teisingi URL turi prasidėti http:// arba https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "\"Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Tolinti"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulų"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Nedēļā nosūtītie pieteikumi (pēdējās 8 nedēļas)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Pieteikts"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Pieteikts"
|
||||
msgid "Applied on"
|
||||
msgstr "Pieteikts"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Pielietots ar brīdinājumiem"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arābu"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Ievadiet savu paroli, lai apstiprinātu divu faktoru autentifikācijas i
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Ievadiet savu paroli, lai deaktivētu divu faktoru autentifikāciju. Bez ieslēgtas 2FA jūsu konts būs mazāk drošs."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Kļūda"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Neizdevās pārbaudīt jūsu dublējuma kodu. Lūdzu, mēģiniet vēlrei
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Jūsu kodu neizdevās pārbaudīt. Lūdzu, mēģiniet vēlreiz."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkcijas"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Priekšskatījums"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Priekšskatījumā un eksportā tiek izmantota pēdējā derīgā versija."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lasa…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Gatavs aktivizēšanai"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Gatavs aktivizēšanai ar brīdinājumiem"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Atiestatīt"
|
||||
msgid "Reset Password"
|
||||
msgstr "Atiestatīt paroli"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Atiestatīt uz lietoto stila lapu"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Atiestatīt paroli"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Dažām pieteikumu sistēmām tas ir nepieciešams, lai to varētu iesni
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Daži parsētāji nepareizi apstrādā attēlus, un dažos reģionos fotoattēli nav ieteicami."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Kaut kas nogāja greizi"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stilu lapu redaktors"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stilu lapā ir kļūdas"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Lietotāji"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbeku"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Derīgiem URL jāsākas ar http:// vai https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI vārtejas"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Tālināt"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "ആഴ്ചയിൽ അയച്ച അപേക്ഷകൾ (കഴിഞ്ഞ 8 ആഴ്ച)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "അപേക്ഷിച്ചു"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "അപേക്ഷിച്ചു"
|
||||
msgid "Applied on"
|
||||
msgstr "പ്രയോഗിച്ചു"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "മുന്നറിയിപ്പുകൾ ഉപയോഗിച്ച് പ്രയോഗിച്ചു"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "അറബിക്"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "ടു‑ഫാക്ടർ ഓത്ന്റിക്കേഷൻ
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "ടു‑ഫാക്ടർ ഓത്ന്റിക്കേഷൻ അപ്രാപ്തമാക്കാൻ നിങ്ങളുടെ പാസ്വേഡ് നൽകുക. 2FA പ്രാപ്തമാക്കിയിട്ടില്ലാത്തതിനാൽ നിങ്ങളുടെ അക്കൗണ്ട് കുറച്ച് കുറവായി സുരക്ഷിതമായിവരും."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "പിശക്"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "നിങ്ങളുടെ ബാക്കപ്പ് കോഡ് പ
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "നിങ്ങളുടെ കോഡ് പരിശോധിക്കുന്നതിൽ പരാജയപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "സവിശേഷതകൾ"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "പ്രിവ്യൂ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "അവസാനത്തെ സാധുവായ പതിപ്പ് ഉപയോഗിച്ച് പ്രിവ്യൂ ചെയ്ത് എക്സ്പോർട്ട് ചെയ്യുക."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "വായിക്കുന്നു..."
|
||||
msgid "Ready to activate"
|
||||
msgstr "സജീവമാക്കാൻ തയ്യാറാണ്"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "മുന്നറിയിപ്പുകൾ നൽകി സജീവമാക്കാൻ തയ്യാറാണ്"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "റീസെറ്റ് ചെയ്യുക"
|
||||
msgid "Reset Password"
|
||||
msgstr "പാസ്വേഡ് റീസെറ്റ് ചെയ്യുക"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "പ്രയോഗിച്ച സ്റ്റൈൽഷീറ്റിലേക്ക് പുനഃസജ്ജമാക്കുക"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "നിങ്ങളുടെ പാസ്വേഡ് റീസെറ്റ് ചെയ്യുക"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "നിങ്ങൾ സമർപ്പിക്കുന്നതിന
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "ചില പാഴ്സർമാർ ചിത്രങ്ങൾ തെറ്റായി കൈകാര്യം ചെയ്യുന്നു, ചില പ്രദേശങ്ങളിൽ ഫോട്ടോകൾ നിരുത്സാഹപ്പെടുത്തുന്നു."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "എന്തോ കുഴപ്പം സംഭവിച്ചു."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "സ്റ്റൈൽഷീറ്റ് എഡിറ്റർ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "സ്റ്റൈൽഷീറ്റിൽ പിശകുകൾ ഉണ്ട്"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "യൂസർമാർ"
|
||||
msgid "Uzbek"
|
||||
msgstr "ഉസ്ബെക്ക്"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "സാധുവായ URL‑കൾ http:// അല്ലെങ്കിൽ https:// കൊണ്ട് ആരംഭിക്കണം."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "വെർസെൽ എഐ ഗേറ്റ്വേ"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "സൂം ഔട്ട് ചെയ്യുക"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "സൂളു"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "दर आठवड्याला पाठवलेले अर्ज (मागील 8 आठवडे)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "अर्ज केला"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "अर्ज केला"
|
||||
msgid "Applied on"
|
||||
msgstr "वर अर्ज केला"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "इशाऱ्यांसह लागू केले"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "अरबी"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "दुहेरी घटक ओथेंटिकेशन सेटअ
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "दुहेरी घटक ओथेंटिकेशन अक्षम करण्यासाठी तुमचा पासवर्ड टाका. 2FA शिवाय तुमचे खाते कमी सुरक्षित राहील."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "त्रुटी"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "आपला बॅकअप कोड पडताळणी करण
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "तुमचा कोड पडताळणी करण्यात अयशस्वी झाला. कृपया पुन्हा प्रयत्न करा."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "वैशिष्ट्ये"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "पूर्वावलोकन"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "पूर्वावलोकन आणि निर्यातीसाठी शेवटची वैध आवृत्ती वापरली जाते."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "वाचत आहे…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "सक्रिय करण्यासाठी तयार"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "इशाऱ्यांसह सक्रिय करण्यास तयार"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "रीसेट करा"
|
||||
msgid "Reset Password"
|
||||
msgstr "पासवर्ड रीसेट करा"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "लागू केलेल्या स्टाईलशीटवर रीसेट करा"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "तुमचा पासवर्ड रीसेट करा"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "तुम्ही सबमिट करण्यापूर्वी
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "काही विश्लेषक प्रतिमांची चुकीची हाताळणी करतात आणि काही प्रदेशांमध्ये फोटोंना परावृत्त केले जाते."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "काहीतरी चूक झाली."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "स्टाईलशीट संपादक"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "स्टाईलशीटमध्ये त्रुटी आहेत"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "वापरकर्ते"
|
||||
msgid "Uzbek"
|
||||
msgstr "उझबेक"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "वैध URL ची सुरुवात http:// किंवा https:// ने होणे आवश्यक आहे."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "व्हर्सेल एआय गेटवे"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "बाहेर झूम करा"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "झुलू"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Permohonan dihantar setiap minggu (8 minggu lepas)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Dimohon"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Dimohon"
|
||||
msgid "Applied on"
|
||||
msgstr "Digunakan pada"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Digunakan dengan amaran"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Bahasa Arab"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Masukkan kata laluan anda untuk mengesahkan penyediaan pengesahan dua fa
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Masukkan kata laluan anda untuk melumpuhkan pengesahan dua faktor. Akaun anda akan menjadi kurang selamat tanpa 2FA diaktifkan."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Ralat"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Gagal mengesahkan kod sandaran anda. Sila cuba lagi."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Gagal mengesahkan kod anda. Sila cuba lagi."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Ciri"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Pratonton"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Pratonton dan eksport gunakan versi terakhir yang sah."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Membaca…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Bersedia untuk diaktifkan"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Bersedia untuk diaktifkan dengan amaran"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Tetapkan Semula"
|
||||
msgid "Reset Password"
|
||||
msgstr "Tetapkan Semula Kata Laluan"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Tetapkan semula kepada helaian gaya yang digunakan"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Tetapkan semula kata laluan anda"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Sesetengah sistem aplikasi memerlukan satu sebelum anda boleh menyerahka
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Sesetengah penghurai salah mengendalikan imej, dan foto tidak digalakkan di sesetengah wilayah."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Ada yang tidak kena"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor helaian gaya"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Helaian gaya mempunyai ralat"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Pengguna"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL yang sah mesti bermula dengan http:// atau https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Pintu Gerbang AI Vercel"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zum keluar"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "प्रति हप्ता पठाइएका आवेदनहरू (पछिल्ला 8 हप्ता)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "आवेदन गरियो"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "आवेदन गरियो"
|
||||
msgid "Applied on"
|
||||
msgstr "मा लागू गरियो"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "चेतावनीहरू सहित लागू गरियो"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "अरबी"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "दुई-कारक प्रमाणीकरण सेटअप
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "दुई-कारक प्रमाणीकरण अक्षम गर्न आफ्नो पासवर्ड प्रविष्ट गर्नुहोस्। २FA सक्रिय नभएको अवस्थामा तपाईंको खाता कम सुरक्षित हुनेछ।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "त्रुटि"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "तपाईंको ब्याकअप कोड प्रमाण
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "तपाईंको कोड प्रमाणित गर्न असफल भयो। कृपया फेरि प्रयास गर्नुहोस्।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "विशेषताहरू"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "पूर्वावलोकन"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "पूर्वावलोकन र निर्यात अन्तिम मान्य संस्करण प्रयोग गर्नुहोस्।"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "पढ्दै…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "सक्रिय गर्न तयार छ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "चेतावनी सहित सक्रिय गर्न तयार"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "रिसेट गर्नुहोस्"
|
||||
msgid "Reset Password"
|
||||
msgstr "पासवर्ड रिसेट गर्नुहोस्"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "लागू गरिएको शैली पानामा रिसेट गर्नुहोस्"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "तपाईंको पासवर्ड रिसेट गर्नुहोस्"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "तपाईंले पेश गर्न सक्नु अघि
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "केही पार्सरहरूले तस्बिरहरूलाई गलत प्रयोग गर्छन्, र फोटोहरू केही क्षेत्रहरूमा निरुत्साहित हुन्छन्।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "केही गडबड भयो।"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "स्टाइलसिट सम्पादक"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "स्टाइलसिटमा त्रुटिहरू छन्"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "प्रयोगकर्ताहरू"
|
||||
msgid "Uzbek"
|
||||
msgstr "उज्बेक"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "मान्य URL http:// वा https:// बाट सुरु हुनुुपर्छ।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "वर्सेल एआई गेटवे"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "जूम आउट"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "जुलु"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Sollicitaties per week verstuurd (laatste 8 weken)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Gesolliciteerd"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Gesolliciteerd"
|
||||
msgid "Applied on"
|
||||
msgstr "Toegepast op"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Toegepast met waarschuwingen"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabisch"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Voer uw wachtwoord in om het instellen van tweestapsverificatie te beves
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Voer uw wachtwoord in om tweestapsverificatie uit te schakelen. Uw account is minder goed beveiligd zonder 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fout"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Het is niet gelukt om uw back-upcode te verifiëren. Probeer het opnieuw
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Het is niet gelukt om uw code te verifiëren. Probeer het opnieuw."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Functies"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Voorbeeld"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Voor het bekijken en exporteren wordt de laatst geldige versie gebruikt."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lezen…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Klaar om te activeren"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Klaar voor activering met waarschuwingen"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Resetten"
|
||||
msgid "Reset Password"
|
||||
msgstr "Wachtwoord resetten"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Terugzetten naar toegepaste stijlblad"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Reset uw wachtwoord"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Sommige parsers verwerken afbeeldingen verkeerd en foto’s worden in so
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Wissel de begin- en einddatum om."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Er is iets misgegaan."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stijlpagina-editor"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Het stylesheet bevat fouten."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Gebruikers"
|
||||
msgid "Uzbek"
|
||||
msgstr "Oezbeeks"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Geldige URL's moeten beginnen met http:// of https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Uitzoomen"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Søknader sendt per uke (siste 8 uker)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Søkt"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Søkt"
|
||||
msgid "Applied on"
|
||||
msgstr "Påført på"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Brukes med advarsler"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabisk"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Skriv inn passordet ditt for å bekrefte at du vil sette opp tofaktoraut
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Skriv inn passordet ditt for å deaktivere tofaktorautentisering. Kontoen din vil være mindre sikker uten aktivert 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Feil"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Kunne ikke bekrefte reservekoden din. Prøv igjen."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Kunne ikke bekrefte koden din. Prøv igjen."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funksjoner"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Forhåndsvisning"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Forhåndsvisning og eksport med den siste gyldige versjonen."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Leser …"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Klar til å aktiveres"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Klar til aktivering med advarsler"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Tilbakestill"
|
||||
msgid "Reset Password"
|
||||
msgstr "Tilbakestill passord"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Tilbakestill til brukt stilark"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Tilbakestill passordet ditt"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Noen søknadssystemer krever dette før du kan sende inn."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Noen parsere håndterer bilder dårlig, og bilder frarådes i enkelte regioner."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Noe gikk galt"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stilarkredigeringsprogram"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stilarket inneholder feil"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Brukere"
|
||||
msgid "Uzbek"
|
||||
msgstr "Usbekisk"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Gyldige URL-er må starte med http:// eller https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zoom ut"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "ପ୍ରତି ସପ୍ତାହରେ ପଠାଯାଇଥିବା ଆବେଦନ (ଗତ 8 ସପ୍ତାହ)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "ଆବେଦନ କରାଯାଇଛି"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "ଆବେଦନ କରାଯାଇଛି"
|
||||
msgid "Applied on"
|
||||
msgstr "ପ୍ରୟୋଗ ହୋଇଛି |"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "ଚେତାବନୀ ସହିତ ଲାଗୁ କରାଯାଇଛି"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "ଆରବୀ"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "ଦ୍ୱି-ଘଟକ ପରିଚୟ ପ୍ରମାଣିକରଣ
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "ଦ୍ୱି-ଘଟକ ପରିଚୟ ପ୍ରମାଣିକରଣ ଅସକ୍ରିୟ କରିବା ପାଇଁ ଆପଣଙ୍କ ପାସୱାର୍ଡ ପ୍ରବେଶ କରନ୍ତୁ। 2FA ସକ୍ରିୟ ନ ଥିଲେ ଆପଣଙ୍କ ଆକାଉଣ୍ଟ କମ୍ ସୁରକ୍ଷିତ ହେବ।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "ତ୍ରୁଟି"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "ଆପଣଙ୍କ ବ୍ୟାକଅପ୍ କୋଡ୍ ସତ୍ୟା
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "ଆପଣଙ୍କ କୋଡ୍ ସତ୍ୟାପନ କରିବାରେ ବିଫଳ ହେଲା। ଦୟାକରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "ବିଶେଷତାଗୁଡିକ"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "ପୂର୍ବାବଲୋକନ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "ପୂର୍ବାବଲୋକନ ଏବଂ ରପ୍ତାନି ଶେଷ ବୈଧ ସଂସ୍କରଣ ବ୍ୟବହାର କରନ୍ତୁ।"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "ପ Reading ିବା…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "ସକ୍ରିୟ କରିବାକୁ ପ୍ରସ୍ତୁତ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "ଚେତାବନୀ ସହିତ ସକ୍ରିୟ କରିବାକୁ ପ୍ରସ୍ତୁତ"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "ରିସେଟ୍ କରନ୍ତୁ"
|
||||
msgid "Reset Password"
|
||||
msgstr "ପାସୱାର୍ଡ ରିସେଟ୍ କରନ୍ତୁ"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "ଲାଗୁ ହୋଇଥିବା ଷ୍ଟାଇଲସିଟକୁ ରିସେଟ୍ କରନ୍ତୁ"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "ଆପଣଙ୍କ ପାସୱାର୍ଡ ରିସେଟ୍ କରନ୍ତୁ"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "କିଛି ଆବେଦନ ସିଷ୍ଟମ୍ରେ ଦାଖଲ
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "କିଛି ପାର୍ସର୍ ଚିତ୍ରକୁ ଠିକ୍ଭାବେ ପ୍ରକ୍ରିୟା କରିପାରେ ନାହିଁ, ଏବଂ କିଛି ଅଞ୍ଚଳରେ ଫଟୋ ନିରୁତ୍ସାହିତ କରାଯାଏ।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "କିଛି ତ୍ରୁଟି ହୋଇଗଲା"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "ଷ୍ଟାଇଲସିଟ୍ ଏଡିଟର୍"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "ଷ୍ଟାଇଲସିଟରେ ତ୍ରୁଟି ଅଛି"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "ଉପଯୋଗକର୍ତ୍ତାମାନେ"
|
||||
msgid "Uzbek"
|
||||
msgstr "ଉଜବେକ୍"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "ବୈଧ URL http:// କିମ୍ବା https:// ସହ ଆରମ୍ଭ ହେବା ଉଚିତ।"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "ଜୁମ୍ ଆଉଟ୍ କରନ୍ତୁ"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ଜୁଲୁ"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Aplikacje wysłane tygodniowo (ostatnie 8 tygodni)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Złożono aplikację"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Złożono aplikację"
|
||||
msgid "Applied on"
|
||||
msgstr "Zastosowano"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Zastosowano z ostrzeżeniami"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabski"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Wprowadź hasło, aby potwierdzić konfigurację uwierzytelniania dwusk
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Wprowadź hasło, aby wyłączyć uwierzytelnianie dwuskładnikowe. Twoje konto będzie mniej bezpieczne bez włączonego 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Błąd"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Nie udało się zweryfikować kodu kopii zapasowej. Proszę spróbować
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Nie udało się zweryfikować kodu. Proszę spróbować ponownie."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkcje"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Zapowiedź"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Podczas podglądu i eksportu użyj ostatniej ważnej wersji."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Czytanie…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Gotowy do aktywacji"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Gotowy do aktywacji z ostrzeżeniami"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Zresetuj"
|
||||
msgid "Reset Password"
|
||||
msgstr "Zresetuj hasło"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Resetuj do zastosowanego arkusza stylów"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Zresetuj swoje hasło"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Niektóre systemy rekrutacyjne wymagają go przed wysłaniem zgłoszenia
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Niektóre parsery nieprawidłowo obsługują obrazy, a w niektórych regionach odradza się dodawanie zdjęć."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Coś poszło nie tak"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Edytor arkuszy stylów"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Arkusz stylów zawiera błędy"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Użytkownicy"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbecki"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Prawidłowe adresy URL muszą zaczynać się od http:// lub https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Pomniejsz"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Candidaturas enviadas por semana (últimas 8 semanas)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Aplicado"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Aplicado"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplicado em"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Aplicado com ressalvas."
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Árabe"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Digite sua senha para confirmar a configuração da autenticação de do
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Digite sua senha para desativar a autenticação de dois fatores. Sua conta ficará menos segura sem o 2FA ativado."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Erro"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Falha ao verificar seu código de backup. Tente novamente."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Falha ao verificar seu código. Por favor, tente novamente."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Recursos"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Pré-visualização"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "A pré-visualização e a exportação utilizam a última versão válida."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lendo…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Pronto para ativar"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Pronto para ativar com avisos"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Redefinir"
|
||||
msgid "Reset Password"
|
||||
msgstr "Redefinir senha"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Redefinir para a folha de estilo aplicada"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Redefinir a sua senha"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Alguns sistemas de candidatura exigem um número de telefone antes que v
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Alguns analisadores lidam mal com imagens, e fotos são desencorajadas em algumas regiões."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Algo deu errado"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor de folhas de estilo"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "A folha de estilo contém erros."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Usuários"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbeque"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URLs válidos devem começar com http:// ou https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Gateway de IA da Vercel"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Diminuir zoom"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Candidaturas enviadas por semana (últimas 8 semanas)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Candidatura enviada"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Candidatura enviada"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplicado em"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Aplicado com reservas."
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Árabe"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Introduza a sua senha para confirmar a configuração da autenticação
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Introduza a sua senha para desativar a autenticação de dois fatores. A sua conta ficará menos segura sem a 2FA ativada."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Erro"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Não foi possível verificar o seu código de cópia de segurança. Tent
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Não foi possível verificar o seu código. Por favor, tente novamente."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funcionalidades"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Pré-visualização"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "A pré-visualização e a exportação utilizam a última versão válida."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Leitura…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Pronto para ativar"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Pronto para ativar com avisos"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Repor"
|
||||
msgid "Reset Password"
|
||||
msgstr "Repor senha"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Repor para a folha de estilo aplicada"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Repor a sua senha"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Alguns sistemas de inscrição exigem um antes que você possa enviar."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Alguns analisadores manipulam imagens incorretamente e as fotos são desencorajadas em algumas regiões."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Algo correu mal"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor de folhas de estilo"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "A folha de estilos contém erros."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Utilizadores"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbeque"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URLs válidos devem começar por http:// ou https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Afastar"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Candidaturi trimise pe săptămână (ultimele 8 săptămâni)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Candidatură trimisă"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Candidatură trimisă"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplicat pe"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Aplicat cu avertismente"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabă"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Introduceți parola pentru a confirma configurarea autentificării cu do
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Introduceți parola pentru a dezactiva autentificarea cu doi factori. Contul va fi mai puțin sigur fără 2FA activat."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Eroare"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Nu s-a reușit verificarea codului dvs. de rezervă. Vă rugăm să înc
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Nu s-a reușit verificarea codului dvs. Vă rugăm să încercați din nou."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funcționalități"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Previzualizare"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Previzualizarea și exportarea utilizează ultima versiune validă."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Lectură…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Gata de activare"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Gata de activare cu avertismente"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Resetează"
|
||||
msgid "Reset Password"
|
||||
msgstr "Resetați parola"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Resetare la foaia de stil aplicată"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Resetați-vă parola"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Unele sisteme de aplicații necesită unul înainte de a putea trimite."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Unii analizatori gestionează greșit imaginile, iar fotografiile sunt descurajate în unele regiuni."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Ceva nu a mers bine"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor de foi de stil"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Foaia de stil conține erori"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Utilizatori"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbecă"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL-urile valide trebuie să înceapă cu http:// sau https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Micșorează"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Заявки, отправленные за неделю (последние 8 недель)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Отклик отправлен"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Отклик отправлен"
|
||||
msgid "Applied on"
|
||||
msgstr "Применяется на"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Применяется с предупреждениями."
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Арабский"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Введите пароль, чтобы подтвердить наст
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Введите пароль, чтобы отключить двухфакторную аутентификацию. Без 2FA ваша учетная запись будет менее защищена."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Ошибка"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Не удалось проверить Ваш код резервног
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Не удалось проверить Ваш код. Пожалуйста, попробуйте еще раз."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Возможности"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Предварительный просмотр"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Для предварительного просмотра и экспорта используется последняя действующая версия."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Чтение…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Готово к активации"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Готов к активации с предупреждениями."
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Сброс"
|
||||
msgid "Reset Password"
|
||||
msgstr "Сбросить пароль"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Сбросить до примененной таблицы стилей"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Сбросить пароль"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Некоторые системы подачи заявок требую
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Некоторые парсеры неправильно обрабатывают изображения, а в некоторых регионах фотографии не рекомендуются."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Что-то пошло не так."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Редактор таблиц стилей"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "В таблице стилей обнаружены ошибки."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Пользователи"
|
||||
msgid "Uzbek"
|
||||
msgstr "Узбекский"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Корректные URL должны начинаться с http:// или https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Уменьшить"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Зулу"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Žiadosti odoslané za týždeň (posledných 8 týždňov)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Odoslané"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Odoslané"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplikované dňa"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Použité s upozorneniami"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabčina"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Zadaj svoje heslo na potvrdenie nastavenia dvojfaktorového overenia. Po
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Zadaj svoje heslo na vypnutie dvojfaktorového overenia. Bez zapnutého 2FA bude tvoj účet menej zabezpečený."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Chyba"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Nepodarilo sa overiť váš záložný kód. Skúste to prosím znova."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Nepodarilo sa overiť váš kód. Skúste to prosím znova."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkcie"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Náhľad"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Pri ukážke a exporte sa použije posledná platná verzia."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Číta sa…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Pripravené na aktiváciu"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Pripravené na aktiváciu s upozorneniami"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Resetovať"
|
||||
msgid "Reset Password"
|
||||
msgstr "Obnoviť heslo"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Obnoviť na použitý štýlový hárok"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Obnov svoje heslo"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Niektoré aplikačné systémy vyžadujú pred odoslaním žiadosť."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Niektoré analyzátory nesprávne narábajú s obrázkami a v niektorých regiónoch sa fotografie neodporúčajú."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Niečo sa pokazilo"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Editor štýlov"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Štýlový hárok obsahuje chyby"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Používatelia"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbečtina"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Platné URL adresy musia začínať http:// alebo https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Brána Vercel AI"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Oddialiť"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Prijave, poslane na teden (zadnjih 8 tednov)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Prijavljeno"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Prijavljeno"
|
||||
msgid "Applied on"
|
||||
msgstr "Uporabljeno na"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Uporabljeno z opozorili"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabščina"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Vnesite svoje geslo, da potrdite nastavitev dvostopenjskega preverjanja
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Vnesite svoje geslo za onemogočanje dvostopenjskega preverjanja pristnosti. Vaš račun bo brez omogočenega 2FA manj varen."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Napaka"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Varnostne kode ni uspelo preveriti. Poskusite znova."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Ni uspelo preveriti vaše kode. Poskusite znova."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funkcionalnosti"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Predogled"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Predogled in izvoz uporabljata zadnjo veljavno različico."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Branje ..."
|
||||
msgid "Ready to activate"
|
||||
msgstr "Pripravljeno za aktivacijo"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Pripravljeno za aktivacijo z opozorili"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Ponastavi"
|
||||
msgid "Reset Password"
|
||||
msgstr "Ponastavi geslo"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Ponastavi na uporabljeno slogovno predlogo"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Ponastavite svoje geslo"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Nekateri sistemi prijav zahtevajo, da jo lahko oddate."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Nekateri razčlenjevalniki napačno obravnavajo slike, zato fotografije v nekaterih regijah odsvetujejo."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Nekaj je šlo narobe"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Urejevalnik slogovnih predlog"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Slogovna predloga vsebuje napake"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Uporabniki"
|
||||
msgid "Uzbek"
|
||||
msgstr "uzbeščina"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Veljavni URL-ji se morajo začeti z http:// ali https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Oddalji"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zuluščina"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Aplikimet e dërguara për javë (8 javët e fundit)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Aplikuar"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Aplikuar"
|
||||
msgid "Applied on"
|
||||
msgstr "Aplikuar në"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Zbatuar me paralajmërime"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabisht"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Shkruani fjalëkalimin tuaj për të konfirmuar konfigurimin e vërtetim
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Shkruani fjalëkalimin tuaj për të çaktivizuar vërtetimin me dy faktorë. Llogaria juaj do të jetë më pak e sigurt pa 2FA të aktivizuar."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Gabim"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Dështoi verifikimi i kodit tuaj të rezervës. Ju lutemi provoni përs
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Dështoi verifikimi i kodit tuaj. Ju lutemi provoni përsëri."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Veçori"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Pamje paraprake"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Parapamja dhe eksportimi përdorin versionin e fundit të vlefshëm."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Duke lexuar…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Gati për t'u aktivizuar"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Gati për t'u aktivizuar me paralajmërime"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Rivendos"
|
||||
msgid "Reset Password"
|
||||
msgstr "Rivendos fjalëkalimin"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Rivendos në fletën e stilit të aplikuar"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Rivendosni fjalëkalimin tuaj"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Disa sisteme aplikimi kërkojnë një para se të mund të dorëzoni."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Disa analizues i keqpërdorin imazhet dhe fotot dekurajohen në disa rajone."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Diçka shkoi keq"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Redaktuesi i fletës së stilit"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Fleta e stilit ka gabime"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Përdorues"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbeke"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL-të e vlefshme duhet të fillojnë me http:// ose https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Porta Vercel AI"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zvogëlo"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Пријаве послате недељно (последњих 8 недеља)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Пријављено"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Пријављено"
|
||||
msgid "Applied on"
|
||||
msgstr "Примењено на"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Примењено са упозорењима"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Арапски"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Унесите своју лозинку да потврдите под
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Унесите своју лозинку да бисте онемогућили двофакторску аутентификацију. Ваш налог ће бити мање безбедан без омогућеног 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Грешка"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Није успело да се потврди ваш резервни
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Није успело да се потврди ваш код. Молимо покушајте поново."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Функције"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Преглед"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Преглед и извоз користе последњу важећу верзију."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Читање…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Спремно за активирање"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Спремно за активирање са упозорењима"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Ресетуј"
|
||||
msgid "Reset Password"
|
||||
msgstr "Ресетуј лозинку"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Врати на примењени стилски лист"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Ресетујте своју лозинку"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Неки системи апликација захтевају једа
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Неки парсери погрешно рукују сликама, а фотографије су обесхрабрене у неким регионима."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Нешто је пошло наопако"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Уређивач стилских листова"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Стилски лист садржи грешке"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Корисници"
|
||||
msgid "Uzbek"
|
||||
msgstr "Узбечки"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Важећи URL-ови морају да почињу са http:// или https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Версел АИ Гејтвеј"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Умањи"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Зулу"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Ansökningar skickade per vecka (senaste 8 veckorna)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Ansökt"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Ansökt"
|
||||
msgid "Applied on"
|
||||
msgstr "Tillämpas på"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Tillämpas med varningar"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arabiska"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Ange ditt lösenord för att bekräfta att du vill ställa in tvåfaktor
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Ange ditt lösenord för att inaktivera tvåfaktorsautentisering. Ditt konto blir mindre säkert utan aktiverad 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fel"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Det gick inte att verifiera din säkerhetskopieringskod. Vänligen förs
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Det gick inte att verifiera din kod. Vänligen försök igen."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Funktioner"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Förhandsvisning"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Förhandsgranska och exportera med den senast giltiga versionen."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Läsning…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Klar att aktiveras"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Klar att aktiveras med varningar"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Återställ"
|
||||
msgid "Reset Password"
|
||||
msgstr "Återställ lösenord"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Återställ till tillämpat formatmall"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Återställ ditt lösenord"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Vissa ansökningssystem kräver ett innan du kan skicka in."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Vissa tolkar hanterar bilder felaktigt och foton avråds från vissa regioner."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Något gick fel"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stilarksredigerare"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stilarket innehåller fel"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Användare"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbekiska"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Giltiga URL:er måste börja med http:// eller https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Zooma ut"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "வாரத்திற்கு அனுப்பப்பட்ட விண்ணப்பங்கள் (கடைசி 8 வாரங்கள்)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "விண்ணப்பிக்கப்பட்டது"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "விண்ணப்பிக்கப்பட்டது"
|
||||
msgid "Applied on"
|
||||
msgstr "அன்று விண்ணப்பித்தது"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "எச்சரிக்கைகளுடன் கூடிய அறிவு"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "அரபிக்"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "இரண்டு நிலை அங்கீகாரத்தை அ
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "இரண்டு நிலை அங்கீகாரத்தை முடக்க உங்கள் கடவுச்சொல்லை உள்ளிடவும். 2FA இயங்காவிட்டால் உங்கள் கணக்கு குறைந்த பாதுகாப்புடன் இருக்கும்."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "பிழை"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "உங்கள் காப்புக் குறியீட்ட
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "உங்கள் குறியீட்டைச் சரிபார்க்கத் தவறியது. மீண்டும் முயற்சிக்கவும்."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "அம்சங்கள்"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "முன்னோட்டம்"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "முன்னோட்டம் மற்றும் ஏற்றுமதிக்கு, கடைசியாகச் செல்லுபடியாகும் பதிப்பைப் பயன்படுத்தவும்."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "படிக்கிறது…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "செயல்படுத்தத் தயார்"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "எச்சரிக்கைகளுடன் செயல்படுத்தத் தயார்"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "மீட்டமை"
|
||||
msgid "Reset Password"
|
||||
msgstr "கடவுச்சொல்லை மீட்டமைக்கவும்"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "பயன்படுத்தப்பட்ட பாணித் தாளுக்கு மீட்டமைக்கவும்"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "உங்கள் கடவுச்சொல்லை மீட்டமைக்கவும்"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "நீங்கள் சமர்ப்பிக்கும் மு
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "சில பாகுபடுத்துபவர்கள் படங்களை தவறாகக் கையாளுகின்றனர், மேலும் சில பகுதிகளில் புகைப்படங்கள் ஊக்கமளிக்கவில்லை."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "ஏதோ தவறு நடந்துவிட்டது"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "ஸ்டைல்ஷீட் எடிட்டர்"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "ஸ்டைல்ஷீட்டில் பிழைகள் உள்ளன"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "பயனர்கள்"
|
||||
msgid "Uzbek"
|
||||
msgstr "உஸ்பெக்"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "சரியான URL-கள் http:// அல்லது https:// கொண்டு தொடங்க வேண்டும்."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "வெர்சல் ஏஐ கேட்வே"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "சிறிதாக்கு"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ஜூலு"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "వారానికి పంపిన దరఖాస్తులు (చివరి 8 వారాలు)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "దరఖాస్తు చేశారు"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "దరఖాస్తు చేశారు"
|
||||
msgid "Applied on"
|
||||
msgstr "దరఖాస్తు చేసిన తేదీ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "హెచ్చరికలతో వర్తింపజేయబడింది"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "అరబిక్"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "రెండు దశల ధృవీకరణ ఏర్పాటున
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "రెండు దశల ధృవీకరణను నిలిపివేయడానికి మీ పాస్వర్డ్ని నమోదు చేయండి. 2FA ప్రారంభించబడకుండా ఉంటే మీ ఖాతా తక్కువ భద్రతగా ఉంటుంది."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "లోపం"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "మీ బ్యాకప్ కోడ్ను ధృవీకరి
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "మీ కోడ్ను ధృవీకరించడంలో విఫలమయ్యాము. దయచేసి మళ్ళీ ప్రయత్నించండి."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "ఫీచర్లు"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "ప్రివ్యూ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "ప్రివ్యూ మరియు ఎక్స్పోర్ట్ చివరి చెల్లుబాటు అయ్యే వెర్షన్ను ఉపయోగిస్తాయి."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "చదువుతోంది…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "యాక్టివేట్ చేయడానికి సిద్ధంగా ఉంది"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "హెచ్చరికలతో యాక్టివేట్ చేయడానికి సిద్ధంగా ఉంది"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "రీసెట్ చేయండి"
|
||||
msgid "Reset Password"
|
||||
msgstr "పాస్వర్డ్ రీసెట్ చేయండి"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "వర్తింపజేసిన స్టైల్షీట్కు రీసెట్ చేయండి"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "మీ పాస్వర్డ్ను రీసెట్ చేయండి"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "మీరు సమర్పించే ముందు కొన్న
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "కొంతమంది పార్సర్లు చిత్రాలను తప్పుగా నిర్వహిస్తారు మరియు కొన్ని ప్రాంతాలలో ఫోటోలు నిరుత్సాహపరుస్తాయి."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "ఏదో తప్పు జరిగింది"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "స్టైల్షీట్ ఎడిటర్"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "స్టైల్షీట్లో లోపాలు ఉన్నాయి"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "వినియోగదారులు"
|
||||
msgid "Uzbek"
|
||||
msgstr "ఉజ్బెక్"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "చెల్లే URLలు తప్పనిసరిగా http:// లేదా https://తో ప్రారంభించాలి."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "వర్సెల్ AI గేట్వే"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "జూమ్ అవుట్ చేయండి"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "జులు"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "ใบสมัครที่ส่งต่อสัปดาห์ (8 สัปดาห์ล่าสุด)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "สมัครแล้ว"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "สมัครแล้ว"
|
||||
msgid "Applied on"
|
||||
msgstr "สมัครเมื่อ"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "ใช้งานโดยมีคำเตือน"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "อารบิก"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "กรอกรหัสผ่านของคุณเพื่อย
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "กรอกรหัสผ่านของคุณเพื่อปิดการใช้งานการยืนยันตัวตนสองขั้นตอน บัญชีของคุณจะไม่ปลอดภัยเท่าเดิมหากไม่ได้เปิดใช้งาน 2FA"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "ข้อผิดพลาด"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "ไม่สามารถยืนยันรหัสสำรอง
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "ไม่สามารถยืนยันรหัสของคุณได้ กรุณาลองอีกครั้ง"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "คุณสมบัติ"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "ตัวอย่าง"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "ให้ใช้เวอร์ชันล่าสุดที่ถูกต้องในการแสดงตัวอย่างและการส่งออก"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "กำลังอ่าน…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "พร้อมใช้งานแล้ว"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "พร้อมใช้งานพร้อมคำเตือน"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "รีเซ็ต"
|
||||
msgid "Reset Password"
|
||||
msgstr "รีเซ็ตรหัสผ่าน"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "รีเซ็ตเป็นสไตล์ชีตที่ใช้งานอยู่"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "รีเซ็ตรหัสผ่านของคุณ"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "ระบบการสมัครบางระบบจำเป็
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "โปรแกรมแยกวิเคราะห์บางคนจัดการรูปภาพในทางที่ผิด และรูปภาพไม่ได้รับการสนับสนุนในบางภูมิภาค"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "เกิดข้อผิดพลาดบางอย่าง"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "ตัวแก้ไขสไตล์ชีต"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "ไฟล์สไตล์ชีตมีข้อผิดพลาด"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "ผู้ใช้"
|
||||
msgid "Uzbek"
|
||||
msgstr "อุซเบก"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL ที่ถูกต้องต้องขึ้นต้นด้วย http:// หรือ https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "เกตเวย์ AI ของ Vercel"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "ซูมออก"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "ซูลู"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Haftalık gönderilen başvurular (son 8 hafta)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Başvuruldu"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Başvuruldu"
|
||||
msgid "Applied on"
|
||||
msgstr "Başvuru tarihi"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Uyarılarla birlikte uygulandı."
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arapça"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "İki faktörlü kimlik doğrulamayı kurmayı onaylamak için şifrenizi
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "İki faktörlü kimlik doğrulamayı devre dışı bırakmak için şifrenizi girin. 2FA etkin olmadığında hesabınız daha az güvenli olur."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Hata"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Yedekleme kodunuz doğrulanamadı. Lütfen tekrar deneyin."
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Kodunuz doğrulanamadı. Lütfen tekrar deneyin."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Özellikler"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Önizleme"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Önizleme ve dışa aktarma işlemleri için en son geçerli sürüm kullanılır."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Okunuyor…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Etkinleştirmeye hazır"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Uyarılarla birlikte etkinleştirmeye hazır."
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Sıfırla"
|
||||
msgid "Reset Password"
|
||||
msgstr "Şifreyi Sıfırla"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Uygulanan stil sayfasına sıfırla"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Şifrenizi sıfırlayın"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Bazı başvuru sistemleri, gönderebilmeniz için bir başvuru gerektiri
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Bazı ayrıştırıcılar görüntüleri yanlış kullanıyor ve bazı bölgelerde fotoğraf kullanılması önerilmez."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Bir şeyler ters gitti."
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Stil sayfası düzenleyici"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Stil dosyasında hatalar var."
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Kullanıcılar"
|
||||
msgid "Uzbek"
|
||||
msgstr "Özbekçe"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Geçerli URL'ler http:// veya https:// ile başlamalıdır."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Ağ Geçidi"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Uzaklaştır"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Заявки, надіслані за тиждень (останні 8 тижнів)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Подано"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Подано"
|
||||
msgid "Applied on"
|
||||
msgstr "Подано"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Застосовується з попередженнями"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Арабська"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Введіть пароль для підтвердження налаш
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Введіть свій пароль для вимикання двофакторної автентифікації. Ваш профіль буде менш захищений без увімкненого 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Помилка"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Не вдалося підтвердити код резервної к
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Не вдалося підтвердити ваш код. Спробуйте ще раз."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Функції"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Попередній перегляд"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Для попереднього перегляду та експорту використовується остання дійсна версія."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Читання…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Готовий до активації"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Готовий до активації з попередженнями"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Скинути"
|
||||
msgid "Reset Password"
|
||||
msgstr "Скинути пароль"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Скинути до застосованого таблиці стилів"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Скинути пароль"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Деякі системи заявок вимагають її, перш
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Деякі аналізатори неправильно обробляють зображення, тому в деяких регіонах фотографії не рекомендуються."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Щось пішло не так"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Редактор таблиць стилів"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Таблиця стилів містить помилки"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Користувачі"
|
||||
msgid "Uzbek"
|
||||
msgstr "Узбецька"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Дійсні URL-адреси мають починатися з http:// або https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Шлюз штучного інтелекту Vercel"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Зменшити"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Зулу"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Haftasiga yuborilgan arizalar (so‘nggi 8 hafta)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Topshirildi"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Topshirildi"
|
||||
msgid "Applied on"
|
||||
msgstr "Ariza sanasi"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Ogohlantirishlar bilan qo'llanildi"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Arab tili"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Ikki bosqichli autentifikatsiyani yoqishni tasdiqlash uchun parolingizni
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Ikki bosqichli autentifikatsiyani o‘chirishni tasdiqlash uchun parolingizni kiriting. 2BShT yoqilmagan holda hisobingiz kamroq xavfsiz boʻladi."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Xato"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Zaxira kodingizni tasdiqlash muvaffaqiyatsiz bo'ldi. Iltimos, yana urini
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Kodingizni tekshirish muvaffaqiyatsiz bo'ldi. Iltimos, yana urinib ko'ring."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Xususiyatlar"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Oldindan ko'rish"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Oldindan ko'rish va eksport qilish uchun oxirgi amaldagi versiyadan foydalaning."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "O'qilmoqda…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Faollashtirishga tayyor"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Ogohlantirishlar bilan faollashtirishga tayyor"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Tiklash"
|
||||
msgid "Reset Password"
|
||||
msgstr "Parolni tiklash"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Qo'llanilgan uslublar jadvaliga qayta o'rnatish"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Parolingizni tiklang"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Ba'zi dastur tizimlari siz yuborishingizdan oldin uni talab qiladi."
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Ba'zi tahlilchilar tasvirlarni noto'g'ri ishlatishadi va ba'zi hududlarda fotosuratlar tavsiya etilmaydi."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Nimadir noto'g'ri ketdi"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Uslublar jadvali muharriri"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Uslublar jadvalida xatolar mavjud"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Foydalanuvchilar"
|
||||
msgid "Uzbek"
|
||||
msgstr "O'zbek tili"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "Toʻgʻri URL http:// yoki https:// bilan boshlanishi kerak."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Kichraytirish"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "Đơn ứng tuyển đã gửi mỗi tuần (8 tuần gần nhất)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "Đã ứng tuyển"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "Đã ứng tuyển"
|
||||
msgid "Applied on"
|
||||
msgstr "Đã ứng tuyển vào"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "Được áp dụng kèm theo cảnh báo"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "Tiếng Ả Rập"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "Nhập mật khẩu của bạn để xác nhận thiết lập xác th
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "Nhập mật khẩu của bạn để tắt xác thực hai yếu tố. Tài khoản của bạn sẽ kém an toàn hơn nếu không bật 2FA."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "Lỗi"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "Không thể xác minh mã dự phòng của bạn. Vui lòng thử lạ
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "Không thể xác minh mã của bạn. Vui lòng thử lại."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "Tính năng"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "Xem trước"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "Chức năng xem trước và xuất khẩu sử dụng phiên bản hợp lệ mới nhất."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "Đang đọc…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "Sẵn sàng kích hoạt"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "Sẵn sàng kích hoạt kèm cảnh báo"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "Thiết lập lại"
|
||||
msgid "Reset Password"
|
||||
msgstr "Đặt mật khẩu mới"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "Đặt lại về kiểu định dạng đã áp dụng"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "Đặt lại mật khẩu"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "Một số hệ thống ứng dụng yêu cầu phải có trước khi
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "Một số trình phân tích cú pháp xử lý sai hình ảnh và ảnh không được khuyến khích ở một số vùng."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "Đã xảy ra lỗi"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "Trình chỉnh sửa bảng định kiểu"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "Tệp định kiểu có lỗi"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "Người dùng"
|
||||
msgid "Uzbek"
|
||||
msgstr "Uzbek"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "URL hợp lệ phải bắt đầu với http:// hoặc https://."
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Cổng AI Vercel"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "Thu nhỏ"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "Zulu"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "每周发送的申请(最近 8 周)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "已申请"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "已申请"
|
||||
msgid "Applied on"
|
||||
msgstr "申请日期"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "附带警告"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "阿拉伯语"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "请输入你的密码以确认设置双重身份验证。启用后,你
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "请输入你的密码以禁用双重身份验证。关闭 2FA 后,你的账号将不再那么安全。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "验证备份代码失败。请重试。"
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "验证您的代码失败。请重试。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "功能"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "预览"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "预览和导出时请使用最新有效版本。"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "正在读取…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "准备激活"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "准备激活并发出警告"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "重置"
|
||||
msgid "Reset Password"
|
||||
msgstr "重置密码"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "重置为已应用的样式表"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "重置密码"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "有些申请系统需要先有一个,然后才能提交。”"
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "一些解析器错误地处理图像,并且在某些地区不鼓励使用照片。”"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "出问题了"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "样式表编辑器"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "样式表存在错误"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "用户数"
|
||||
msgid "Uzbek"
|
||||
msgstr "乌兹别克语"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "有效的 URL 必须以 http:// 或 https:// 开头。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel 人工智能网关"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "缩小"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "祖鲁语"
|
||||
|
||||
|
||||
+32
-15
@@ -526,7 +526,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr "每週送出的申請(最近 8 週)"
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr "已申請"
|
||||
|
||||
@@ -534,10 +533,6 @@ msgstr "已申請"
|
||||
msgid "Applied on"
|
||||
msgstr "應徵日期"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr "附帶警告"
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr "阿拉伯文"
|
||||
@@ -1765,7 +1760,6 @@ msgstr "請輸入您的密碼以確認設定雙因子驗證。啟用後,每次
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr "請輸入您的密碼以停用雙因子驗證。停用後,您的帳戶將會比較不安全。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr "錯誤"
|
||||
@@ -2003,6 +1997,10 @@ msgstr "驗證您的備份碼失敗。請再試一次。"
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr "驗證您的驗證碼失敗。請再試一次。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr "功能"
|
||||
@@ -3420,8 +3418,12 @@ msgid "Preview"
|
||||
msgstr "預覽"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgstr "預覽和匯出時請使用最新有效版本。"
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
msgid "Primary Color"
|
||||
@@ -3542,6 +3544,10 @@ msgstr "正在讀取…"
|
||||
msgid "Ready to activate"
|
||||
msgstr "準備啟用"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr "準備啟動並發出警告"
|
||||
@@ -3663,10 +3669,6 @@ msgstr "重置"
|
||||
msgid "Reset Password"
|
||||
msgstr "重設密碼"
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr "重設為已套用的樣式表"
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr "重設您的密碼"
|
||||
@@ -4195,6 +4197,10 @@ msgstr "有些申請系統需要先有一個,然後再提交。 」"
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr "有些解析器錯誤地處理圖像,並且在某些地區不鼓勵使用照片。 」"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr "出問題了"
|
||||
@@ -4338,8 +4344,8 @@ msgid "Stylesheet editor"
|
||||
msgstr "樣式表編輯器"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgstr "樣式表存在錯誤"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
msgid "Subreddit"
|
||||
@@ -5103,10 +5109,22 @@ msgstr "使用者"
|
||||
msgid "Uzbek"
|
||||
msgstr "烏茲別克語"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr "有效的 URL 必須以 http:// 或 https:// 開頭。"
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr "Vercel AI Gateway"
|
||||
@@ -5411,4 +5429,3 @@ msgstr "縮小"
|
||||
#: src/libs/locale.ts
|
||||
msgid "Zulu"
|
||||
msgstr "祖魯語"
|
||||
|
||||
|
||||
+30
-12
@@ -521,7 +521,6 @@ msgid "Applications sent per week (last 8 weeks)"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/applications/components/table-view.tsx
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied"
|
||||
msgstr ""
|
||||
|
||||
@@ -529,10 +528,6 @@ msgstr ""
|
||||
msgid "Applied on"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Applied with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/libs/locale.ts
|
||||
msgid "Arabic"
|
||||
msgstr ""
|
||||
@@ -1760,7 +1755,6 @@ msgstr ""
|
||||
msgid "Enter your password to disable two-factor authentication. Your account will be less secure without 2FA enabled."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/ats-check.tsx
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
@@ -1998,6 +1992,10 @@ msgstr ""
|
||||
msgid "Failed to verify your code. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Fatal error"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/features.tsx
|
||||
msgid "Features"
|
||||
msgstr ""
|
||||
@@ -3415,7 +3413,11 @@ msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export use the last valid version."
|
||||
msgid "Preview and export fall back to base styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Preview and export keep valid styles and ignore invalid styles."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/builder/$resumeId/-sidebar/right/sections/design.tsx
|
||||
@@ -3537,6 +3539,10 @@ msgstr ""
|
||||
msgid "Ready to activate"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Ready to activate with warnings"
|
||||
msgstr ""
|
||||
@@ -3658,10 +3664,6 @@ msgstr ""
|
||||
msgid "Reset Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/toolbar.tsx
|
||||
msgid "Reset to applied stylesheet"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/auth/pages/reset-password.tsx
|
||||
msgid "Reset your password"
|
||||
msgstr ""
|
||||
@@ -4190,6 +4192,10 @@ msgstr ""
|
||||
msgid "Some parsers mishandle images, and photos are discouraged in some regions."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Some styles were ignored"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/error-screen.tsx
|
||||
msgid "Something went wrong"
|
||||
msgstr ""
|
||||
@@ -4333,7 +4339,7 @@ msgid "Stylesheet editor"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Stylesheet has errors"
|
||||
msgid "Stylesheet has fatal errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/_home/-sections/footer.tsx
|
||||
@@ -5098,10 +5104,22 @@ msgstr ""
|
||||
msgid "Uzbek"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/input/rich-input.tsx
|
||||
msgid "Valid URLs must start with http:// or https://."
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/resume/stylesheet/status.tsx
|
||||
msgid "Valid with warnings"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/settings/integrations/components/ai-section.tsx
|
||||
msgid "Vercel AI Gateway"
|
||||
msgstr ""
|
||||
|
||||
@@ -35,10 +35,6 @@ const toastMocks = vi.hoisted(() => ({
|
||||
error: vi.fn(() => "sync-error-toast"),
|
||||
}));
|
||||
|
||||
const stylesheetMocks = vi.hoisted(() => ({
|
||||
refresh: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@orpc/client", () => ({
|
||||
consumeEventIterator: consumeEventIteratorMock,
|
||||
}));
|
||||
@@ -81,10 +77,6 @@ vi.mock("sonner", () => ({
|
||||
toast: toastMocks,
|
||||
}));
|
||||
|
||||
vi.mock("@/features/resume/stylesheet/store", () => ({
|
||||
refreshStylesheetStore: stylesheetMocks.refresh,
|
||||
}));
|
||||
|
||||
function cloneResumeData(data: ResumeData): ResumeData {
|
||||
return structuredClone(data);
|
||||
}
|
||||
@@ -135,7 +127,6 @@ describe("builder resume autosave", () => {
|
||||
i18n.loadAndActivate({ locale: "en-US", messages: {} });
|
||||
toastMocks.dismiss.mockClear();
|
||||
toastMocks.error.mockClear();
|
||||
stylesheetMocks.refresh.mockReset();
|
||||
useResumeStore.getState().reset();
|
||||
});
|
||||
|
||||
@@ -169,6 +160,26 @@ describe("builder resume autosave", () => {
|
||||
expect(orpcMocks.patchResume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("autosaves stylesheet source through the ordinary full-data update", async () => {
|
||||
const initial = makeResume("resume-stylesheet-autosave");
|
||||
const source = { languageVersion: 1, text: "@version 1;\nname { color: blue; }\n" };
|
||||
const updated = makeResume(initial.id);
|
||||
updated.data.metadata.stylesheet = { mode: "semantic", source };
|
||||
orpcMocks.updateResume.mockResolvedValue(updated);
|
||||
useResumeStore.getState().initialize(initial);
|
||||
|
||||
useResumeStore.getState().updateResumeData((draft) => {
|
||||
draft.metadata.stylesheet = { mode: "semantic", source };
|
||||
});
|
||||
vi.advanceTimersByTime(500);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(orpcMocks.updateResume).toHaveBeenCalledWith(
|
||||
{ id: initial.id, data: updated.data },
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("saves the latest pending snapshot after an in-flight save resolves", async () => {
|
||||
const initial = makeResume("resume-in-flight");
|
||||
const first = withBasicsName(initial, "First Name");
|
||||
@@ -507,25 +518,13 @@ describe("resume update stream subscription", () => {
|
||||
expect(useResumeStore.getState().resume?.data.basics.name).toBe("Local Name");
|
||||
});
|
||||
|
||||
it("refetches canonical stylesheet state for stylesheet SSE events", async () => {
|
||||
it("applies stylesheet source from the ordinary resume SSE flow", async () => {
|
||||
const initial = makeResume("resume-stylesheet");
|
||||
consumeEventIteratorMock.mockReturnValue(vi.fn().mockResolvedValue(undefined));
|
||||
routerParamsMock.value = { resumeId: initial.id };
|
||||
useResumeStore.getState().initialize(initial);
|
||||
|
||||
renderHook(() => useBuilderResumeUpdateSubscription());
|
||||
const handlers = consumeEventIteratorMock.mock.calls[0]?.[1] as {
|
||||
onEvent: (event: { mutation: string }) => Promise<void>;
|
||||
const remote = makeResume("resume-stylesheet");
|
||||
remote.data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname { color: blue; }\n" },
|
||||
};
|
||||
await act(async () => handlers.onEvent({ mutation: "stylesheet" }));
|
||||
|
||||
expect(stylesheetMocks.refresh).toHaveBeenCalledWith(initial.id);
|
||||
expect(orpcMocks.getResumeById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refreshes the render-data version after content SSE events", async () => {
|
||||
const initial = makeResume("resume-content");
|
||||
const remote = withBasicsName(initial, "Remote");
|
||||
consumeEventIteratorMock.mockReturnValue(vi.fn().mockResolvedValue(undefined));
|
||||
orpcMocks.getResumeById.mockResolvedValue(remote);
|
||||
routerParamsMock.value = { resumeId: initial.id };
|
||||
@@ -537,6 +536,7 @@ describe("resume update stream subscription", () => {
|
||||
};
|
||||
await act(async () => handlers.onEvent({ mutation: "update" }));
|
||||
|
||||
expect(stylesheetMocks.refresh).toHaveBeenCalledWith(initial.id, remote.data);
|
||||
expect(orpcMocks.getResumeById).toHaveBeenCalledWith({ id: initial.id });
|
||||
expect(useResumeStore.getState().resume?.data.metadata.stylesheet).toEqual(remote.data.metadata.stylesheet);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
import { create } from "zustand/react";
|
||||
import { refreshStylesheetStore } from "@/features/resume/stylesheet/store";
|
||||
import { orpc, streamClient } from "@/libs/orpc/client";
|
||||
|
||||
export type Resume = {
|
||||
@@ -26,7 +25,7 @@ export type Resume = {
|
||||
};
|
||||
|
||||
// Mirrors the server-side ResumeUpdatedEvent discriminator (packages/api resume/events.ts).
|
||||
type ResumeUpdateMutation = "sync" | "create" | "update" | "patch" | "lock" | "password" | "delete" | "stylesheet";
|
||||
type ResumeUpdateMutation = "sync" | "create" | "update" | "patch" | "lock" | "password" | "delete";
|
||||
type ResumeUpdateEvent = { mutation: ResumeUpdateMutation };
|
||||
|
||||
type SaveStatus = "idle" | "saving" | "saved" | "error";
|
||||
@@ -616,14 +615,9 @@ export function useBuilderResumeUpdateSubscription() {
|
||||
if (!resumeId) return;
|
||||
|
||||
bindRuntimeQueryClient(resumeId, queryClient);
|
||||
if (event.mutation === "stylesheet") {
|
||||
await refreshStylesheetStore(resumeId);
|
||||
return;
|
||||
}
|
||||
const resume = (await orpc.resume.getById.call({ id: resumeId })) as Resume;
|
||||
|
||||
queryClient.setQueryData(getResumeQueryKey(resumeId), resume);
|
||||
await refreshStylesheetStore(resumeId, resume.data);
|
||||
|
||||
if (hasPendingLocalChanges(resumeId)) {
|
||||
useResumeStore.getState().mergeResumeMetadata(resume);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { Template } from "@reactive-resume/schema/templates";
|
||||
import { useMemo } from "react";
|
||||
import { createResumePdfBlob as createPdfBlob } from "@reactive-resume/pdf/browser";
|
||||
@@ -11,22 +9,6 @@ type ResumePdfRenderOptions = {
|
||||
includeCoverLetterHeader?: boolean;
|
||||
};
|
||||
|
||||
export type ResumePdfPresentation =
|
||||
| { stylesheet: Pick<SemanticStylesheet, "mode"> & { applied: StylesheetSource } }
|
||||
| { publicStyleProjection: PublicStyleProjection };
|
||||
|
||||
const withAppliedStylesheet = (data: ResumeData, presentation?: ResumePdfPresentation): ResumeData => {
|
||||
if (!presentation || !("stylesheet" in presentation)) return data;
|
||||
const { mode, applied } = presentation.stylesheet;
|
||||
return {
|
||||
...data,
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
stylesheet: { mode, source: applied, applied },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useLocalizedResumeDocument = (data?: ResumeData, template?: Template) => {
|
||||
const sectionTitleResolver = useSectionTitleResolver(data?.metadata.page.locale);
|
||||
|
||||
@@ -47,17 +29,13 @@ export const createResumePdfBlob = async (
|
||||
data: ResumeData,
|
||||
template?: Template,
|
||||
renderOptions?: ResumePdfRenderOptions,
|
||||
presentation?: ResumePdfPresentation,
|
||||
) => {
|
||||
const sectionTitleResolver = await createSectionTitleResolverForLocale(data.metadata.page.locale);
|
||||
|
||||
return createPdfBlob({
|
||||
data: withAppliedStylesheet(data, presentation),
|
||||
data,
|
||||
template,
|
||||
...(renderOptions ? { renderOptions } : {}),
|
||||
...(presentation && "publicStyleProjection" in presentation
|
||||
? { publicStyleProjection: presentation.publicStyleProjection }
|
||||
: {}),
|
||||
resolveSectionTitle: sectionTitleResolver,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
import { useResumeExport } from "./use-resume-export";
|
||||
|
||||
@@ -14,22 +13,13 @@ const mocks = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-document", () => ({
|
||||
vi.mock("@/features/resume/export/pdf-document", () => ({
|
||||
createResumePdfBlob: mocks.createResumePdfBlob,
|
||||
}));
|
||||
vi.mock("@reactive-resume/utils/file", () => ({
|
||||
downloadWithAnchor: mocks.downloadWithAnchor,
|
||||
generateFilename: (name: string, extension: string) => `${name}.${extension}`,
|
||||
}));
|
||||
vi.mock("@/features/resume/stylesheet/store", () => ({
|
||||
useStylesheetStore: (selector: (state: object) => unknown) =>
|
||||
selector({
|
||||
resumeId: undefined,
|
||||
mode: "legacy",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\n" },
|
||||
}),
|
||||
}));
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
loading: vi.fn(() => "toast"),
|
||||
@@ -49,21 +39,16 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("useResumeExport public PDF", () => {
|
||||
it("downloads the authorized server blob after one mismatched-projection refetch", async () => {
|
||||
it("downloads the authorized server blob after public browser rendering rejects", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const source = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source, applied: source };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const mismatchedProjection = { ...projection, renderDataHash: "0".repeat(64) };
|
||||
const refetchStyleProjection = vi.fn(async () => mismatchedProjection);
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source };
|
||||
mocks.createResumePdfBlob.mockRejectedValueOnce(new Error("browser renderer failed"));
|
||||
const { result } = renderHook(() =>
|
||||
useResumeExport(
|
||||
{ name: "Sample", slug: "sample", data: sampleResumeData },
|
||||
{ name: "Sample", slug: "sample", data: semanticData },
|
||||
{
|
||||
publicResumePdf: {
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: mismatchedProjection,
|
||||
refetchStyleProjection,
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
},
|
||||
},
|
||||
@@ -72,19 +57,14 @@ describe("useResumeExport public PDF", () => {
|
||||
|
||||
await act(() => result.current.onDownloadPDF());
|
||||
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.createResumePdfBlob).toHaveBeenCalledWith(semanticData);
|
||||
expect(mocks.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
const blob = mocks.downloadWithAnchor.mock.calls[0]?.[0] as Blob;
|
||||
expect(await blob.text()).toBe("server");
|
||||
});
|
||||
|
||||
it("does not download an unstyled PDF when semantic rendering rejects", async () => {
|
||||
mocks.createResumePdfBlob.mockRejectedValueOnce(
|
||||
new Error("The semantic stylesheet could not be rendered.", {
|
||||
cause: [{ code: "RESOURCE_LIMIT", severity: "error" }],
|
||||
}),
|
||||
);
|
||||
it("does not download a PDF when the renderer rejects", async () => {
|
||||
mocks.createResumePdfBlob.mockRejectedValueOnce(new Error("PDF renderer failed"));
|
||||
const { result } = renderHook(() => useResumeExport({ name: "Sample", slug: "sample", data: sampleResumeData }));
|
||||
|
||||
await act(() => result.current.onDownloadPDF());
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { ResumeExportTarget } from "@reactive-resume/resume/export-sections";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { PublicResumePdfOptions } from "@/features/resume/public/public-pdf";
|
||||
import type { ResumePdfPresentation } from "./pdf-document";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { buildDocx } from "@reactive-resume/docx";
|
||||
import { getResumeSectionTitle } from "@reactive-resume/pdf/section-title";
|
||||
@@ -12,7 +10,6 @@ import { getResumeExportData, resumeHasCoverLetter } from "@reactive-resume/resu
|
||||
import { buildMarkdown } from "@reactive-resume/resume/markdown";
|
||||
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
|
||||
import { resolvePublicResumePdfBlob } from "@/features/resume/public/public-pdf";
|
||||
import { useStylesheetStore } from "@/features/resume/stylesheet/store";
|
||||
import { createSectionTitleResolverForLocale } from "@/libs/resume/section-title-locale";
|
||||
import { createResumePdfBlob } from "./pdf-document";
|
||||
|
||||
@@ -54,43 +51,12 @@ type DownloadPdfOptions = {
|
||||
export function useResumeExport(resume: ExportableResume | undefined, exportOptions: UseResumeExportOptions = {}) {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const hasCoverLetter = resume ? resumeHasCoverLetter(resume.data) : false;
|
||||
const stylesheetResumeId = useStylesheetStore((state) => state.resumeId);
|
||||
const stylesheetMode = useStylesheetStore((state) => state.mode);
|
||||
const stylesheetSource = useStylesheetStore((state) => state.source);
|
||||
const stylesheetApplied = useStylesheetStore((state) => state.applied);
|
||||
const canonicalStylesheet = useMemo<SemanticStylesheet | undefined>(
|
||||
() =>
|
||||
resume?.id && resume.id === stylesheetResumeId
|
||||
? {
|
||||
mode: stylesheetMode,
|
||||
source: stylesheetSource,
|
||||
applied: stylesheetApplied,
|
||||
}
|
||||
: undefined,
|
||||
[resume?.id, stylesheetApplied, stylesheetMode, stylesheetResumeId, stylesheetSource],
|
||||
);
|
||||
const pdfPresentation = useMemo<ResumePdfPresentation | undefined>(
|
||||
() =>
|
||||
canonicalStylesheet
|
||||
? { stylesheet: { mode: canonicalStylesheet.mode, applied: canonicalStylesheet.applied } }
|
||||
: undefined,
|
||||
[canonicalStylesheet],
|
||||
);
|
||||
|
||||
const onDownloadJSON = useCallback(() => {
|
||||
if (!resume) return;
|
||||
const data = canonicalStylesheet
|
||||
? {
|
||||
...resume.data,
|
||||
metadata: {
|
||||
...resume.data.metadata,
|
||||
stylesheet: canonicalStylesheet,
|
||||
},
|
||||
}
|
||||
: resume.data;
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const blob = new Blob([JSON.stringify(resume.data, null, 2)], { type: "application/json" });
|
||||
downloadWithAnchor(blob, generateFilename(getExportName(resume), "json"));
|
||||
}, [canonicalStylesheet, resume]);
|
||||
}, [resume]);
|
||||
|
||||
const onDownloadMarkdown = useCallback(
|
||||
async (target: ResumeExportTarget = "resume") => {
|
||||
@@ -136,7 +102,6 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
||||
target === "cover-letter"
|
||||
? { includeCoverLetterHeader: downloadOptions?.includeCoverLetterHeader }
|
||||
: undefined,
|
||||
pdfPresentation,
|
||||
);
|
||||
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "pdf"));
|
||||
} catch {
|
||||
@@ -146,7 +111,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
||||
toast.dismiss(toastId);
|
||||
}
|
||||
},
|
||||
[exportOptions.publicResumePdf, pdfPresentation, resume],
|
||||
[exportOptions.publicResumePdf, resume],
|
||||
);
|
||||
|
||||
const onPrint = useCallback(async () => {
|
||||
@@ -156,7 +121,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
||||
try {
|
||||
const blob = exportOptions.publicResumePdf
|
||||
? await resolvePublicResumePdfBlob({ data: resume.data, ...exportOptions.publicResumePdf })
|
||||
: await createResumePdfBlob(resume.data, undefined, undefined, pdfPresentation);
|
||||
: await createResumePdfBlob(resume.data);
|
||||
const url = URL.createObjectURL(blob);
|
||||
// ponytail: print the generated PDF via a hidden iframe (reliable in Chromium). If the browser
|
||||
// blocks iframe printing, fall back to opening the PDF in a new tab so the user can print manually.
|
||||
@@ -182,7 +147,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
||||
setIsExporting(false);
|
||||
toast.dismiss(toastId);
|
||||
}
|
||||
}, [exportOptions.publicResumePdf, pdfPresentation, resume]);
|
||||
}, [exportOptions.publicResumePdf, resume]);
|
||||
|
||||
return { onDownloadJSON, onDownloadMarkdown, onDownloadDOCX, onDownloadPDF, onPrint, isExporting, hasCoverLetter };
|
||||
}
|
||||
|
||||
@@ -8,14 +8,7 @@ import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
import { ResumePreviewClient } from "./preview.browser";
|
||||
|
||||
const previewMock = vi.hoisted(() => ({
|
||||
builderResumeId: undefined as string | undefined,
|
||||
builderResumeData: undefined as ResumeData | undefined,
|
||||
stylesheet: {
|
||||
resumeId: undefined as string | undefined,
|
||||
mode: "legacy" as "legacy" | "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\n" },
|
||||
},
|
||||
toastError: vi.fn(),
|
||||
toBlob: vi.fn(async () => new Blob(["%PDF"], { type: "application/pdf" })),
|
||||
}));
|
||||
@@ -53,22 +46,9 @@ vi.mock("sonner", () => ({
|
||||
|
||||
vi.mock("../builder/draft", () => ({
|
||||
useResumeData: () => previewMock.builderResumeData,
|
||||
useResumeStore: (selector: (state: { resumeId?: string }) => unknown) =>
|
||||
selector({ resumeId: previewMock.builderResumeId }),
|
||||
usePreviewPausedStore: (selector: (state: { paused: boolean }) => unknown) => selector({ paused: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/resume/stylesheet/store", () => ({
|
||||
useStylesheetStore: (
|
||||
selector: (state: {
|
||||
resumeId?: string;
|
||||
mode: "legacy" | "semantic";
|
||||
source: { languageVersion: number; text: string };
|
||||
applied: { languageVersion: number; text: string };
|
||||
}) => unknown,
|
||||
) => selector(previewMock.stylesheet),
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-canvas", async () => {
|
||||
const React = await import("react");
|
||||
const pdfDocument = { numPages: 1 };
|
||||
@@ -102,14 +82,7 @@ describe("ResumePreviewClient", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
previewMock.builderResumeId = undefined;
|
||||
previewMock.builderResumeData = undefined;
|
||||
previewMock.stylesheet = {
|
||||
resumeId: undefined,
|
||||
mode: "legacy",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\n" },
|
||||
};
|
||||
previewMock.toBlob.mockReset();
|
||||
previewMock.toBlob.mockImplementation(async () => new Blob(["%PDF"], { type: "application/pdf" }));
|
||||
previewMock.toastError.mockReset();
|
||||
@@ -135,7 +108,7 @@ describe("ResumePreviewClient", () => {
|
||||
expect(previewMock.toBlob).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(previewMock.toBlob).toHaveBeenCalledWith(sampleResumeData, undefined, undefined, undefined);
|
||||
expect(previewMock.toBlob).toHaveBeenCalledWith(sampleResumeData);
|
||||
});
|
||||
|
||||
it("keeps the rendered template identity on the active layer while its replacement renders", async () => {
|
||||
@@ -159,47 +132,39 @@ describe("ResumePreviewClient", () => {
|
||||
expect(activeLayer?.getAttribute("data-resume-preview-template")).toBe("azurill");
|
||||
});
|
||||
|
||||
it("renders the canonical applied stylesheet and ignores invalid editable source", async () => {
|
||||
const validApplied = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
previewMock.builderResumeId = "resume-1";
|
||||
previewMock.builderResumeData = sampleResumeData;
|
||||
previewMock.stylesheet = {
|
||||
resumeId: "resume-1",
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "section {" },
|
||||
applied: validApplied,
|
||||
it("renders the current stylesheet source from the ordinary builder draft", async () => {
|
||||
const source = { languageVersion: 1, text: "section {" };
|
||||
previewMock.builderResumeData = {
|
||||
...sampleResumeData,
|
||||
metadata: {
|
||||
...sampleResumeData.metadata,
|
||||
stylesheet: { mode: "semantic", source },
|
||||
},
|
||||
};
|
||||
|
||||
render(<ResumePreviewClient pageLayout="vertical" pageScale={1.25} showPageNumbers={false} />);
|
||||
|
||||
await waitFor(() => expect(previewMock.toBlob).toHaveBeenCalledTimes(1));
|
||||
expect(previewMock.toBlob).toHaveBeenCalledWith(sampleResumeData, undefined, undefined, {
|
||||
stylesheet: { mode: "semantic", applied: validApplied },
|
||||
});
|
||||
expect(previewMock.toBlob).toHaveBeenCalledWith(previewMock.builderResumeData);
|
||||
|
||||
expect(previewMock.toBlob).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the active PDF visible and reports later semantic render diagnostics", async () => {
|
||||
previewMock.builderResumeId = "resume-1";
|
||||
it("keeps the active PDF visible and reports a later renderer failure", async () => {
|
||||
previewMock.builderResumeData = sampleResumeData;
|
||||
previewMock.stylesheet = {
|
||||
resumeId: "resume-1",
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
};
|
||||
const view = render(<ResumePreviewClient pageLayout="vertical" pageScale={1.25} showPageNumbers={false} />);
|
||||
expect(await screen.findByRole("img", { name: "Resume page 1 of 1" })).toBeTruthy();
|
||||
|
||||
previewMock.toBlob.mockRejectedValueOnce(
|
||||
new Error("The semantic stylesheet could not be rendered.", {
|
||||
cause: [{ code: "RESOURCE_LIMIT", severity: "error" }],
|
||||
}),
|
||||
);
|
||||
previewMock.stylesheet.applied = {
|
||||
languageVersion: 1,
|
||||
text: "@version 1;\nname { color: #654321; }\n",
|
||||
previewMock.toBlob.mockRejectedValueOnce(new Error("PDF renderer failed"));
|
||||
previewMock.builderResumeData = {
|
||||
...sampleResumeData,
|
||||
metadata: {
|
||||
...sampleResumeData.metadata,
|
||||
stylesheet: {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname { color: #654321; }\n" },
|
||||
},
|
||||
},
|
||||
};
|
||||
view.rerender(<ResumePreviewClient pageLayout="vertical" pageScale={1.25} showPageNumbers={false} />);
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ import type { ResolvedResumePreviewProps } from "./preview.shared";
|
||||
import type { PreviewPageSize } from "./preview.shared.utils";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { AnimatePresence, m } from "motion/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { isRTL } from "@reactive-resume/utils/locale";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||
import { useStylesheetStore } from "@/features/resume/stylesheet/store";
|
||||
import { usePreviewPausedStore, useResumeData, useResumeStore } from "../builder/draft";
|
||||
import { usePreviewPausedStore, useResumeData } from "../builder/draft";
|
||||
import { PdfCanvasDocument, PdfCanvasPage } from "./pdf-canvas";
|
||||
import { ResumePreviewLoader } from "./preview.shared";
|
||||
import { getResumePreviewGapValue, getResumePreviewPageCount } from "./preview.shared.utils";
|
||||
@@ -102,17 +101,6 @@ export function ResumePreviewClient({
|
||||
}: ResolvedResumePreviewProps) {
|
||||
const builderResumeData = useResumeData();
|
||||
const resumeData = data ?? builderResumeData;
|
||||
const builderResumeId = useResumeStore((state) => state.resumeId);
|
||||
const stylesheetResumeId = useStylesheetStore((state) => state.resumeId);
|
||||
const mode = useStylesheetStore((state) => state.mode);
|
||||
const applied = useStylesheetStore((state) => state.applied);
|
||||
const presentation = useMemo(
|
||||
() =>
|
||||
data === undefined && builderResumeId !== undefined && stylesheetResumeId === builderResumeId
|
||||
? { stylesheet: { mode, applied } }
|
||||
: undefined,
|
||||
[applied, builderResumeId, data, mode, stylesheetResumeId],
|
||||
);
|
||||
const paused = usePreviewPausedStore((state) => state.paused);
|
||||
|
||||
const [previewLayers, setPreviewLayers] = useState<PreviewPdf[]>([]);
|
||||
@@ -133,7 +121,7 @@ export function ResumePreviewClient({
|
||||
const generatePdfPreview = async () => {
|
||||
try {
|
||||
if (cancelled || requestId !== requestIdRef.current) return;
|
||||
const blob = await createResumePdfBlob(resumeData, undefined, undefined, presentation);
|
||||
const blob = await createResumePdfBlob(resumeData);
|
||||
|
||||
if (!cancelled && requestId === requestIdRef.current) {
|
||||
const nextPdf = createPreviewPdf(
|
||||
@@ -162,7 +150,7 @@ export function ResumePreviewClient({
|
||||
cancelled = true;
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [paused, presentation, resumeData]);
|
||||
}, [paused, resumeData]);
|
||||
|
||||
if (!resumeData) return null;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
|
||||
const pdfViewerMock = vi.hoisted(() => {
|
||||
@@ -120,67 +119,25 @@ describe("PdfViewer", () => {
|
||||
expect(pdfViewerMock.loadingTask.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders a valid public projection through the shared PDF entrypoint", async () => {
|
||||
it("renders exposed semantic source through the shared PDF entrypoint", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const applied = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source: applied, applied };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const refetchStyleProjection = vi.fn();
|
||||
semanticData.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
};
|
||||
|
||||
render(
|
||||
<PdfViewer
|
||||
data={sampleResumeData}
|
||||
stylesheetMode="semantic"
|
||||
styleProjection={projection}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
publicResume={{ username: "amruth", slug: "sample" }}
|
||||
/>,
|
||||
);
|
||||
render(<PdfViewer data={semanticData} publicResume={{ username: "amruth", slug: "sample" }} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(pdfViewerMock.createResumePdfBlob).toHaveBeenCalledWith(sampleResumeData, undefined, undefined, {
|
||||
publicStyleProjection: projection,
|
||||
}),
|
||||
);
|
||||
expect(refetchStyleProjection).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(pdfViewerMock.createResumePdfBlob).toHaveBeenCalledWith(semanticData));
|
||||
expect(pdfViewerMock.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refetches a mismatched projection once before using the authorized PDF fallback", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const applied = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source: applied, applied };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const mismatchedProjection = { ...projection, renderDataHash: "0".repeat(64) };
|
||||
const refetchStyleProjection = vi.fn(async () => mismatchedProjection);
|
||||
it("uses the authorized PDF fallback after browser generation rejects", async () => {
|
||||
pdfViewerMock.createResumePdfBlob.mockRejectedValueOnce(new Error("browser renderer failed"));
|
||||
|
||||
const view = render(
|
||||
<PdfViewer
|
||||
data={sampleResumeData}
|
||||
stylesheetMode="semantic"
|
||||
styleProjection={mismatchedProjection}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
publicResume={{ username: "amruth", slug: "sample" }}
|
||||
/>,
|
||||
);
|
||||
render(<PdfViewer data={sampleResumeData} publicResume={{ username: "amruth", slug: "sample" }} />);
|
||||
|
||||
await waitFor(() => expect(refetchStyleProjection).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(pdfViewerMock.fetch).toHaveBeenCalledTimes(1));
|
||||
expect(String(pdfViewerMock.fetch.mock.calls[0]?.[0])).toContain("/api/resumes/amruth/sample/pdf");
|
||||
expect(String(pdfViewerMock.fetch.mock.calls[0]?.[0])).toContain("reason=render-data-hash");
|
||||
expect(pdfViewerMock.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
|
||||
view.rerender(
|
||||
<PdfViewer
|
||||
data={sampleResumeData}
|
||||
stylesheetMode="semantic"
|
||||
styleProjection={{ ...mismatchedProjection, renderDataHash: "1".repeat(64) }}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
publicResume={{ username: "amruth", slug: "sample" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(pdfViewerMock.fetch).toHaveBeenCalledTimes(2));
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(pdfViewerMock.fetch).toHaveBeenCalledWith("/api/resumes/amruth/sample/pdf", { credentials: "include" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { PDFDocumentLoadingTask, PDFDocumentProxy } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
import { AnnotationMode, GlobalWorkerOptions, getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
import { EventBus, LinkTarget, PDFLinkService, PDFViewer } from "pdfjs-dist/legacy/web/pdf_viewer.mjs";
|
||||
@@ -17,9 +15,6 @@ GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/legacy/build/pdf.worker.min.
|
||||
type PdfViewerProps = {
|
||||
className?: string;
|
||||
data: ResumeData;
|
||||
stylesheetMode?: SemanticStylesheet["mode"];
|
||||
styleProjection?: PublicStyleProjection;
|
||||
refetchStyleProjection?: () => Promise<PublicStyleProjection | undefined>;
|
||||
publicResume?: {
|
||||
username: string;
|
||||
slug: string;
|
||||
@@ -77,21 +72,11 @@ function pdfViewerReducer(state: PdfViewerState, action: PdfViewerAction): PdfVi
|
||||
}
|
||||
}
|
||||
|
||||
export function PdfViewer({
|
||||
className,
|
||||
data,
|
||||
stylesheetMode,
|
||||
styleProjection,
|
||||
refetchStyleProjection,
|
||||
publicResume,
|
||||
}: PdfViewerProps) {
|
||||
export function PdfViewer({ className, data, publicResume }: PdfViewerProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewerRef = useRef<HTMLDivElement>(null);
|
||||
const fileRef = useRef<Blob | null>(null);
|
||||
const projectionRetryRef = useRef<{ data?: ResumeData; publicKey?: string; retried: boolean }>({
|
||||
retried: false,
|
||||
});
|
||||
const [{ error, fileVersion, isReady, viewerHeight }, dispatch] = useReducer(
|
||||
pdfViewerReducer,
|
||||
INITIAL_PDF_VIEWER_STATE,
|
||||
@@ -103,27 +88,8 @@ export function PdfViewer({
|
||||
fileRef.current = null;
|
||||
dispatch({ type: "resetForData" });
|
||||
|
||||
const createPdf = () => {
|
||||
if (!stylesheetMode || !publicResume) return createResumePdfBlob(data);
|
||||
const publicKey = `${publicResume.username}/${publicResume.slug}`;
|
||||
if (projectionRetryRef.current.data !== data || projectionRetryRef.current.publicKey !== publicKey) {
|
||||
projectionRetryRef.current = { data, publicKey, retried: false };
|
||||
}
|
||||
const retryProjection =
|
||||
refetchStyleProjection && !projectionRetryRef.current.retried
|
||||
? () => {
|
||||
projectionRetryRef.current.retried = true;
|
||||
return refetchStyleProjection();
|
||||
}
|
||||
: undefined;
|
||||
return resolvePublicResumePdfBlob({
|
||||
data,
|
||||
stylesheetMode,
|
||||
publicResume,
|
||||
...(styleProjection ? { styleProjection } : {}),
|
||||
...(retryProjection ? { refetchStyleProjection: retryProjection } : {}),
|
||||
});
|
||||
};
|
||||
const createPdf = () =>
|
||||
publicResume ? resolvePublicResumePdfBlob({ data, publicResume }) : createResumePdfBlob(data);
|
||||
|
||||
void createPdf()
|
||||
.then((blob) => {
|
||||
@@ -142,7 +108,7 @@ export function PdfViewer({
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [data, publicResume, refetchStyleProjection, styleProjection, stylesheetMode]);
|
||||
}, [data, publicResume]);
|
||||
|
||||
useEffect(() => {
|
||||
void fileVersion;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
import { resolvePublicResumePdfBlob } from "./public-pdf";
|
||||
|
||||
@@ -15,58 +14,35 @@ vi.mock("@/features/resume/export/pdf-document", () => ({
|
||||
const publicResume = { username: "amruth", slug: "sample" };
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.createResumePdfBlob.mockClear();
|
||||
mocks.fetch.mockClear();
|
||||
mocks.createResumePdfBlob.mockReset();
|
||||
mocks.createResumePdfBlob.mockResolvedValue(new Blob(["local"], { type: "application/pdf" }));
|
||||
mocks.fetch.mockReset();
|
||||
mocks.fetch.mockResolvedValue(new Response(new Blob(["server"], { type: "application/pdf" })));
|
||||
vi.stubGlobal("fetch", mocks.fetch);
|
||||
});
|
||||
|
||||
describe("resolvePublicResumePdfBlob", () => {
|
||||
it("keeps legitimate legacy resumes on the local PDF path", async () => {
|
||||
await resolvePublicResumePdfBlob({
|
||||
data: sampleResumeData,
|
||||
stylesheetMode: "legacy",
|
||||
publicResume,
|
||||
});
|
||||
it("renders the exposed stylesheet source directly in the browser", async () => {
|
||||
const data = structuredClone(sampleResumeData);
|
||||
data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
};
|
||||
|
||||
const blob = await resolvePublicResumePdfBlob({ data, publicResume });
|
||||
|
||||
expect(mocks.createResumePdfBlob).toHaveBeenCalledWith(data);
|
||||
expect(mocks.fetch).not.toHaveBeenCalled();
|
||||
expect(await blob.text()).toBe("local");
|
||||
});
|
||||
|
||||
it("fetches the server PDF only after browser rendering rejects", async () => {
|
||||
mocks.createResumePdfBlob.mockRejectedValue(new Error("browser renderer failed"));
|
||||
|
||||
const blob = await resolvePublicResumePdfBlob({ data: sampleResumeData, publicResume });
|
||||
|
||||
expect(mocks.createResumePdfBlob).toHaveBeenCalledWith(sampleResumeData);
|
||||
expect(mocks.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the authorized server fallback when a semantic projection is unavailable", async () => {
|
||||
const refetchStyleProjection = vi.fn().mockRejectedValue(new Error("projection unavailable"));
|
||||
|
||||
await resolvePublicResumePdfBlob({
|
||||
data: sampleResumeData,
|
||||
stylesheetMode: "semantic",
|
||||
publicResume,
|
||||
refetchStyleProjection,
|
||||
});
|
||||
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(String(mocks.fetch.mock.calls[0]?.[0])).toContain("/api/resumes/amruth/sample/pdf");
|
||||
expect(String(mocks.fetch.mock.calls[0]?.[0])).toContain("reason=missing-projection");
|
||||
expect(mocks.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refetches a mismatched projection once before returning the authorized server blob", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const source = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source, applied: source };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const mismatchedProjection = { ...projection, renderDataHash: "0".repeat(64) };
|
||||
const refetchStyleProjection = vi.fn(async () => mismatchedProjection);
|
||||
|
||||
const blob = await resolvePublicResumePdfBlob({
|
||||
data: sampleResumeData,
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: mismatchedProjection,
|
||||
publicResume,
|
||||
refetchStyleProjection,
|
||||
});
|
||||
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetch).toHaveBeenCalledWith("/api/resumes/amruth/sample/pdf", { credentials: "include" });
|
||||
expect(await blob.text()).toBe("server");
|
||||
expect(mocks.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,97 +1,28 @@
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import {
|
||||
getPublicStyleProjectionFingerprints,
|
||||
PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
|
||||
SEMANTIC_TREE_VERSION,
|
||||
validatePublicStyleProjection,
|
||||
} from "@reactive-resume/pdf/public-projection";
|
||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||
|
||||
export type PublicResumePdfOptions = {
|
||||
stylesheetMode: SemanticStylesheet["mode"];
|
||||
styleProjection?: PublicStyleProjection;
|
||||
refetchStyleProjection?: () => Promise<PublicStyleProjection | undefined>;
|
||||
publicResume: {
|
||||
username: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ProjectionMismatchReason =
|
||||
| "format-version"
|
||||
| "language-version"
|
||||
| "semantic-tree-version"
|
||||
| "registry-fingerprint"
|
||||
| "adapter-fingerprint"
|
||||
| "render-data-hash"
|
||||
| "invalid-projection";
|
||||
|
||||
const projectionMismatchReason = async (
|
||||
data: ResumeData,
|
||||
projection: PublicStyleProjection,
|
||||
): Promise<ProjectionMismatchReason | null> => {
|
||||
if (projection.formatVersion !== PUBLIC_STYLE_PROJECTION_FORMAT_VERSION) return "format-version";
|
||||
if (projection.languageVersion !== 1) return "language-version";
|
||||
if (projection.semanticTreeVersion !== SEMANTIC_TREE_VERSION) return "semantic-tree-version";
|
||||
const fingerprints = await getPublicStyleProjectionFingerprints();
|
||||
if (projection.registryFingerprint !== fingerprints.registryFingerprint) return "registry-fingerprint";
|
||||
if (projection.adapterFingerprint !== fingerprints.adapterFingerprint) return "adapter-fingerprint";
|
||||
return (await validatePublicStyleProjection(data, projection)) ? null : "render-data-hash";
|
||||
};
|
||||
|
||||
const fetchPublicResumePdf = async (
|
||||
publicResume: PublicResumePdfOptions["publicResume"],
|
||||
reason: ProjectionMismatchReason | "missing-projection",
|
||||
projection?: PublicStyleProjection,
|
||||
) => {
|
||||
const search = new URLSearchParams({
|
||||
reason,
|
||||
...(projection
|
||||
? {
|
||||
registryFingerprint: projection.registryFingerprint,
|
||||
adapterFingerprint: projection.adapterFingerprint,
|
||||
}
|
||||
: {}),
|
||||
const fetchPublicResumePdf = async ({ username, slug }: PublicResumePdfOptions["publicResume"]) => {
|
||||
const response = await fetch(`/api/resumes/${encodeURIComponent(username)}/${encodeURIComponent(slug)}/pdf`, {
|
||||
credentials: "include",
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/resumes/${encodeURIComponent(publicResume.username)}/${encodeURIComponent(publicResume.slug)}/pdf?${search}`,
|
||||
{ credentials: "include" },
|
||||
);
|
||||
if (!response.ok) throw new Error(`Public PDF fallback failed with ${response.status}`);
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
export async function resolvePublicResumePdfBlob({
|
||||
data,
|
||||
...options
|
||||
publicResume,
|
||||
}: PublicResumePdfOptions & { data: ResumeData }): Promise<Blob> {
|
||||
if (options.stylesheetMode === "legacy") return createResumePdfBlob(data);
|
||||
|
||||
let projection = options.styleProjection;
|
||||
let refetched = false;
|
||||
const refetch = async () => {
|
||||
if (!options.refetchStyleProjection || refetched) return;
|
||||
refetched = true;
|
||||
try {
|
||||
projection = await options.refetchStyleProjection();
|
||||
} catch {
|
||||
projection = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (!projection) await refetch();
|
||||
if (!projection) return fetchPublicResumePdf(options.publicResume, "missing-projection");
|
||||
|
||||
let reason = await projectionMismatchReason(data, projection).catch(() => "invalid-projection" as const);
|
||||
if (reason) {
|
||||
await refetch();
|
||||
if (!projection) return fetchPublicResumePdf(options.publicResume, "missing-projection");
|
||||
reason = await projectionMismatchReason(data, projection).catch(() => "invalid-projection" as const);
|
||||
try {
|
||||
return await createResumePdfBlob(data);
|
||||
} catch {
|
||||
return fetchPublicResumePdf(publicResume);
|
||||
}
|
||||
|
||||
return reason
|
||||
? fetchPublicResumePdf(options.publicResume, reason, projection)
|
||||
: createResumePdfBlob(data, undefined, undefined, { publicStyleProjection: projection });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { ReactNode } from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
@@ -12,30 +11,12 @@ import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
type PdfViewerProps = {
|
||||
className?: string;
|
||||
data: ResumeData;
|
||||
stylesheetMode?: "legacy" | "semantic";
|
||||
styleProjection?: PublicStyleProjection;
|
||||
refetchStyleProjection?: () => Promise<PublicStyleProjection | undefined>;
|
||||
publicResume?: { username: string; slug: string };
|
||||
};
|
||||
|
||||
const publicResumeMock = vi.hoisted(() => ({
|
||||
onDownloadPDF: vi.fn(),
|
||||
PdfViewer: vi.fn<(_props: PdfViewerProps) => ReactNode>(() => null),
|
||||
projection: {
|
||||
formatVersion: 1,
|
||||
languageVersion: 1,
|
||||
semanticTreeVersion: 1,
|
||||
registryFingerprint: "1".repeat(64),
|
||||
adapterFingerprint: "2".repeat(64),
|
||||
renderDataHash: "3".repeat(64),
|
||||
nodes: { resume: {} },
|
||||
} as PublicStyleProjection,
|
||||
refetchProjection: vi.fn(),
|
||||
projectionResult: {
|
||||
data: undefined as PublicStyleProjection | undefined,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
},
|
||||
useResumeExport: vi.fn(),
|
||||
resume: undefined as
|
||||
| undefined
|
||||
@@ -43,61 +24,28 @@ const publicResumeMock = vi.hoisted(() => ({
|
||||
data: ResumeData;
|
||||
name: string;
|
||||
slug: string;
|
||||
stylesheetMode: "legacy" | "semantic";
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: (options: { query: "resume" | "projection" }) =>
|
||||
options.query === "resume"
|
||||
? { data: publicResumeMock.resume }
|
||||
: { ...publicResumeMock.projectionResult, refetch: publicResumeMock.refetchProjection },
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({ useQuery: () => ({ data: publicResumeMock.resume }) }));
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
getRouteApi: () => ({
|
||||
useParams: () => ({ username: "amruth", slug: "sample" }),
|
||||
}),
|
||||
getRouteApi: () => ({ useParams: () => ({ username: "amruth", slug: "sample" }) }),
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-viewer", () => ({
|
||||
PdfViewer: publicResumeMock.PdfViewer,
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-viewer", () => ({ PdfViewer: publicResumeMock.PdfViewer }));
|
||||
vi.mock("@/libs/orpc/client", () => ({
|
||||
orpc: {
|
||||
resume: {
|
||||
getBySlug: { queryOptions: () => ({ query: "resume" }) },
|
||||
getStyleProjection: { queryOptions: () => ({ query: "projection" }) },
|
||||
},
|
||||
},
|
||||
orpc: { resume: { getBySlug: { queryOptions: () => ({ query: "resume" }) } } },
|
||||
}));
|
||||
|
||||
vi.mock("@/features/resume/export/use-resume-export", () => ({
|
||||
useResumeExport: publicResumeMock.useResumeExport,
|
||||
}));
|
||||
|
||||
const { PublicResumeRoute } = await import("./public-resume");
|
||||
|
||||
beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
});
|
||||
beforeAll(() => i18n.loadAndActivate({ locale: "en", messages: {} }));
|
||||
|
||||
beforeEach(() => {
|
||||
publicResumeMock.resume = {
|
||||
data: sampleResumeData,
|
||||
name: "Sample Resume",
|
||||
slug: "sample",
|
||||
stylesheetMode: "semantic",
|
||||
};
|
||||
publicResumeMock.projectionResult = {
|
||||
data: publicResumeMock.projection,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
};
|
||||
publicResumeMock.resume = { data: sampleResumeData, name: "Sample Resume", slug: "sample" };
|
||||
publicResumeMock.PdfViewer.mockClear();
|
||||
publicResumeMock.refetchProjection.mockReset();
|
||||
publicResumeMock.refetchProjection.mockResolvedValue({ data: publicResumeMock.projection });
|
||||
publicResumeMock.useResumeExport.mockReset();
|
||||
publicResumeMock.useResumeExport.mockReturnValue({
|
||||
onDownloadPDF: publicResumeMock.onDownloadPDF,
|
||||
@@ -116,78 +64,26 @@ const renderPublicResumeRoute = () =>
|
||||
);
|
||||
|
||||
describe("PublicResumeRoute", () => {
|
||||
it("renders the public resume through the route-local PDF.js viewer", () => {
|
||||
it("passes exposed source data directly to the browser viewer and export fallback", () => {
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(screen.getByTestId("pdf-viewer")).toHaveClass("block", "w-full");
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: sampleResumeData }),
|
||||
expect.objectContaining({
|
||||
data: sampleResumeData,
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(publicResumeMock.useResumeExport).toHaveBeenCalledWith(publicResumeMock.resume, {
|
||||
publicResumePdf: expect.objectContaining({
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: publicResumeMock.projection,
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
publicResumePdf: { publicResume: { username: "amruth", slug: "sample" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("routes a missing semantic projection to the shared fallback seam", () => {
|
||||
publicResumeMock.projectionResult = { data: undefined, isError: true, isPending: false };
|
||||
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: undefined,
|
||||
refetchStyleProjection: expect.any(Function),
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(publicResumeMock.useResumeExport).toHaveBeenCalledWith(
|
||||
publicResumeMock.resume,
|
||||
expect.objectContaining({
|
||||
publicResumePdf: expect.objectContaining({
|
||||
stylesheetMode: "semantic",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps missing legacy projections on the local rendering path", () => {
|
||||
if (publicResumeMock.resume) publicResumeMock.resume.stylesheetMode = "legacy";
|
||||
publicResumeMock.projectionResult = { data: undefined, isError: false, isPending: false };
|
||||
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ stylesheetMode: "legacy", styleProjection: undefined }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("loads the public projection and passes it to the shared viewer", () => {
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
styleProjection: publicResumeMock.projection,
|
||||
refetchStyleProjection: expect.any(Function),
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("lets the public resume page grow to the full PDF length", () => {
|
||||
renderPublicResumeRoute();
|
||||
|
||||
const viewerFrame = screen.getByTestId("pdf-viewer").parentElement;
|
||||
const page = viewerFrame?.parentElement;
|
||||
|
||||
expect(page).not.toHaveClass("min-h-svh", "h-svh", "max-h-svh", "overflow-hidden");
|
||||
expect(viewerFrame).not.toHaveClass("min-h-0", "flex-1", "overflow-hidden");
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Trans } from "@lingui/react/macro";
|
||||
import { CircleNotchIcon, DownloadSimpleIcon } from "@phosphor-icons/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getRouteApi } from "@tanstack/react-router";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
@@ -17,30 +17,12 @@ export function PublicResumeRoute() {
|
||||
const { username, slug } = publicResumeRoute.useParams();
|
||||
|
||||
const { data: resume } = useQuery(orpc.resume.getBySlug.queryOptions({ input: { username, slug } }));
|
||||
const projectionQuery = useQuery(
|
||||
orpc.resume.getStyleProjection.queryOptions({ input: { username, slug }, enabled: resume !== undefined }),
|
||||
);
|
||||
const styleProjection =
|
||||
projectionQuery.data && Object.keys(projectionQuery.data.nodes).length > 0 ? projectionQuery.data : undefined;
|
||||
const publicResume = useMemo(() => ({ username, slug }), [slug, username]);
|
||||
const refetchStyleProjection = useCallback(async () => {
|
||||
const result = await projectionQuery.refetch();
|
||||
return result.data;
|
||||
}, [projectionQuery.refetch]);
|
||||
const { onDownloadPDF, isExporting } = useResumeExport(resume, {
|
||||
...(resume
|
||||
? {
|
||||
publicResumePdf: {
|
||||
stylesheetMode: resume.stylesheetMode,
|
||||
publicResume,
|
||||
refetchStyleProjection,
|
||||
...(styleProjection ? { styleProjection } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(resume ? { publicResumePdf: { publicResume } } : {}),
|
||||
});
|
||||
|
||||
if (!resume || projectionQuery.isPending) return <LoadingScreen />;
|
||||
if (!resume) return <LoadingScreen />;
|
||||
|
||||
const { basics, picture } = resume.data;
|
||||
|
||||
@@ -66,14 +48,7 @@ export function PublicResumeRoute() {
|
||||
</header>
|
||||
|
||||
<main className="w-full max-w-5xl bg-white print:max-w-full">
|
||||
<PdfViewer
|
||||
data={resume.data}
|
||||
className="block w-full"
|
||||
stylesheetMode={resume.stylesheetMode}
|
||||
styleProjection={styleProjection}
|
||||
publicResume={publicResume}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
/>
|
||||
<PdfViewer data={resume.data} className="block w-full" publicResume={publicResume} />
|
||||
</main>
|
||||
|
||||
<footer className="flex justify-center print:hidden">
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { SemanticCssDiagnostic, StyleProgram } from "@reactive-resume/resume/stylesheet";
|
||||
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { TooltipProvider } from "@reactive-resume/ui/components/tooltip";
|
||||
import StylesheetEditorShell, { StylesheetCodeEditor } from "./editor";
|
||||
import { LegacyStylesheetBanner } from "./legacy-banner";
|
||||
import { StylesheetStatus } from "./status";
|
||||
import { useStylesheetStore } from "./store";
|
||||
|
||||
const media = vi.hoisted(() => ({ mobile: false }));
|
||||
const compileWorker = vi.hoisted(() => ({
|
||||
program: { languageVersion: 1, rules: [] } as StyleProgram | null,
|
||||
diagnostics: [] as SemanticCssDiagnostic[],
|
||||
}));
|
||||
const builder = vi.hoisted(() => ({
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
data: undefined as typeof defaultResumeData | undefined,
|
||||
isLocked: false,
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("usehooks-ts", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("usehooks-ts")>()),
|
||||
@@ -23,16 +35,52 @@ vi.mock("@/features/theme/provider", () => ({
|
||||
useTheme: () => ({ theme: "light" }),
|
||||
}));
|
||||
|
||||
const error: SemanticCssDiagnostic = {
|
||||
code: "SEMANTIC_CSS_UNKNOWN_PROPERTY",
|
||||
vi.mock("@/features/resume/builder/draft", () => ({
|
||||
useResumeData: () => builder.data,
|
||||
useIsResumeLocked: () => builder.isLocked,
|
||||
useUpdateResumeData: () => (update: (draft: typeof defaultResumeData) => void) => {
|
||||
if (builder.data) update(builder.data);
|
||||
},
|
||||
useResumeStore: (selector: (state: object) => unknown) =>
|
||||
selector({ canUndo: builder.canUndo, canRedo: builder.canRedo, undo: builder.undo, redo: builder.redo }),
|
||||
}));
|
||||
|
||||
vi.mock("./worker-client", () => ({
|
||||
createCompileWorkerClient: () => ({
|
||||
compile: vi.fn(async ({ editGeneration }: { editGeneration: number }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: compileWorker.program,
|
||||
diagnostics: compileWorker.diagnostics,
|
||||
colorTokens: [],
|
||||
})),
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const recoverableError: SemanticCssDiagnostic = {
|
||||
code: "INVALID_VALUE",
|
||||
severity: "error",
|
||||
message: "Unknown property",
|
||||
message: "Invalid value",
|
||||
range: {
|
||||
start: { line: 2, column: 3, offset: 17 },
|
||||
end: { line: 2, column: 9, offset: 23 },
|
||||
},
|
||||
};
|
||||
|
||||
const fatalError: SemanticCssDiagnostic = {
|
||||
...recoverableError,
|
||||
code: "VERSION_MISMATCH",
|
||||
message: "Version mismatch",
|
||||
};
|
||||
|
||||
const resolutionError: SemanticCssDiagnostic = {
|
||||
...recoverableError,
|
||||
code: "UNRESOLVED_VARIABLE",
|
||||
message: "Undefined variable --missing",
|
||||
};
|
||||
|
||||
const guideName = /read the applying custom styles guide.*opens in new tab/i;
|
||||
|
||||
const expectGuideLink = (root: HTMLElement) => {
|
||||
@@ -47,28 +95,48 @@ beforeAll(() => {
|
||||
Object.defineProperty(Element.prototype, "getAnimations", { configurable: true, value: () => [] });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
media.mobile = false;
|
||||
builder.data = structuredClone(defaultResumeData);
|
||||
builder.isLocked = false;
|
||||
builder.canUndo = false;
|
||||
builder.canRedo = false;
|
||||
builder.undo.mockReset();
|
||||
builder.redo.mockReset();
|
||||
compileWorker.program = { languageVersion: 1, rules: [] };
|
||||
compileWorker.diagnostics = [];
|
||||
});
|
||||
|
||||
const renderWithI18n = (element: React.ReactNode) => render(<I18nProvider i18n={i18n}>{element}</I18nProvider>);
|
||||
|
||||
describe("stylesheet editor status", () => {
|
||||
it("shows that invalid source keeps the last valid preview", () => {
|
||||
renderWithI18n(<StylesheetStatus mode="semantic" status="error" diagnostics={[error]} />);
|
||||
it("explains that fatal source falls back to base styles", () => {
|
||||
renderWithI18n(<StylesheetStatus mode="semantic" status="idle" diagnostics={[fatalError]} />);
|
||||
|
||||
expect(screen.getByText(/preview and export use the last valid version/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("Unknown property")).toBeInTheDocument();
|
||||
expect(screen.getByText(/preview and export fall back to base styles/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("Version mismatch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("explains that recoverable errors preserve valid styles", () => {
|
||||
renderWithI18n(<StylesheetStatus mode="semantic" status="idle" diagnostics={[recoverableError]} />);
|
||||
|
||||
expect(screen.getByText("Valid with errors")).toBeInTheDocument();
|
||||
expect(screen.getByText(/preview and export keep valid styles/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/fall back to base styles/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels a valid legacy draft as ready to activate", () => {
|
||||
renderWithI18n(<StylesheetStatus mode="legacy" status="idle" diagnostics={[]} />);
|
||||
|
||||
expect(screen.getByText("Ready to activate")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Applied")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels legacy warnings without claiming they are applied", () => {
|
||||
renderWithI18n(<StylesheetStatus mode="legacy" status="idle" diagnostics={[{ ...error, severity: "warning" }]} />);
|
||||
it("labels a legacy draft with warnings as ready to activate", () => {
|
||||
renderWithI18n(
|
||||
<StylesheetStatus mode="legacy" status="idle" diagnostics={[{ ...recoverableError, severity: "warning" }]} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Ready to activate with warnings")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Applied with warnings")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables activation while the converted draft has errors", () => {
|
||||
@@ -113,7 +181,7 @@ describe("StylesheetCodeEditor", () => {
|
||||
<StylesheetCodeEditor
|
||||
value={"@version 1;\nsection { color: red; }\n"}
|
||||
{...props}
|
||||
diagnostics={[error]}
|
||||
diagnostics={[recoverableError]}
|
||||
theme="dark"
|
||||
readOnly
|
||||
/>
|
||||
@@ -173,18 +241,105 @@ describe("StylesheetEditorShell", () => {
|
||||
expectGuideLink(container);
|
||||
});
|
||||
|
||||
it("makes the editor and mutation controls read-only while a restore is pending", () => {
|
||||
media.mobile = false;
|
||||
useStylesheetStore.setState({
|
||||
mode: "legacy",
|
||||
it("has no apply or save action for an already-semantic stylesheet", async () => {
|
||||
if (!builder.data) throw new Error("Missing resume fixture");
|
||||
builder.data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
};
|
||||
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<StylesheetEditorShell />
|
||||
</TooltipProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
await screen.findByText("Valid");
|
||||
expect(screen.queryByRole("button", { name: /activate semantic css/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /save|apply/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("writes semantic edits into the ordinary resume draft immediately", () => {
|
||||
if (!builder.data) throw new Error("Missing resume fixture");
|
||||
builder.data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
};
|
||||
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<StylesheetEditorShell />
|
||||
</TooltipProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
const textbox = screen.getByRole("textbox", { name: "Semantic CSS stylesheet" });
|
||||
const view = EditorView.findFromDOM(textbox);
|
||||
if (!view) throw new Error("Missing editor view");
|
||||
|
||||
act(() => view.dispatch({ changes: { from: view.state.doc.length, insert: "name { color: blue; }\n" } }));
|
||||
|
||||
expect(builder.data.metadata.stylesheet.source.text).toBe("@version 1;\nname { color: blue; }\n");
|
||||
});
|
||||
|
||||
it("switches a converted legacy draft through the ordinary resume update", async () => {
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<StylesheetEditorShell />
|
||||
</TooltipProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
const activate = await screen.findByRole("button", { name: "Activate Semantic CSS" });
|
||||
await waitFor(() => expect(activate).toBeEnabled());
|
||||
fireEvent.click(activate);
|
||||
|
||||
expect(builder.data?.metadata.stylesheet).toEqual({
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\n" },
|
||||
diagnostics: [],
|
||||
status: "idle",
|
||||
canUndo: true,
|
||||
canRedo: true,
|
||||
restoreLocked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("allows a converted legacy draft with recoverable errors to activate", async () => {
|
||||
compileWorker.diagnostics = [resolutionError];
|
||||
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<StylesheetEditorShell />
|
||||
</TooltipProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
const activate = await screen.findByRole("button", { name: "Activate Semantic CSS" });
|
||||
await waitFor(() => expect(activate).toBeEnabled());
|
||||
fireEvent.click(activate);
|
||||
|
||||
expect(builder.data?.metadata.stylesheet?.mode).toBe("semantic");
|
||||
});
|
||||
|
||||
it("keeps legacy activation disabled for fatal diagnostics", async () => {
|
||||
compileWorker.diagnostics = [fatalError];
|
||||
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<StylesheetEditorShell />
|
||||
</TooltipProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
await screen.findByText("Fatal error");
|
||||
expect(screen.getByRole("button", { name: "Activate Semantic CSS" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("makes editor mutation controls read-only while the resume is locked", () => {
|
||||
builder.isLocked = true;
|
||||
builder.canUndo = true;
|
||||
builder.canRedo = true;
|
||||
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
@@ -202,19 +357,10 @@ describe("StylesheetEditorShell", () => {
|
||||
expect(screen.getByRole("button", { name: "Undo stylesheet edit" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Redo stylesheet edit" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Format stylesheet" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Reset to applied stylesheet" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("moves the only visible editor into a titled mobile sheet", async () => {
|
||||
media.mobile = true;
|
||||
useStylesheetStore.setState({
|
||||
mode: "legacy",
|
||||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\n" },
|
||||
diagnostics: [{ ...error, severity: "warning" }],
|
||||
status: "idle",
|
||||
restoreLocked: false,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
@@ -231,8 +377,7 @@ describe("StylesheetEditorShell", () => {
|
||||
expect(within(sheet).getByRole("heading", { name: "Semantic CSS stylesheet" })).toBeInTheDocument();
|
||||
expect(within(sheet).getByRole("button", { name: "Activate Semantic CSS" })).toBeInTheDocument();
|
||||
expect(within(sheet).getByRole("toolbar", { name: "Stylesheet editor" })).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("Ready to activate with warnings")).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("Unknown property")).toBeInTheDocument();
|
||||
await within(sheet).findByText("Ready to activate");
|
||||
expectGuideLink(sheet);
|
||||
expect(document.querySelectorAll(".cm-editor")).toHaveLength(1);
|
||||
media.mobile = false;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import type { SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet";
|
||||
import type { SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { SemanticCssColorToken } from "./color-tokens";
|
||||
import type { SemanticCssEditorMetadata } from "./protocol";
|
||||
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
|
||||
@@ -17,11 +19,20 @@ import {
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { BookOpenIcon } from "@phosphor-icons/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
import { convertLegacyStyleRules } from "@reactive-resume/pdf/semantic-legacy";
|
||||
import {
|
||||
buildSemanticTree,
|
||||
getTemplateSemanticManifest,
|
||||
semanticNodeKeys,
|
||||
shouldShowResumeHeader,
|
||||
} from "@reactive-resume/pdf/semantic-tree";
|
||||
import { isFatalStylesheetDiagnostic } from "@reactive-resume/resume/stylesheet";
|
||||
import { PopoverTrigger } from "@reactive-resume/ui/components/popover";
|
||||
import { Sheet, SheetContent, SheetTitle } from "@reactive-resume/ui/components/sheet";
|
||||
import { ColorPicker } from "@/components/input/color-picker";
|
||||
import { useIsResumeLocked, useResumeData, useResumeStore, useUpdateResumeData } from "@/features/resume/builder/draft";
|
||||
import { useTheme } from "@/features/theme/provider";
|
||||
import { useBuilderSidebarStore } from "@/routes/builder/$resumeId/-store/sidebar";
|
||||
import { compositionAwareDocumentListener, createSemanticCssEditorExtensions } from "./editor-extensions";
|
||||
@@ -29,8 +40,8 @@ import { enterStylesheetFocusMode } from "./focus-mode";
|
||||
import { formatEditorDocument } from "./formatter";
|
||||
import { LegacyStylesheetBanner } from "./legacy-banner";
|
||||
import { StylesheetStatus } from "./status";
|
||||
import { useStylesheetStore } from "./store";
|
||||
import { StylesheetToolbar } from "./toolbar";
|
||||
import { createCompileWorkerClient } from "./worker-client";
|
||||
|
||||
const externalReplacement = Annotation.define<boolean>();
|
||||
const emptyMetadata: SemanticCssEditorMetadata = {
|
||||
@@ -314,30 +325,78 @@ type StylesheetEditorShellProps = {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
const pageDimensions = (data: ResumeData) => {
|
||||
const size = data.metadata.page.format === "letter" ? { width: 612, height: 792 } : { width: 595.28, height: 841.89 };
|
||||
return data.metadata.layout.pages.map((_page, index) => ({
|
||||
pageKey: semanticNodeKeys.page(index + 1),
|
||||
...size,
|
||||
}));
|
||||
};
|
||||
|
||||
const createEditorMetadata = (data: ResumeData): SemanticCssEditorMetadata => {
|
||||
const pages = data.metadata.layout.pages.map((page, index) =>
|
||||
buildSemanticTree({
|
||||
data,
|
||||
template: data.metadata.template,
|
||||
page,
|
||||
pageNumber: index + 1,
|
||||
showHeader: shouldShowResumeHeader(data, index),
|
||||
}),
|
||||
);
|
||||
const semanticTree: SemanticNode = {
|
||||
key: semanticNodeKeys.resume(),
|
||||
kind: "resume",
|
||||
attributes: { template: data.metadata.template },
|
||||
roles: [],
|
||||
children: pages.flatMap(({ children }) => children),
|
||||
};
|
||||
return {
|
||||
semanticTree,
|
||||
templateParts: getTemplateSemanticManifest(data.metadata.template).parts.map(({ name }) => name),
|
||||
};
|
||||
};
|
||||
|
||||
function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) {
|
||||
const { theme } = useTheme();
|
||||
const isMobile = useMediaQuery("(max-width: 767px)", { initializeWithValue: false });
|
||||
const [focusOpen, setFocusOpen] = useState(false);
|
||||
const [diagnostics, setDiagnostics] = useState<readonly SemanticCssDiagnostic[]>([]);
|
||||
const [colorTokens, setColorTokens] = useState<readonly SemanticCssColorToken[]>([]);
|
||||
const [status, setStatus] = useState<"idle" | "compiling" | "error">("compiling");
|
||||
const [compiler, setCompiler] = useState<ReturnType<typeof createCompileWorkerClient>>();
|
||||
const restoreDesktopRef = useRef<(() => void) | null>(null);
|
||||
const mode = useStylesheetStore((state) => state.mode);
|
||||
const source = useStylesheetStore((state) => state.source.text);
|
||||
const applied = useStylesheetStore((state) => state.applied.text);
|
||||
const diagnostics = useStylesheetStore((state) => state.diagnostics);
|
||||
const colorTokens = useStylesheetStore((state) => state.colorTokens);
|
||||
const metadata = useStylesheetStore((state) => state.editorMetadata);
|
||||
const status = useStylesheetStore((state) => state.status);
|
||||
const restoreLocked = useStylesheetStore((state) => state.restoreLocked);
|
||||
const canUndo = useStylesheetStore((state) => state.canUndo);
|
||||
const canRedo = useStylesheetStore((state) => state.canRedo);
|
||||
const setSourceText = useStylesheetStore((state) => state.setSourceText);
|
||||
const setFocused = useStylesheetStore((state) => state.setFocused);
|
||||
const activate = useStylesheetStore((state) => state.activate);
|
||||
const undo = useStylesheetStore((state) => state.undo);
|
||||
const redo = useStylesheetStore((state) => state.redo);
|
||||
const refreshIntelligence = useStylesheetStore((state) => state.refreshIntelligence);
|
||||
const data = useResumeData();
|
||||
const updateResumeData = useUpdateResumeData();
|
||||
const isLocked = useIsResumeLocked();
|
||||
const canUndo = useResumeStore((state) => state.canUndo);
|
||||
const canRedo = useResumeStore((state) => state.canRedo);
|
||||
const undo = useResumeStore((state) => state.undo);
|
||||
const redo = useResumeStore((state) => state.redo);
|
||||
const editorViewRef = useRef<EditorView | null>(null);
|
||||
const hasErrors = status === "error" || diagnostics.some(({ severity }) => severity === "error");
|
||||
const isChecking = status === "compiling" || status === "preflighting" || status === "saving";
|
||||
const compileGenerationRef = useRef(0);
|
||||
useEffect(() => {
|
||||
const client = createCompileWorkerClient(
|
||||
() =>
|
||||
new Worker(new URL("./stylesheet.worker.ts", import.meta.url), {
|
||||
type: "module",
|
||||
name: "semantic-css-compiler",
|
||||
}),
|
||||
);
|
||||
setCompiler(client);
|
||||
return () => client.destroy();
|
||||
}, []);
|
||||
const stylesheet = data?.metadata.stylesheet;
|
||||
const mode = stylesheet?.mode ?? "legacy";
|
||||
const source = useMemo<StylesheetSource>(
|
||||
() =>
|
||||
stylesheet?.source ??
|
||||
(data ? convertLegacyStyleRules(data).source : { languageVersion: 1, text: "@version 1;\n" }),
|
||||
[data, stylesheet],
|
||||
);
|
||||
const metadata = useMemo(() => (data ? createEditorMetadata(data) : emptyMetadata), [data]);
|
||||
const hasFatalErrors = status === "error" || diagnostics.some(isFatalStylesheetDiagnostic);
|
||||
const isChecking = status === "compiling";
|
||||
const disabled = readOnly || isLocked;
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -347,8 +406,59 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refreshIntelligence();
|
||||
}, [refreshIntelligence]);
|
||||
if (!compiler || !data) return;
|
||||
let cancelled = false;
|
||||
const editGeneration = ++compileGenerationRef.current;
|
||||
setStatus("compiling");
|
||||
setColorTokens([]);
|
||||
const timer = window.setTimeout(() => {
|
||||
void compiler
|
||||
.compile({
|
||||
editGeneration,
|
||||
source,
|
||||
semanticTree: metadata.semanticTree,
|
||||
baseSettings: {
|
||||
picture: data.picture,
|
||||
template: data.metadata.template,
|
||||
design: data.metadata.design,
|
||||
typography: data.metadata.typography,
|
||||
page: data.metadata.page,
|
||||
layout: { sidebarWidth: data.metadata.layout.sidebarWidth },
|
||||
},
|
||||
pages: pageDimensions(data),
|
||||
})
|
||||
.then((result) => {
|
||||
if (cancelled || result.editGeneration !== compileGenerationRef.current) return;
|
||||
setDiagnostics(result.diagnostics);
|
||||
setColorTokens(result.colorTokens ?? []);
|
||||
setStatus(result.program && !result.diagnostics.some(isFatalStylesheetDiagnostic) ? "idle" : "error");
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled && editGeneration === compileGenerationRef.current) setStatus("error");
|
||||
});
|
||||
}, 180);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [compiler, data, metadata, source]);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const setSourceText = (text: string) => {
|
||||
if (disabled || text === source.text) return;
|
||||
updateResumeData((draft) => {
|
||||
draft.metadata.stylesheet = { mode, source: { ...source, text } };
|
||||
});
|
||||
};
|
||||
|
||||
const activate = () => {
|
||||
if (disabled || mode === "semantic" || hasFatalErrors || isChecking) return;
|
||||
updateResumeData((draft) => {
|
||||
draft.metadata.stylesheet = { mode: "semantic", source };
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFocus = () => {
|
||||
if (isMobile) {
|
||||
@@ -374,15 +484,14 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps)
|
||||
|
||||
const editor = (
|
||||
<StylesheetCodeEditor
|
||||
value={source}
|
||||
value={source.text}
|
||||
diagnostics={diagnostics}
|
||||
colorTokens={colorTokens}
|
||||
metadata={metadata}
|
||||
theme={theme}
|
||||
readOnly={readOnly || restoreLocked}
|
||||
readOnly={disabled}
|
||||
label={t`Semantic CSS stylesheet`}
|
||||
onChange={setSourceText}
|
||||
onFocusChange={setFocused}
|
||||
onReady={(view) => {
|
||||
editorViewRef.current = view;
|
||||
}}
|
||||
@@ -393,22 +502,21 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps)
|
||||
const editorChrome = (
|
||||
<div className="space-y-3">
|
||||
{mode === "legacy" && (
|
||||
<LegacyStylesheetBanner disabled={restoreLocked || hasErrors || isChecking} onActivate={activate} />
|
||||
<LegacyStylesheetBanner disabled={disabled || hasFatalErrors || isChecking} onActivate={activate} />
|
||||
)}
|
||||
|
||||
<StylesheetToolbar
|
||||
source={source}
|
||||
source={source.text}
|
||||
canUndo={canUndo}
|
||||
canRedo={canRedo}
|
||||
focused={focusOpen}
|
||||
disabled={restoreLocked}
|
||||
disabled={disabled}
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
onFormat={() => {
|
||||
const view = editorViewRef.current;
|
||||
if (view) void formatEditorDocument(view).catch(() => undefined);
|
||||
}}
|
||||
onReset={() => setSourceText(applied)}
|
||||
onFocusToggle={toggleFocus}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { inspectPdfPageCount } from "./pdf-inspection";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
workerDestroy: vi.fn(),
|
||||
getDocument: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("pdfjs-dist/legacy/build/pdf.mjs", () => ({
|
||||
PDFWorker: class {
|
||||
destroy = mocks.workerDestroy;
|
||||
},
|
||||
getDocument: mocks.getDocument,
|
||||
}));
|
||||
|
||||
describe("inspectPdfPageCount", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("inspects a copy through a nested worker without detaching the result buffer", async () => {
|
||||
const destroy = vi.fn();
|
||||
const nestedWorker = { terminate: vi.fn() } as unknown as Worker;
|
||||
mocks.getDocument.mockReturnValue({ promise: Promise.resolve({ numPages: 3 }), destroy });
|
||||
const pdf = Uint8Array.of(1, 2, 3, 4).buffer;
|
||||
|
||||
await expect(inspectPdfPageCount(pdf, () => nestedWorker)).resolves.toBe(3);
|
||||
|
||||
expect(mocks.getDocument).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: expect.any(ArrayBuffer), worker: expect.any(Object) }),
|
||||
);
|
||||
const inspectedPdf = mocks.getDocument.mock.calls[0]?.[0].data as ArrayBuffer;
|
||||
expect(inspectedPdf).not.toBe(pdf);
|
||||
expect(Array.from(new Uint8Array(inspectedPdf))).toEqual([1, 2, 3, 4]);
|
||||
expect(Array.from(new Uint8Array(pdf))).toEqual([1, 2, 3, 4]);
|
||||
expect(destroy).toHaveBeenCalledOnce();
|
||||
expect(nestedWorker.terminate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("destroys the loading task and nested worker when parsing fails", async () => {
|
||||
const destroy = vi.fn();
|
||||
const nestedWorker = { terminate: vi.fn() } as unknown as Worker;
|
||||
mocks.getDocument.mockReturnValue({ promise: Promise.reject(new Error("invalid PDF")), destroy });
|
||||
|
||||
await expect(inspectPdfPageCount(new ArrayBuffer(4), () => nestedWorker)).rejects.toThrow("invalid PDF");
|
||||
|
||||
expect(destroy).toHaveBeenCalledOnce();
|
||||
expect(nestedWorker.terminate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("terminates the nested worker even when loading-task cleanup fails", async () => {
|
||||
const nestedWorker = { terminate: vi.fn() } as unknown as Worker;
|
||||
mocks.getDocument.mockReturnValue({
|
||||
promise: Promise.resolve({ numPages: 1 }),
|
||||
destroy: vi.fn().mockRejectedValue(new Error("cleanup failed")),
|
||||
});
|
||||
|
||||
await expect(inspectPdfPageCount(new ArrayBuffer(4), () => nestedWorker)).rejects.toThrow("cleanup failed");
|
||||
|
||||
expect(nestedWorker.terminate).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
let pdfModule: Promise<typeof import("pdfjs-dist/legacy/build/pdf.mjs")>;
|
||||
|
||||
const loadPdfModule = () => (pdfModule ??= import("pdfjs-dist/legacy/build/pdf.mjs"));
|
||||
|
||||
const createNestedWorker = () =>
|
||||
new Worker(new URL("pdfjs-dist/legacy/build/pdf.worker.min.mjs", import.meta.url), {
|
||||
type: "module",
|
||||
name: "semantic-css-pdfjs",
|
||||
});
|
||||
|
||||
export async function initializePdfInspection(): Promise<void> {
|
||||
await loadPdfModule();
|
||||
}
|
||||
|
||||
export async function inspectPdfPageCount(
|
||||
pdf: ArrayBuffer,
|
||||
createWorker: () => Worker = createNestedWorker,
|
||||
): Promise<number> {
|
||||
const { PDFWorker, getDocument } = await loadPdfModule();
|
||||
const nestedWorker = createWorker();
|
||||
const WorkerWithPort = PDFWorker as unknown as new (options: { port: Worker }) => InstanceType<typeof PDFWorker>;
|
||||
const worker = new WorkerWithPort({ port: nestedWorker });
|
||||
let loadingTask: ReturnType<typeof getDocument> | undefined;
|
||||
|
||||
try {
|
||||
// PDF.js transfers its input to the nested worker and detaches the buffer.
|
||||
// Keep the caller's buffer intact so preflight can return those same bytes.
|
||||
loadingTask = getDocument({ data: pdf.slice(0), worker });
|
||||
const document = await loadingTask.promise;
|
||||
return document.numPages;
|
||||
} finally {
|
||||
try {
|
||||
if (loadingTask) await loadingTask.destroy();
|
||||
else worker.destroy();
|
||||
} finally {
|
||||
nestedWorker.terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { PreflightWorkerRequest } from "./protocol";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
renderPreflightPdf: vi.fn(),
|
||||
initializePdfInspection: vi.fn(async () => undefined),
|
||||
inspectPdfPageCount: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@reactive-resume/pdf/preflight", () => ({
|
||||
renderPreflightPdf: mocks.renderPreflightPdf,
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-inspection", () => ({
|
||||
initializePdfInspection: mocks.initializePdfInspection,
|
||||
inspectPdfPageCount: mocks.inspectPdfPageCount,
|
||||
}));
|
||||
|
||||
describe("stylesheet preflight worker", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("serializes schema errors into a correlated preflight error packet", async () => {
|
||||
let handler: ((event: MessageEvent<PreflightWorkerRequest>) => Promise<void>) | undefined;
|
||||
const postMessage = vi.fn();
|
||||
vi.stubGlobal("self", {
|
||||
postMessage,
|
||||
addEventListener: vi.fn((_type, listener) => {
|
||||
handler = listener as typeof handler;
|
||||
}),
|
||||
});
|
||||
const issues = [{ path: ["customSections", 0, "items", 0, "company"] }];
|
||||
mocks.renderPreflightPdf.mockRejectedValueOnce(
|
||||
Object.assign(new Error("Invalid resume data"), { name: "ZodError", issues }),
|
||||
);
|
||||
vi.resetModules();
|
||||
await import("./preflight.worker");
|
||||
|
||||
await handler?.({
|
||||
data: {
|
||||
type: "preflight",
|
||||
requestId: 7,
|
||||
editGeneration: 3,
|
||||
input: {} as never,
|
||||
limits: {} as never,
|
||||
},
|
||||
} as unknown as MessageEvent<PreflightWorkerRequest>);
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith({
|
||||
type: "preflight_error",
|
||||
requestId: 7,
|
||||
editGeneration: 3,
|
||||
cause: { name: "ZodError", message: "Invalid resume data", issues },
|
||||
});
|
||||
});
|
||||
|
||||
it("includes a sanitized cause when PDF preflight throws an unexpected error", async () => {
|
||||
let handler: ((event: MessageEvent<PreflightWorkerRequest>) => Promise<void>) | undefined;
|
||||
const postMessage = vi.fn();
|
||||
vi.stubGlobal("self", {
|
||||
postMessage,
|
||||
addEventListener: vi.fn((_type, listener) => {
|
||||
handler = listener as typeof handler;
|
||||
}),
|
||||
});
|
||||
mocks.renderPreflightPdf.mockRejectedValueOnce(new Error("Canvas is already closed"));
|
||||
vi.resetModules();
|
||||
await import("./preflight.worker");
|
||||
|
||||
await handler?.({
|
||||
data: {
|
||||
type: "preflight",
|
||||
requestId: 8,
|
||||
editGeneration: 4,
|
||||
input: {} as never,
|
||||
limits: {} as never,
|
||||
},
|
||||
} as unknown as MessageEvent<PreflightWorkerRequest>);
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "preflight_result",
|
||||
requestId: 8,
|
||||
editGeneration: 4,
|
||||
result: expect.objectContaining({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: expect.stringContaining("Canvas is already closed"),
|
||||
diagnostics: [],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import type { PdfPreflightFailure } from "@reactive-resume/pdf/preflight";
|
||||
import type {
|
||||
PreflightWorkerError,
|
||||
PreflightWorkerRequest,
|
||||
PreflightWorkerResponse,
|
||||
SerializedPreflightCause,
|
||||
} from "./protocol";
|
||||
import { Buffer } from "buffer";
|
||||
import { initializePdfInspection, inspectPdfPageCount } from "./pdf-inspection";
|
||||
import { getPreflightTransferables } from "./protocol";
|
||||
|
||||
Object.assign(globalThis, { Buffer });
|
||||
|
||||
const failure = (code: PdfPreflightFailure["code"], message: string): PdfPreflightFailure => ({
|
||||
ok: false,
|
||||
code,
|
||||
message,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const sanitizeWorkerCause = (cause: unknown): string => {
|
||||
if (!(cause instanceof Error)) return "The PDF preflight worker failed.";
|
||||
const detail = cause.message.replace(/\s+/g, " ").trim().slice(0, 200);
|
||||
if (!detail) return "The PDF preflight worker failed.";
|
||||
return `The PDF preflight worker failed. (${cause.name}: ${detail})`;
|
||||
};
|
||||
|
||||
const serializeZodCause = (cause: unknown): SerializedPreflightCause | undefined => {
|
||||
if (!(cause instanceof Error) || cause.name !== "ZodError" || !("issues" in cause) || !Array.isArray(cause.issues)) {
|
||||
return;
|
||||
}
|
||||
return { name: cause.name, message: cause.message, issues: cause.issues };
|
||||
};
|
||||
|
||||
const initialization = Promise.all([import("@reactive-resume/pdf/preflight"), initializePdfInspection()] as const);
|
||||
void initialization.then(() => self.postMessage({ type: "preflight_ready" }));
|
||||
|
||||
self.addEventListener("message", async ({ data }: MessageEvent<PreflightWorkerRequest>) => {
|
||||
if (data.type !== "preflight") return;
|
||||
const [{ renderPreflightPdf }] = await initialization;
|
||||
let rendered: Awaited<ReturnType<typeof renderPreflightPdf>>;
|
||||
|
||||
try {
|
||||
rendered = await renderPreflightPdf(data.input, data.limits);
|
||||
} catch (cause) {
|
||||
const serializedCause = serializeZodCause(cause);
|
||||
if (serializedCause) {
|
||||
const response: PreflightWorkerError = {
|
||||
type: "preflight_error",
|
||||
requestId: data.requestId,
|
||||
editGeneration: data.editGeneration,
|
||||
cause: serializedCause,
|
||||
};
|
||||
self.postMessage(response);
|
||||
return;
|
||||
}
|
||||
const result = failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", sanitizeWorkerCause(cause));
|
||||
const response: PreflightWorkerResponse = {
|
||||
type: "preflight_result",
|
||||
requestId: data.requestId,
|
||||
editGeneration: data.editGeneration,
|
||||
result,
|
||||
};
|
||||
self.postMessage(response);
|
||||
return;
|
||||
}
|
||||
|
||||
let result: PreflightWorkerResponse["result"];
|
||||
if (!rendered.ok) {
|
||||
result = rendered;
|
||||
} else if (rendered.bytes.byteLength > data.limits.maxBytes) {
|
||||
result = failure("STYLESHEET_PREFLIGHT_BYTE_LIMIT", "The PDF exceeds the preflight byte limit.");
|
||||
} else {
|
||||
try {
|
||||
const pdf = Uint8Array.from(rendered.bytes).buffer;
|
||||
const pageCount = await inspectPdfPageCount(pdf);
|
||||
result =
|
||||
pageCount > data.limits.maxPages
|
||||
? failure("STYLESHEET_PREFLIGHT_PAGE_LIMIT", "The PDF exceeds the preflight page limit.")
|
||||
: {
|
||||
ok: true,
|
||||
pageCount,
|
||||
byteCount: pdf.byteLength,
|
||||
diagnostics: rendered.diagnostics,
|
||||
pdf,
|
||||
};
|
||||
} catch {
|
||||
result = failure("STYLESHEET_PREFLIGHT_PARSE_FAILED", "The generated PDF could not be inspected.");
|
||||
}
|
||||
}
|
||||
|
||||
const response: PreflightWorkerResponse = {
|
||||
type: "preflight_result",
|
||||
requestId: data.requestId,
|
||||
editGeneration: data.editGeneration,
|
||||
result,
|
||||
};
|
||||
self.postMessage(response, { transfer: getPreflightTransferables(response) });
|
||||
});
|
||||
@@ -1,8 +1,3 @@
|
||||
import type {
|
||||
BrowserPdfPreflightResult,
|
||||
PdfPreflightPageLimits,
|
||||
StylesheetPreflightInput,
|
||||
} from "@reactive-resume/pdf/preflight";
|
||||
import type {
|
||||
AuthoredPageContext,
|
||||
BaseSettingsSnapshot,
|
||||
@@ -39,47 +34,3 @@ export type CompileWorkerResponse = {
|
||||
diagnostics: readonly SemanticCssDiagnostic[];
|
||||
colorTokens?: readonly SemanticCssColorToken[];
|
||||
};
|
||||
|
||||
type PreflightLimits = PdfPreflightPageLimits & {
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
};
|
||||
|
||||
export type PreflightWorkerInput = {
|
||||
editGeneration: number;
|
||||
input: StylesheetPreflightInput;
|
||||
limits: PreflightLimits;
|
||||
};
|
||||
|
||||
export type PreflightWorkerRequest = PreflightWorkerInput & {
|
||||
type: "preflight";
|
||||
requestId: number;
|
||||
};
|
||||
|
||||
export type PreflightWorkerResponse = {
|
||||
type: "preflight_result";
|
||||
requestId: number;
|
||||
editGeneration: number;
|
||||
result: BrowserPdfPreflightResult;
|
||||
};
|
||||
|
||||
export type SerializedPreflightCause = {
|
||||
name: string;
|
||||
message: string;
|
||||
issues: readonly unknown[];
|
||||
};
|
||||
|
||||
export type PreflightWorkerError = {
|
||||
type: "preflight_error";
|
||||
requestId: number;
|
||||
editGeneration: number;
|
||||
cause: SerializedPreflightCause;
|
||||
};
|
||||
|
||||
export type PreflightWorkerReady = {
|
||||
type: "preflight_ready";
|
||||
};
|
||||
|
||||
export function getPreflightTransferables(response: PreflightWorkerResponse): Transferable[] {
|
||||
return response.result.ok ? [response.result.pdf] : [];
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { SemanticStylesheet } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getState: vi.fn(),
|
||||
workers: [] as FakeWorker[],
|
||||
}));
|
||||
|
||||
class FakeWorker {
|
||||
terminated = false;
|
||||
|
||||
constructor() {
|
||||
mocks.workers.push(this);
|
||||
}
|
||||
|
||||
postMessage() {}
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
terminate() {
|
||||
this.terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("@/libs/orpc/client", () => ({
|
||||
orpc: {
|
||||
resume: {
|
||||
stylesheet: {
|
||||
getState: { call: mocks.getState },
|
||||
mutate: { call: vi.fn() },
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const stylesheet = (text: string): SemanticStylesheet => {
|
||||
const source = { languageVersion: 1, text };
|
||||
return { mode: "semantic", source, applied: source };
|
||||
};
|
||||
|
||||
describe("stylesheet store reinitialization", () => {
|
||||
beforeEach(() => {
|
||||
mocks.workers.length = 0;
|
||||
vi.stubGlobal("Worker", FakeWorker);
|
||||
});
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("suspends edits while a delayed restore is pending, then atomically installs the restored state", async () => {
|
||||
const storeModule = await import("./store");
|
||||
const cleanup = storeModule.initializeStylesheetStore({
|
||||
resumeId: "resume-1",
|
||||
initial: { stylesheet: stylesheet("old"), revision: 3, renderDataVersion: 7 },
|
||||
resumeData: defaultResumeData,
|
||||
});
|
||||
let finishRestore: (() => void) | undefined;
|
||||
const delayedRestore = new Promise<void>((resolve) => {
|
||||
finishRestore = resolve;
|
||||
});
|
||||
const restore = async () => {
|
||||
const token = storeModule.lockStylesheetStoreForRestore("resume-1");
|
||||
await delayedRestore;
|
||||
return storeModule.replaceStylesheetStoreAfterRestore({
|
||||
resumeId: "resume-1",
|
||||
resumeData: defaultResumeData,
|
||||
initial: { stylesheet: stylesheet("restored"), revision: 9, renderDataVersion: 12 },
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
||||
const pendingRestore = restore();
|
||||
expect(storeModule.useStylesheetStore.getState().restoreLocked).toBe(true);
|
||||
const editGeneration = storeModule.useStylesheetStore.getState().editGeneration;
|
||||
storeModule.useStylesheetStore.getState().setSourceText("edit while restoring");
|
||||
storeModule.useStylesheetStore.getState().deactivate();
|
||||
storeModule.useStylesheetStore.getState().undo();
|
||||
expect(storeModule.useStylesheetStore.getState().source.text).toBe("old");
|
||||
expect(storeModule.useStylesheetStore.getState().editGeneration).toBe(editGeneration);
|
||||
finishRestore?.();
|
||||
const replaced = await pendingRestore;
|
||||
expect(replaced).toBe(true);
|
||||
expect(mocks.getState).not.toHaveBeenCalled();
|
||||
expect(storeModule.useStylesheetStore.getState()).toMatchObject({
|
||||
resumeId: "resume-1",
|
||||
source: { text: "restored" },
|
||||
applied: { text: "restored" },
|
||||
revision: 9,
|
||||
renderDataVersion: 12,
|
||||
restoreLocked: false,
|
||||
});
|
||||
storeModule.useStylesheetStore.getState().setSourceText("later edit");
|
||||
expect(storeModule.useStylesheetStore.getState().source.text).toBe("later edit");
|
||||
expect(mocks.workers).toHaveLength(4);
|
||||
expect(mocks.workers.slice(0, 2).every((worker) => worker.terminated)).toBe(true);
|
||||
|
||||
cleanup();
|
||||
|
||||
expect(mocks.workers.slice(2).every((worker) => worker.terminated)).toBe(true);
|
||||
expect(storeModule.useStylesheetStore.getState().resumeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("unlocks interaction after a restore request fails", async () => {
|
||||
const storeModule = await import("./store");
|
||||
const cleanup = storeModule.initializeStylesheetStore({
|
||||
resumeId: "resume-1",
|
||||
initial: { stylesheet: stylesheet("old"), revision: 3, renderDataVersion: 7 },
|
||||
resumeData: defaultResumeData,
|
||||
});
|
||||
|
||||
const token = storeModule.lockStylesheetStoreForRestore("resume-1");
|
||||
expect(storeModule.useStylesheetStore.getState().restoreLocked).toBe(true);
|
||||
expect(storeModule.unlockStylesheetStoreAfterRestore(token)).toBe(true);
|
||||
expect(storeModule.useStylesheetStore.getState().restoreLocked).toBe(false);
|
||||
|
||||
storeModule.useStylesheetStore.getState().setSourceText("edit after failure");
|
||||
expect(storeModule.useStylesheetStore.getState().source.text).toBe("edit after failure");
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("ignores a stale same-resume restore completion after away-and-back runtime replacement", async () => {
|
||||
const storeModule = await import("./store");
|
||||
const cleanupFirst = storeModule.initializeStylesheetStore({
|
||||
resumeId: "resume-1",
|
||||
initial: { stylesheet: stylesheet("first"), revision: 1, renderDataVersion: 1 },
|
||||
resumeData: defaultResumeData,
|
||||
});
|
||||
const staleToken = storeModule.lockStylesheetStoreForRestore("resume-1");
|
||||
|
||||
cleanupFirst();
|
||||
const cleanupSecond = storeModule.initializeStylesheetStore({
|
||||
resumeId: "resume-1",
|
||||
initial: { stylesheet: stylesheet("second"), revision: 2, renderDataVersion: 2 },
|
||||
resumeData: defaultResumeData,
|
||||
});
|
||||
|
||||
const replaced = storeModule.replaceStylesheetStoreAfterRestore({
|
||||
resumeId: "resume-1",
|
||||
resumeData: defaultResumeData,
|
||||
initial: { stylesheet: stylesheet("stale restore"), revision: 3, renderDataVersion: 3 },
|
||||
token: staleToken,
|
||||
});
|
||||
|
||||
expect(replaced).toBe(false);
|
||||
expect(storeModule.useStylesheetStore.getState()).toMatchObject({
|
||||
source: { text: "second" },
|
||||
revision: 2,
|
||||
renderDataVersion: 2,
|
||||
});
|
||||
|
||||
cleanupSecond();
|
||||
});
|
||||
});
|
||||
@@ -1,50 +1,67 @@
|
||||
import type { SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { WarningCircleIcon, WarningIcon } from "@phosphor-icons/react";
|
||||
import { isFatalStylesheetDiagnostic } from "@reactive-resume/resume/stylesheet";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
|
||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
|
||||
|
||||
export type StylesheetStatusProps = {
|
||||
mode: "legacy" | "semantic";
|
||||
status: "idle" | "compiling" | "preflighting" | "saving" | "applied" | "error";
|
||||
status: "idle" | "compiling" | "error";
|
||||
diagnostics: readonly SemanticCssDiagnostic[];
|
||||
};
|
||||
|
||||
export function StylesheetStatus({ mode, status, diagnostics }: StylesheetStatusProps) {
|
||||
const errors = diagnostics.filter(({ severity }) => severity === "error");
|
||||
const warnings = diagnostics.filter(({ severity }) => severity === "warning");
|
||||
const hasErrors = status === "error" || errors.length > 0;
|
||||
const isPending = status === "compiling" || status === "preflighting" || status === "saving";
|
||||
const hasFatalErrors = status === "error" || diagnostics.some(isFatalStylesheetDiagnostic);
|
||||
const hasRecoverableErrors = !hasFatalErrors && errors.length > 0;
|
||||
const isPending = status === "compiling";
|
||||
|
||||
return (
|
||||
<div className="space-y-2" aria-live="polite">
|
||||
{hasErrors ? (
|
||||
{hasFatalErrors ? (
|
||||
<Badge variant="destructive">
|
||||
<WarningCircleIcon data-icon="inline-start" />
|
||||
<Trans>Error</Trans>
|
||||
<Trans>Fatal error</Trans>
|
||||
</Badge>
|
||||
) : isPending ? (
|
||||
<Badge variant="outline">{mode === "legacy" ? <Trans>Checking draft</Trans> : <Trans>Checking</Trans>}</Badge>
|
||||
) : hasRecoverableErrors ? (
|
||||
<Badge variant="secondary">
|
||||
<WarningCircleIcon data-icon="inline-start" />
|
||||
{mode === "legacy" ? <Trans>Ready to activate with errors</Trans> : <Trans>Valid with errors</Trans>}
|
||||
</Badge>
|
||||
) : warnings.length > 0 ? (
|
||||
<Badge variant="secondary">
|
||||
<WarningIcon data-icon="inline-start" />
|
||||
{mode === "legacy" ? <Trans>Ready to activate with warnings</Trans> : <Trans>Applied with warnings</Trans>}
|
||||
{mode === "legacy" ? <Trans>Ready to activate with warnings</Trans> : <Trans>Valid with warnings</Trans>}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
{mode === "legacy" ? <Trans>Ready to activate</Trans> : <Trans>Applied</Trans>}
|
||||
</Badge>
|
||||
<Badge variant="secondary">{mode === "legacy" ? <Trans>Ready to activate</Trans> : <Trans>Valid</Trans>}</Badge>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
{hasFatalErrors && (
|
||||
<Alert variant="destructive">
|
||||
<WarningCircleIcon />
|
||||
<AlertTitle>
|
||||
<Trans>Stylesheet has errors</Trans>
|
||||
<Trans>Stylesheet has fatal errors</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview and export use the last valid version.</Trans>
|
||||
<Trans>Preview and export fall back to base styles.</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{hasRecoverableErrors && (
|
||||
<Alert>
|
||||
<WarningCircleIcon />
|
||||
<AlertTitle>
|
||||
<Trans>Some styles were ignored</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview and export keep valid styles and ignore invalid styles.</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -1,940 +0,0 @@
|
||||
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { createStylesheetStoreRuntime } from "./store";
|
||||
|
||||
const source = (text: string): StylesheetSource => ({ languageVersion: 1, text });
|
||||
const stylesheet = (text: string): SemanticStylesheet => ({
|
||||
mode: "semantic",
|
||||
source: source(text),
|
||||
applied: source(text),
|
||||
});
|
||||
|
||||
const initial = {
|
||||
stylesheet: stylesheet("generation zero"),
|
||||
revision: 3,
|
||||
renderDataVersion: 7,
|
||||
};
|
||||
|
||||
type MutationResult = {
|
||||
stylesheet: SemanticStylesheet;
|
||||
revision: number;
|
||||
renderDataVersion: number;
|
||||
editGeneration: number;
|
||||
diagnostics: [];
|
||||
};
|
||||
|
||||
describe("stylesheet store runtime", () => {
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
|
||||
it("clears compiler-confirmed color tokens synchronously when same-length source text changes", () => {
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial: { ...initial, stylesheet: stylesheet("section { color: red; }") },
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 1_000_000,
|
||||
compile: vi.fn(),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn(),
|
||||
});
|
||||
runtime.store.setState({ colorTokens: [{ from: 17, to: 20, value: "red" }] });
|
||||
|
||||
runtime.store.getState().setSourceText("section { color: var; }");
|
||||
|
||||
expect(runtime.store.getState().source.text).toBe("section { color: var; }");
|
||||
expect(runtime.store.getState().colorTokens).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects delayed editor intelligence for a canonically replaced source", async () => {
|
||||
let resolveCompile!: (value: {
|
||||
type: "compile_result";
|
||||
requestId: number;
|
||||
editGeneration: number;
|
||||
program: { languageVersion: number; rules: [] };
|
||||
diagnostics: [
|
||||
{
|
||||
code: string;
|
||||
severity: "error";
|
||||
message: string;
|
||||
range: {
|
||||
start: { line: number; column: number; offset: number };
|
||||
end: { line: number; column: number; offset: number };
|
||||
};
|
||||
},
|
||||
];
|
||||
colorTokens: [{ from: number; to: number; value: string }];
|
||||
}) => void;
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial: { ...initial, stylesheet: stylesheet("section { color: red; }") },
|
||||
resumeData: defaultResumeData,
|
||||
compile: () => new Promise((resolve) => (resolveCompile = resolve)),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn(),
|
||||
});
|
||||
|
||||
runtime.store.getState().refreshIntelligence();
|
||||
runtime.rebaseCanonical({
|
||||
stylesheet: stylesheet("section { color: blue; }"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
});
|
||||
resolveCompile({
|
||||
type: "compile_result",
|
||||
requestId: 1,
|
||||
editGeneration: 0,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [
|
||||
{
|
||||
code: "OLD_SOURCE",
|
||||
severity: "error",
|
||||
message: "Old source diagnostic",
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 2, offset: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
colorTokens: [{ from: 17, to: 20, value: "red" }],
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(runtime.store.getState()).toMatchObject({
|
||||
source: source("section { color: blue; }"),
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("consumes stale acknowledgements before saving the replaceable pending edit", async () => {
|
||||
const resolvers: Array<(value: MutationResult) => void> = [];
|
||||
const mutate = vi.fn(
|
||||
(_input: unknown) =>
|
||||
new Promise<MutationResult>((resolve) => {
|
||||
resolvers.push((value) => resolve(value));
|
||||
}),
|
||||
);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 4, diagnostics: [], pdf: new ArrayBuffer(4) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("generation one");
|
||||
await vi.runAllTimersAsync();
|
||||
runtime.store.getState().setSourceText("generation two");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
resolvers[0]?.({
|
||||
stylesheet: stylesheet("generation one"),
|
||||
revision: 4,
|
||||
renderDataVersion: 8,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState()).toMatchObject({
|
||||
revision: 4,
|
||||
renderDataVersion: 8,
|
||||
source: source("generation two"),
|
||||
applied: source("generation zero"),
|
||||
});
|
||||
expect(mutate).toHaveBeenCalledTimes(2);
|
||||
expect(mutate.mock.calls[1]?.[0]).toMatchObject({
|
||||
expectedRevision: 4,
|
||||
expectedRenderDataVersion: 8,
|
||||
editGeneration: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("rebases conflicts without dropping the focused local draft", async () => {
|
||||
const mutate = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce({
|
||||
code: "STYLESHEET_REVISION_CONFLICT",
|
||||
data: {
|
||||
state: { stylesheet: stylesheet("remote"), revision: 8, renderDataVersion: 11 },
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stylesheet: stylesheet("local unsaved source"),
|
||||
revision: 9,
|
||||
renderDataVersion: 11,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setFocused(true);
|
||||
runtime.store.getState().setSourceText("local unsaved source");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState()).toMatchObject({
|
||||
revision: 9,
|
||||
renderDataVersion: 11,
|
||||
source: source("local unsaved source"),
|
||||
});
|
||||
expect(mutate.mock.calls[1]?.[0]).toMatchObject({ expectedRevision: 8, expectedRenderDataVersion: 11 });
|
||||
});
|
||||
|
||||
it("keeps the newer pending edit when an older request conflicts", async () => {
|
||||
let rejectFirst!: (error: unknown) => void;
|
||||
const mutate = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectFirst = reject;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({
|
||||
stylesheet: stylesheet("newer"),
|
||||
revision: 9,
|
||||
renderDataVersion: 11,
|
||||
editGeneration: 2,
|
||||
diagnostics: [],
|
||||
});
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("older");
|
||||
await vi.runAllTimersAsync();
|
||||
runtime.store.getState().setSourceText("newer");
|
||||
await vi.runAllTimersAsync();
|
||||
rejectFirst({
|
||||
code: "STYLESHEET_REVISION_CONFLICT",
|
||||
data: { state: { stylesheet: stylesheet("remote"), revision: 8, renderDataVersion: 11 } },
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mutate.mock.calls[1]?.[0]).toMatchObject({
|
||||
transition: "edit_source",
|
||||
editGeneration: 2,
|
||||
source: source("newer"),
|
||||
});
|
||||
});
|
||||
|
||||
it("invalidates pending preflight eligibility on content changes and keeps versions monotonic", async () => {
|
||||
let resolveMutation!: (result: MutationResult) => void;
|
||||
let resolveRepreflight!: (result: {
|
||||
type: "preflight_result";
|
||||
requestId: number;
|
||||
editGeneration: number;
|
||||
result: {
|
||||
ok: true;
|
||||
pageCount: number;
|
||||
byteCount: number;
|
||||
diagnostics: [];
|
||||
pdf: ArrayBuffer;
|
||||
};
|
||||
}) => void;
|
||||
const mutate = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(new Promise<MutationResult>((resolve) => (resolveMutation = resolve)))
|
||||
.mockResolvedValueOnce({
|
||||
stylesheet: stylesheet("newer"),
|
||||
revision: 11,
|
||||
renderDataVersion: 20,
|
||||
editGeneration: 2,
|
||||
diagnostics: [],
|
||||
});
|
||||
const preflight = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}))
|
||||
.mockImplementationOnce(async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}))
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveRepreflight = resolve;
|
||||
}),
|
||||
);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight,
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("older");
|
||||
await vi.runAllTimersAsync();
|
||||
runtime.store.getState().setSourceText("newer");
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mutate).toHaveBeenCalledTimes(1);
|
||||
|
||||
runtime.replaceResumeSnapshot(defaultResumeData, {
|
||||
stylesheet: stylesheet("remote"),
|
||||
revision: 10,
|
||||
renderDataVersion: 20,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
expect(preflight).toHaveBeenCalledTimes(3);
|
||||
|
||||
resolveMutation({
|
||||
stylesheet: stylesheet("older"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(runtime.store.getState()).toMatchObject({ revision: 10, renderDataVersion: 20 });
|
||||
expect(mutate).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveRepreflight({
|
||||
type: "preflight_result",
|
||||
requestId: 3,
|
||||
editGeneration: 2,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mutate.mock.calls[1]?.[0]).toMatchObject({
|
||||
source: source("newer"),
|
||||
expectedRevision: 10,
|
||||
expectedRenderDataVersion: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not requeue an invalidated in-flight candidate after conflict", async () => {
|
||||
let rejectMutation!: (error: unknown) => void;
|
||||
let resolveRepreflight!: (result: {
|
||||
type: "preflight_result";
|
||||
requestId: number;
|
||||
editGeneration: number;
|
||||
result: {
|
||||
ok: true;
|
||||
pageCount: number;
|
||||
byteCount: number;
|
||||
diagnostics: [];
|
||||
pdf: ArrayBuffer;
|
||||
};
|
||||
}) => void;
|
||||
const mutate = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(new Promise<MutationResult>((_resolve, reject) => (rejectMutation = reject)))
|
||||
.mockResolvedValueOnce({
|
||||
stylesheet: stylesheet("newer"),
|
||||
revision: 11,
|
||||
renderDataVersion: 20,
|
||||
editGeneration: 2,
|
||||
diagnostics: [],
|
||||
});
|
||||
const preflight = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}))
|
||||
.mockImplementationOnce(async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}))
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveRepreflight = resolve;
|
||||
}),
|
||||
);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight,
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("older");
|
||||
await vi.runAllTimersAsync();
|
||||
runtime.store.getState().setSourceText("newer");
|
||||
await vi.runAllTimersAsync();
|
||||
runtime.replaceResumeSnapshot(defaultResumeData, {
|
||||
stylesheet: stylesheet("remote"),
|
||||
revision: 10,
|
||||
renderDataVersion: 20,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
rejectMutation({
|
||||
code: "STYLESHEET_REVISION_CONFLICT",
|
||||
data: { state: { stylesheet: stylesheet("remote"), revision: 10, renderDataVersion: 20 } },
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(mutate).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveRepreflight({
|
||||
type: "preflight_result",
|
||||
requestId: 3,
|
||||
editGeneration: 2,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mutate.mock.calls[1]?.[0]).toMatchObject({
|
||||
source: source("newer"),
|
||||
expectedRevision: 10,
|
||||
expectedRenderDataVersion: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("reconciles a deferred focused canonical source on blur", () => {
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
compile: vi.fn(),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn(),
|
||||
});
|
||||
|
||||
runtime.store.getState().setFocused(true);
|
||||
runtime.rebaseCanonical({
|
||||
stylesheet: stylesheet("remote"),
|
||||
revision: 8,
|
||||
renderDataVersion: 11,
|
||||
});
|
||||
|
||||
expect(runtime.store.getState()).toMatchObject({
|
||||
source: source("generation zero"),
|
||||
applied: source("remote"),
|
||||
revision: 8,
|
||||
renderDataVersion: 11,
|
||||
});
|
||||
|
||||
runtime.store.getState().setFocused(false);
|
||||
expect(runtime.store.getState()).toMatchObject({
|
||||
source: source("remote"),
|
||||
applied: source("remote"),
|
||||
});
|
||||
});
|
||||
|
||||
it("persists invalid source while preserving applied and restores stylesheet history separately", async () => {
|
||||
const mutate = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
stylesheet: {
|
||||
mode: "semantic",
|
||||
source: source("invalid {"),
|
||||
applied: source("generation zero"),
|
||||
},
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration: 1,
|
||||
diagnostics: [
|
||||
{
|
||||
code: "PARSE_ERROR",
|
||||
severity: "error",
|
||||
message: "Invalid",
|
||||
range: { start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 1, offset: 0 } },
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stylesheet: stylesheet("generation zero"),
|
||||
revision: 5,
|
||||
renderDataVersion: 7,
|
||||
editGeneration: 2,
|
||||
diagnostics: [],
|
||||
});
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration, source: candidate }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: candidate.text === "invalid {" ? null : { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("invalid {");
|
||||
await vi.runAllTimersAsync();
|
||||
expect(runtime.store.getState()).toMatchObject({
|
||||
source: source("invalid {"),
|
||||
applied: source("generation zero"),
|
||||
});
|
||||
expect(mutate.mock.calls[0]?.[0]).toMatchObject({ transition: "edit_source", source: source("invalid {") });
|
||||
|
||||
runtime.store.getState().undo();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mutate.mock.calls[1]?.[0]).toMatchObject({
|
||||
transition: "restore_history",
|
||||
restore: stylesheet("generation zero"),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not publish historical applied state before restore acknowledgement", async () => {
|
||||
const mutate = vi.fn(() => new Promise<MutationResult>(() => {}));
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial: { ...initial, stylesheet: stylesheet("current applied") },
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
runtime.store.setState({
|
||||
source: source("local invalid"),
|
||||
applied: source("current applied"),
|
||||
undoStack: [stylesheet("historical")],
|
||||
canUndo: true,
|
||||
});
|
||||
|
||||
runtime.store.getState().undo();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().source).toEqual(source("historical"));
|
||||
expect(runtime.store.getState().applied).toEqual(source("current applied"));
|
||||
expect(mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ transition: "restore_history", restore: stylesheet("historical") }),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
|
||||
it("retries the focused draft against a newer content render-data version", async () => {
|
||||
const mutate = vi.fn().mockResolvedValue({
|
||||
stylesheet: stylesheet("local"),
|
||||
revision: 4,
|
||||
renderDataVersion: 12,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setFocused(true);
|
||||
runtime.store.getState().setSourceText("local");
|
||||
runtime.replaceResumeSnapshot(defaultResumeData, {
|
||||
stylesheet: stylesheet("remote"),
|
||||
revision: 9,
|
||||
renderDataVersion: 12,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().source).toEqual(source("local"));
|
||||
expect(mutate).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ expectedRevision: 9, expectedRenderDataVersion: 12, source: source("local") }),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a queued draft when content changes after editor blur", async () => {
|
||||
const mutate = vi.fn().mockResolvedValue({
|
||||
stylesheet: stylesheet("local"),
|
||||
revision: 10,
|
||||
renderDataVersion: 12,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("local");
|
||||
runtime.replaceResumeSnapshot(defaultResumeData, {
|
||||
stylesheet: stylesheet("remote"),
|
||||
revision: 9,
|
||||
renderDataVersion: 12,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().source).toEqual(source("local"));
|
||||
expect(mutate).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ expectedRevision: 9, expectedRenderDataVersion: 12, source: source("local") }),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
|
||||
it("queues activation only after browser preflight succeeds", async () => {
|
||||
const mutate = vi.fn().mockResolvedValue({
|
||||
stylesheet: stylesheet("generation zero"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration: 2,
|
||||
diagnostics: [],
|
||||
});
|
||||
const preflight = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
type: "preflight_result",
|
||||
requestId: 1,
|
||||
editGeneration: 1,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_RENDER_FAILED",
|
||||
message: "failed",
|
||||
diagnostics: [],
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
type: "preflight_result",
|
||||
requestId: 2,
|
||||
editGeneration: 2,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
});
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial: { ...initial, stylesheet: { ...initial.stylesheet, mode: "legacy" } },
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight,
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().activate();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mutate).not.toHaveBeenCalled();
|
||||
|
||||
runtime.store.getState().activate();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ transition: "activate", source: source("generation zero") }),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not leave Checking stuck when compile rejects for the current edit", async () => {
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: () => Promise.reject(new Error("Discarded stale stylesheet compiler result.")),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn(),
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("edited source");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().status).not.toBe("compiling");
|
||||
expect(runtime.store.getState().status).toBe("error");
|
||||
});
|
||||
|
||||
it("surfaces browser preflight failures as diagnostics when the worker returns an empty list", async () => {
|
||||
let resolveMutate!: (value: MutationResult & { diagnostics: never[] }) => void;
|
||||
const mutate = vi.fn(
|
||||
() =>
|
||||
new Promise<MutationResult & { diagnostics: never[] }>((resolve) => {
|
||||
resolveMutate = resolve;
|
||||
}),
|
||||
);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed.",
|
||||
diagnostics: [],
|
||||
},
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("@version 1;\nsection { color: teal; }");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().diagnostics).toEqual([
|
||||
expect.objectContaining({
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
severity: "error",
|
||||
message: "The PDF preflight worker failed.",
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 1, offset: 0 },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(runtime.store.getState().diagnostics.some(({ severity }) => severity === "error")).toBe(true);
|
||||
expect(mutate).toHaveBeenCalled();
|
||||
resolveMutate({
|
||||
stylesheet: stylesheet("@version 1;\n"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
it("finishes an edit when refreshIntelligence interleaves through the shared compile client", async () => {
|
||||
const { createCompileWorkerClient } = await import("./worker-client");
|
||||
const listeners = new Map<string, Set<EventListener>>();
|
||||
const fake = {
|
||||
postMessage: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
addEventListener: vi.fn((type: string, listener: EventListener) => {
|
||||
const bucket = listeners.get(type) ?? new Set();
|
||||
bucket.add(listener);
|
||||
listeners.set(type, bucket);
|
||||
}),
|
||||
removeEventListener: vi.fn((type: string, listener: EventListener) => {
|
||||
listeners.get(type)?.delete(listener);
|
||||
}),
|
||||
emit(data: unknown) {
|
||||
for (const listener of listeners.get("message") ?? []) {
|
||||
(listener as (event: MessageEvent) => void)(new MessageEvent("message", { data }));
|
||||
}
|
||||
},
|
||||
};
|
||||
const client = createCompileWorkerClient(() => fake);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: client.compile,
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate: async ({ editGeneration }) => ({
|
||||
stylesheet: stylesheet("edited source"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration,
|
||||
diagnostics: [],
|
||||
}),
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("edited source");
|
||||
await vi.runAllTimersAsync();
|
||||
expect(runtime.store.getState().status).toBe("compiling");
|
||||
expect(fake.postMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Mobile Design remount / accordion reopen refreshes intelligence while the edit is compiling.
|
||||
runtime.store.getState().refreshIntelligence();
|
||||
expect(fake.postMessage).toHaveBeenCalledTimes(2);
|
||||
|
||||
fake.emit({
|
||||
type: "compile_result",
|
||||
requestId: 1,
|
||||
editGeneration: 1,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
});
|
||||
fake.emit({
|
||||
type: "compile_result",
|
||||
requestId: 2,
|
||||
editGeneration: 1,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().status).toBe("applied");
|
||||
expect(runtime.store.getState().applied.text).toBe("edited source");
|
||||
});
|
||||
|
||||
it("terminates both worker clients and clears the store on cleanup", () => {
|
||||
const destroy = vi.fn();
|
||||
let mutationSignal: AbortSignal | undefined;
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
compile: vi.fn(),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn((_input: unknown, signal: AbortSignal) => {
|
||||
mutationSignal = signal;
|
||||
return new Promise<MutationResult>(() => {});
|
||||
}),
|
||||
destroy,
|
||||
});
|
||||
|
||||
runtime.store.getState().deactivate();
|
||||
runtime.destroy();
|
||||
|
||||
expect(destroy).toHaveBeenCalledOnce();
|
||||
expect(mutationSignal?.aborted).toBe(true);
|
||||
expect(runtime.store.getState().resumeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("coalesces rapid source edits and bounds stylesheet history", () => {
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 1_000_000,
|
||||
compile: vi.fn(),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn(),
|
||||
});
|
||||
|
||||
for (let index = 0; index < 10; index++) runtime.store.getState().setSourceText(`rapid ${index}`);
|
||||
expect(runtime.store.getState().undoStack).toHaveLength(1);
|
||||
expect(runtime.store.getState().undoStack[0]).toEqual(stylesheet("generation zero"));
|
||||
|
||||
for (let index = 0; index < 60; index++) {
|
||||
vi.advanceTimersByTime(501);
|
||||
runtime.store.getState().setSourceText(`separate ${index}`);
|
||||
}
|
||||
expect(runtime.store.getState().undoStack).toHaveLength(50);
|
||||
});
|
||||
});
|
||||
@@ -1,699 +0,0 @@
|
||||
import type { SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { StoreApi } from "zustand/vanilla";
|
||||
import type { SemanticCssColorToken } from "./color-tokens";
|
||||
import type {
|
||||
CompileWorkerInput,
|
||||
CompileWorkerResponse,
|
||||
PreflightWorkerInput,
|
||||
PreflightWorkerResponse,
|
||||
SemanticCssEditorMetadata,
|
||||
} from "./protocol";
|
||||
import { create } from "zustand/react";
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import {
|
||||
buildSemanticTree,
|
||||
getTemplateSemanticManifest,
|
||||
semanticNodeKeys,
|
||||
shouldShowResumeHeader,
|
||||
} from "@reactive-resume/pdf/semantic-tree";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
import { createCompileWorkerClient, createPreflightWorkerClient } from "./worker-client";
|
||||
|
||||
export type StylesheetCanonicalState = {
|
||||
stylesheet: SemanticStylesheet;
|
||||
revision: number;
|
||||
renderDataVersion: number;
|
||||
};
|
||||
|
||||
type StylesheetMutationResult = StylesheetCanonicalState & {
|
||||
editGeneration: number;
|
||||
diagnostics: readonly SemanticCssDiagnostic[];
|
||||
};
|
||||
|
||||
type EditMutation = {
|
||||
id: string;
|
||||
expectedRevision: number;
|
||||
expectedRenderDataVersion: number;
|
||||
editGeneration: number;
|
||||
transition: "edit_source";
|
||||
source: StylesheetSource;
|
||||
};
|
||||
|
||||
type RestoreMutation = {
|
||||
id: string;
|
||||
expectedRevision: number;
|
||||
expectedRenderDataVersion: number;
|
||||
editGeneration: number;
|
||||
transition: "restore_history";
|
||||
restore: SemanticStylesheet;
|
||||
};
|
||||
|
||||
type ActivateMutation = Omit<EditMutation, "transition"> & { transition: "activate" };
|
||||
type DeactivateMutation = Omit<EditMutation, "transition" | "source"> & { transition: "deactivate" };
|
||||
type StylesheetMutation = EditMutation | RestoreMutation | ActivateMutation | DeactivateMutation;
|
||||
|
||||
type Candidate =
|
||||
| { generation: number; transition: "edit_source"; source: StylesheetSource }
|
||||
| { generation: number; transition: "restore_history"; restore: SemanticStylesheet }
|
||||
| { generation: number; transition: "activate"; source: StylesheetSource }
|
||||
| { generation: number; transition: "deactivate" };
|
||||
|
||||
export type StylesheetStoreState = {
|
||||
resumeId?: string;
|
||||
mode: SemanticStylesheet["mode"];
|
||||
source: StylesheetSource;
|
||||
applied: StylesheetSource;
|
||||
revision: number;
|
||||
renderDataVersion: number;
|
||||
editGeneration: number;
|
||||
diagnostics: readonly SemanticCssDiagnostic[];
|
||||
colorTokens: readonly SemanticCssColorToken[];
|
||||
editorMetadata: SemanticCssEditorMetadata;
|
||||
status: "idle" | "compiling" | "preflighting" | "saving" | "applied" | "error";
|
||||
restoreLocked: boolean;
|
||||
focused: boolean;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
undoStack: SemanticStylesheet[];
|
||||
redoStack: SemanticStylesheet[];
|
||||
setSourceText(text: string): void;
|
||||
setFocused(focused: boolean): void;
|
||||
activate(): void;
|
||||
deactivate(): void;
|
||||
undo(): void;
|
||||
redo(): void;
|
||||
refreshIntelligence(): void;
|
||||
};
|
||||
|
||||
type RuntimeDependencies = {
|
||||
compile(input: CompileWorkerInput): Promise<CompileWorkerResponse>;
|
||||
preflight(input: PreflightWorkerInput): Promise<PreflightWorkerResponse>;
|
||||
mutate(input: StylesheetMutation, signal: AbortSignal): Promise<StylesheetMutationResult>;
|
||||
destroy?(): void;
|
||||
};
|
||||
|
||||
type CreateStylesheetStoreRuntimeOptions = RuntimeDependencies & {
|
||||
resumeId: string;
|
||||
initial: StylesheetCanonicalState;
|
||||
resumeData: ResumeData;
|
||||
debounceMs?: number;
|
||||
store?: StoreApi<StylesheetStoreState>;
|
||||
};
|
||||
|
||||
const emptySource = (): StylesheetSource => ({ languageVersion: 1, text: "@version 1;\n" });
|
||||
const emptySemanticTree = (): SemanticNode => ({
|
||||
key: "resume",
|
||||
kind: "resume",
|
||||
attributes: {},
|
||||
roles: [],
|
||||
children: [],
|
||||
});
|
||||
const HISTORY_COALESCE_MS = 500;
|
||||
const MAX_HISTORY_ENTRIES = 50;
|
||||
|
||||
const preflightFailureDiagnostic = (
|
||||
result: Extract<PreflightWorkerResponse["result"], { ok: false }>,
|
||||
): SemanticCssDiagnostic => ({
|
||||
code: result.code,
|
||||
severity: "error",
|
||||
message: result.message,
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 1, offset: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
const inactiveState = (): Omit<
|
||||
StylesheetStoreState,
|
||||
"setSourceText" | "setFocused" | "activate" | "deactivate" | "undo" | "redo" | "refreshIntelligence"
|
||||
> => ({
|
||||
resumeId: undefined,
|
||||
mode: "legacy",
|
||||
source: emptySource(),
|
||||
applied: emptySource(),
|
||||
revision: 0,
|
||||
renderDataVersion: 0,
|
||||
editGeneration: 0,
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
editorMetadata: { semanticTree: emptySemanticTree(), templateParts: [] },
|
||||
status: "idle",
|
||||
restoreLocked: false,
|
||||
focused: false,
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
});
|
||||
|
||||
const sourceFromText = (source: StylesheetSource, text: string): StylesheetSource => ({ ...source, text });
|
||||
const sourcesEqual = (left: StylesheetSource, right: StylesheetSource) =>
|
||||
left.languageVersion === right.languageVersion && left.text === right.text;
|
||||
const isEditorFocused = () =>
|
||||
typeof document !== "undefined" && document.activeElement instanceof HTMLElement
|
||||
? document.activeElement.closest(".cm-editor") !== null
|
||||
: false;
|
||||
const currentStylesheet = (state: StylesheetStoreState): SemanticStylesheet => ({
|
||||
mode: state.mode,
|
||||
source: structuredClone(state.source),
|
||||
applied: structuredClone(state.applied),
|
||||
});
|
||||
const appendHistory = (stack: SemanticStylesheet[], value: SemanticStylesheet) =>
|
||||
[...stack, value].slice(-MAX_HISTORY_ENTRIES);
|
||||
|
||||
const pageDimensions = (data: ResumeData) => {
|
||||
const format = data.metadata.page.format;
|
||||
const size = format === "letter" ? { width: 612, height: 792 } : { width: 595.28, height: 841.89 };
|
||||
return data.metadata.layout.pages.map((_page, index) => ({
|
||||
pageKey: semanticNodeKeys.page(index + 1),
|
||||
...size,
|
||||
}));
|
||||
};
|
||||
|
||||
const createEditorMetadata = (data: ResumeData): SemanticCssEditorMetadata => {
|
||||
const pages = data.metadata.layout.pages.map((page, index) =>
|
||||
buildSemanticTree({
|
||||
data,
|
||||
template: data.metadata.template,
|
||||
page,
|
||||
pageNumber: index + 1,
|
||||
showHeader: shouldShowResumeHeader(data, index),
|
||||
}),
|
||||
);
|
||||
const semanticTree: SemanticNode = {
|
||||
key: semanticNodeKeys.resume(),
|
||||
kind: "resume",
|
||||
attributes: { template: data.metadata.template },
|
||||
roles: [],
|
||||
children: pages.flatMap(({ children }) => children),
|
||||
};
|
||||
return {
|
||||
semanticTree,
|
||||
templateParts: getTemplateSemanticManifest(data.metadata.template).parts.map(({ name }) => name),
|
||||
};
|
||||
};
|
||||
|
||||
const compileInput = (
|
||||
data: ResumeData,
|
||||
source: StylesheetSource,
|
||||
editGeneration: number,
|
||||
semanticTree: SemanticNode,
|
||||
): CompileWorkerInput => {
|
||||
return {
|
||||
editGeneration,
|
||||
source,
|
||||
semanticTree,
|
||||
baseSettings: {
|
||||
picture: data.picture,
|
||||
template: data.metadata.template,
|
||||
design: data.metadata.design,
|
||||
typography: data.metadata.typography,
|
||||
page: data.metadata.page,
|
||||
layout: { sidebarWidth: data.metadata.layout.sidebarWidth },
|
||||
},
|
||||
pages: pageDimensions(data),
|
||||
};
|
||||
};
|
||||
|
||||
const conflictState = (error: unknown): StylesheetCanonicalState | undefined => {
|
||||
if (!error || typeof error !== "object") return;
|
||||
const value = error as { code?: string; data?: { state?: StylesheetCanonicalState } };
|
||||
return value.code === "STYLESHEET_REVISION_CONFLICT" ? value.data?.state : undefined;
|
||||
};
|
||||
|
||||
export function createStylesheetStoreRuntime(options: CreateStylesheetStoreRuntimeOptions) {
|
||||
let resumeData = structuredClone(options.resumeData);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let inFlight: Candidate | undefined;
|
||||
let pending: Candidate | undefined;
|
||||
let latestCandidate: Candidate | undefined;
|
||||
let deferredCanonical: StylesheetCanonicalState | undefined;
|
||||
let validationEpoch = 0;
|
||||
let intelligenceEpoch = 0;
|
||||
let historyLastEditAt = 0;
|
||||
let historyCanCoalesce = false;
|
||||
let destroyed = false;
|
||||
const abortController = new AbortController();
|
||||
const debounceMs = options.debounceMs ?? 180;
|
||||
const initial = options.initial.stylesheet;
|
||||
let editorMetadata = createEditorMetadata(resumeData);
|
||||
const store =
|
||||
options.store ??
|
||||
createStore<StylesheetStoreState>(() => ({
|
||||
...inactiveState(),
|
||||
setSourceText: () => {},
|
||||
setFocused: () => {},
|
||||
activate: () => {},
|
||||
deactivate: () => {},
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
refreshIntelligence: () => {},
|
||||
}));
|
||||
|
||||
const patch = (next: Partial<StylesheetStoreState>) => store.setState(next);
|
||||
const replaceCanonical = (canonical: StylesheetCanonicalState, preserveSource: boolean) => {
|
||||
const state = store.getState();
|
||||
const next: Partial<StylesheetStoreState> = {
|
||||
revision: Math.max(state.revision, canonical.revision),
|
||||
renderDataVersion: Math.max(state.renderDataVersion, canonical.renderDataVersion),
|
||||
};
|
||||
if (canonical.revision >= state.revision) {
|
||||
next.mode = canonical.stylesheet.mode;
|
||||
const nextSource = preserveSource ? state.source : canonical.stylesheet.source;
|
||||
next.source = nextSource;
|
||||
next.applied = canonical.stylesheet.applied;
|
||||
if (!sourcesEqual(nextSource, state.source)) {
|
||||
intelligenceEpoch += 1;
|
||||
next.colorTokens = [];
|
||||
}
|
||||
}
|
||||
patch(next);
|
||||
};
|
||||
const resetHistoryCoalescing = () => {
|
||||
historyLastEditAt = 0;
|
||||
historyCanCoalesce = false;
|
||||
};
|
||||
|
||||
const startNext = () => {
|
||||
if (destroyed || inFlight || !pending) return;
|
||||
const candidate = pending;
|
||||
pending = undefined;
|
||||
inFlight = candidate;
|
||||
const requestValidationEpoch = validationEpoch;
|
||||
const state = store.getState();
|
||||
const common = {
|
||||
id: options.resumeId,
|
||||
expectedRevision: state.revision,
|
||||
expectedRenderDataVersion: state.renderDataVersion,
|
||||
editGeneration: candidate.generation,
|
||||
};
|
||||
let input: StylesheetMutation;
|
||||
if (candidate.transition === "edit_source" || candidate.transition === "activate") {
|
||||
input = { ...common, transition: candidate.transition, source: candidate.source };
|
||||
} else if (candidate.transition === "restore_history") {
|
||||
input = { ...common, transition: "restore_history", restore: candidate.restore };
|
||||
} else {
|
||||
input = { ...common, transition: "deactivate" };
|
||||
}
|
||||
patch({ status: "saving" });
|
||||
|
||||
void options
|
||||
.mutate(input, abortController.signal)
|
||||
.then((result) => {
|
||||
if (destroyed) return;
|
||||
const state = store.getState();
|
||||
const staleStylesheet = result.revision < state.revision;
|
||||
patch({
|
||||
revision: Math.max(state.revision, result.revision),
|
||||
renderDataVersion: Math.max(state.renderDataVersion, result.renderDataVersion),
|
||||
});
|
||||
if (result.editGeneration !== store.getState().editGeneration) return;
|
||||
if (staleStylesheet) return;
|
||||
const sourceChanged = !sourcesEqual(result.stylesheet.source, state.source);
|
||||
if (sourceChanged) intelligenceEpoch += 1;
|
||||
patch({
|
||||
mode: result.stylesheet.mode,
|
||||
source: result.stylesheet.source,
|
||||
applied: result.stylesheet.applied,
|
||||
diagnostics: result.diagnostics,
|
||||
colorTokens: sourceChanged ? [] : state.colorTokens,
|
||||
status: result.diagnostics.some(({ severity }) => severity === "error") ? "error" : "applied",
|
||||
});
|
||||
if (latestCandidate?.generation === result.editGeneration) latestCandidate = undefined;
|
||||
if (deferredCanonical && result.revision >= deferredCanonical.revision) deferredCanonical = undefined;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (destroyed) return;
|
||||
const canonical = conflictState(error);
|
||||
if (!canonical) {
|
||||
patch({ status: "error" });
|
||||
return;
|
||||
}
|
||||
replaceCanonical(canonical, true);
|
||||
if (requestValidationEpoch === validationEpoch) pending ??= candidate;
|
||||
})
|
||||
.finally(() => {
|
||||
inFlight = undefined;
|
||||
startNext();
|
||||
});
|
||||
};
|
||||
|
||||
const queue = (candidate: Candidate) => {
|
||||
latestCandidate = candidate;
|
||||
pending = candidate;
|
||||
startNext();
|
||||
};
|
||||
|
||||
const processCandidate = async (candidate: Candidate) => {
|
||||
if (destroyed || candidate.generation !== store.getState().editGeneration) return;
|
||||
const candidateValidationEpoch = validationEpoch;
|
||||
if (candidate.transition === "deactivate") {
|
||||
queue(candidate);
|
||||
return;
|
||||
}
|
||||
const source = candidate.transition === "restore_history" ? candidate.restore.applied : candidate.source;
|
||||
patch({ status: "compiling" });
|
||||
let compiled: CompileWorkerResponse;
|
||||
try {
|
||||
compiled = await options.compile(
|
||||
compileInput(resumeData, source, candidate.generation, editorMetadata.semanticTree),
|
||||
);
|
||||
} catch {
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
if (destroyed || candidate.generation !== store.getState().editGeneration) return;
|
||||
patch({ status: "error" });
|
||||
return;
|
||||
}
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
if (destroyed || compiled.editGeneration !== store.getState().editGeneration) return;
|
||||
patch({ diagnostics: compiled.diagnostics, colorTokens: compiled.colorTokens ?? [] });
|
||||
|
||||
if (!compiled.program) {
|
||||
if (candidate.transition === "edit_source") queue(candidate);
|
||||
else patch({ status: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (compiled.program) {
|
||||
patch({ status: "preflighting" });
|
||||
let preflight: PreflightWorkerResponse;
|
||||
try {
|
||||
preflight = await options.preflight({
|
||||
editGeneration: candidate.generation,
|
||||
input: { data: resumeData, template: resumeData.metadata.template, stylesheet: source },
|
||||
limits: {
|
||||
maxPages: 20,
|
||||
maxBytes: 10_000_000,
|
||||
maxPageWidthPt: 2_000,
|
||||
maxPageHeightPt: 20_000,
|
||||
maxPageAreaPt2: 20_000_000,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
if (candidate.generation !== store.getState().editGeneration) return;
|
||||
patch({ status: "error" });
|
||||
if (candidate.transition === "edit_source") queue(candidate);
|
||||
return;
|
||||
}
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
if (destroyed || preflight.editGeneration !== store.getState().editGeneration) return;
|
||||
if (!preflight.result.ok) {
|
||||
patch({
|
||||
diagnostics: [
|
||||
...compiled.diagnostics,
|
||||
...preflight.result.diagnostics,
|
||||
preflightFailureDiagnostic(preflight.result),
|
||||
],
|
||||
status: "error",
|
||||
});
|
||||
if (candidate.transition !== "edit_source") return;
|
||||
}
|
||||
}
|
||||
|
||||
queue(candidate);
|
||||
};
|
||||
|
||||
const schedule = (candidate: Candidate) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
latestCandidate = candidate;
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
void processCandidate(candidate);
|
||||
}, debounceMs);
|
||||
};
|
||||
|
||||
const restore = (target: SemanticStylesheet, opposite: "undoStack" | "redoStack") => {
|
||||
const state = store.getState();
|
||||
const stack = opposite === "undoStack" ? state.undoStack : state.redoStack;
|
||||
const previous = stack.at(-1);
|
||||
if (!previous) return;
|
||||
const generation = state.editGeneration + 1;
|
||||
const other = opposite === "undoStack" ? "redoStack" : "undoStack";
|
||||
resetHistoryCoalescing();
|
||||
patch({
|
||||
source: previous.source,
|
||||
editGeneration: generation,
|
||||
colorTokens: [],
|
||||
[opposite]: stack.slice(0, -1),
|
||||
[other]: appendHistory(state[other], target),
|
||||
canUndo: opposite === "redoStack" || stack.length > 1,
|
||||
canRedo: opposite === "undoStack" || stack.length > 1,
|
||||
});
|
||||
schedule({ generation, transition: "restore_history", restore: previous });
|
||||
};
|
||||
|
||||
store.setState({
|
||||
resumeId: options.resumeId,
|
||||
mode: initial.mode,
|
||||
source: structuredClone(initial.source),
|
||||
applied: structuredClone(initial.applied),
|
||||
revision: options.initial.revision,
|
||||
renderDataVersion: options.initial.renderDataVersion,
|
||||
editGeneration: 0,
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
editorMetadata,
|
||||
status: "idle",
|
||||
restoreLocked: false,
|
||||
focused: false,
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
setSourceText(text) {
|
||||
const state = store.getState();
|
||||
if (state.restoreLocked || text === state.source.text) return;
|
||||
const generation = state.editGeneration + 1;
|
||||
const nextSource = sourceFromText(state.source, text);
|
||||
const now = Date.now();
|
||||
const undoStack =
|
||||
historyCanCoalesce && now - historyLastEditAt <= HISTORY_COALESCE_MS
|
||||
? state.undoStack
|
||||
: appendHistory(state.undoStack, currentStylesheet(state));
|
||||
historyLastEditAt = now;
|
||||
historyCanCoalesce = true;
|
||||
patch({
|
||||
source: nextSource,
|
||||
editGeneration: generation,
|
||||
colorTokens: [],
|
||||
undoStack,
|
||||
redoStack: [],
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
});
|
||||
schedule({ generation, transition: "edit_source", source: nextSource });
|
||||
},
|
||||
setFocused(focused) {
|
||||
patch({ focused });
|
||||
if (focused || !deferredCanonical) return;
|
||||
const canonical = deferredCanonical;
|
||||
deferredCanonical = undefined;
|
||||
const candidate = latestCandidate;
|
||||
const hasLocalDraft =
|
||||
candidate !== undefined && store.getState().source.text !== canonical.stylesheet.source.text;
|
||||
replaceCanonical(canonical, hasLocalDraft);
|
||||
if (hasLocalDraft && candidate) schedule(candidate);
|
||||
else resetHistoryCoalescing();
|
||||
},
|
||||
activate() {
|
||||
const state = store.getState();
|
||||
if (state.restoreLocked || state.mode === "semantic") return;
|
||||
const generation = state.editGeneration + 1;
|
||||
resetHistoryCoalescing();
|
||||
patch({
|
||||
editGeneration: generation,
|
||||
colorTokens: [],
|
||||
undoStack: appendHistory(state.undoStack, currentStylesheet(state)),
|
||||
redoStack: [],
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
});
|
||||
schedule({ generation, transition: "activate", source: state.source });
|
||||
},
|
||||
deactivate() {
|
||||
const state = store.getState();
|
||||
if (state.restoreLocked || state.mode === "legacy") return;
|
||||
const generation = state.editGeneration + 1;
|
||||
resetHistoryCoalescing();
|
||||
patch({
|
||||
editGeneration: generation,
|
||||
colorTokens: [],
|
||||
undoStack: appendHistory(state.undoStack, currentStylesheet(state)),
|
||||
redoStack: [],
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
});
|
||||
queue({ generation, transition: "deactivate" });
|
||||
},
|
||||
undo() {
|
||||
if (store.getState().restoreLocked) return;
|
||||
restore(currentStylesheet(store.getState()), "undoStack");
|
||||
},
|
||||
redo() {
|
||||
if (store.getState().restoreLocked) return;
|
||||
restore(currentStylesheet(store.getState()), "redoStack");
|
||||
},
|
||||
refreshIntelligence() {
|
||||
const state = store.getState();
|
||||
const generation = state.editGeneration;
|
||||
const source = structuredClone(state.source);
|
||||
const requestEpoch = ++intelligenceEpoch;
|
||||
void options
|
||||
.compile(compileInput(resumeData, source, generation, editorMetadata.semanticTree))
|
||||
.then((compiled) => {
|
||||
const current = store.getState();
|
||||
if (
|
||||
destroyed ||
|
||||
requestEpoch !== intelligenceEpoch ||
|
||||
current.editGeneration !== generation ||
|
||||
!sourcesEqual(current.source, source)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
patch({ diagnostics: compiled.diagnostics, colorTokens: compiled.colorTokens ?? [] });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
replaceResumeSnapshot(data: ResumeData, canonical: StylesheetCanonicalState) {
|
||||
const candidate = latestCandidate;
|
||||
resumeData = structuredClone(data);
|
||||
editorMetadata = createEditorMetadata(resumeData);
|
||||
patch({ editorMetadata });
|
||||
const renderDataChanged = canonical.renderDataVersion > store.getState().renderDataVersion;
|
||||
const preserveSource = store.getState().focused || isEditorFocused() || candidate !== undefined;
|
||||
replaceCanonical(canonical, preserveSource);
|
||||
if (renderDataChanged) {
|
||||
validationEpoch += 1;
|
||||
pending = undefined;
|
||||
if (candidate) schedule(candidate);
|
||||
}
|
||||
},
|
||||
rebaseCanonical(canonical: StylesheetCanonicalState) {
|
||||
const candidate = latestCandidate;
|
||||
const hasLocalDraft =
|
||||
candidate !== undefined && store.getState().source.text !== canonical.stylesheet.source.text;
|
||||
const focused = store.getState().focused || isEditorFocused();
|
||||
const sourceChanged = store.getState().source.text !== canonical.stylesheet.source.text;
|
||||
if (focused && sourceChanged && canonical.revision >= store.getState().revision) deferredCanonical = canonical;
|
||||
const preserveSource = (focused && sourceChanged) || hasLocalDraft;
|
||||
replaceCanonical(canonical, preserveSource);
|
||||
if (hasLocalDraft && candidate) schedule(candidate);
|
||||
else if (!preserveSource) resetHistoryCoalescing();
|
||||
},
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
abortController.abort();
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = undefined;
|
||||
pending = undefined;
|
||||
latestCandidate = undefined;
|
||||
deferredCanonical = undefined;
|
||||
options.destroy?.();
|
||||
store.setState(inactiveState());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const useStylesheetStore = create<StylesheetStoreState>(() => ({
|
||||
...inactiveState(),
|
||||
setSourceText: () => {},
|
||||
setFocused: () => {},
|
||||
activate: () => {},
|
||||
deactivate: () => {},
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
refreshIntelligence: () => {},
|
||||
}));
|
||||
|
||||
let activeRuntime: ReturnType<typeof createStylesheetStoreRuntime> | undefined;
|
||||
declare const stylesheetRuntimeTokenBrand: unique symbol;
|
||||
export type StylesheetRuntimeToken = Readonly<{ [stylesheetRuntimeTokenBrand]: true }>;
|
||||
let activeRuntimeToken: StylesheetRuntimeToken | undefined;
|
||||
|
||||
const compilerClient = () =>
|
||||
createCompileWorkerClient(
|
||||
() =>
|
||||
new Worker(new URL("./stylesheet.worker.ts", import.meta.url), { type: "module", name: "semantic-css-compiler" }),
|
||||
);
|
||||
const preflightClient = () =>
|
||||
createPreflightWorkerClient(
|
||||
() =>
|
||||
new Worker(new URL("./preflight.worker.ts", import.meta.url), { type: "module", name: "semantic-css-preflight" }),
|
||||
5_000,
|
||||
);
|
||||
|
||||
export function initializeStylesheetStore(input: {
|
||||
resumeId: string;
|
||||
initial: StylesheetCanonicalState;
|
||||
resumeData: ResumeData;
|
||||
}) {
|
||||
activeRuntime?.destroy();
|
||||
const compiler = compilerClient();
|
||||
const preflight = preflightClient();
|
||||
preflight.warmup();
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
...input,
|
||||
store: useStylesheetStore,
|
||||
compile: compiler.compile,
|
||||
preflight: preflight.preflight,
|
||||
mutate: (mutation, signal) => orpc.resume.stylesheet.mutate.call(mutation, { signal }),
|
||||
destroy: () => {
|
||||
compiler.destroy();
|
||||
preflight.destroy();
|
||||
},
|
||||
});
|
||||
activeRuntime = runtime;
|
||||
activeRuntimeToken = {} as StylesheetRuntimeToken;
|
||||
return () => {
|
||||
if (activeRuntime?.store.getState().resumeId !== input.resumeId) return;
|
||||
activeRuntime.destroy();
|
||||
activeRuntime = undefined;
|
||||
activeRuntimeToken = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function lockStylesheetStoreForRestore(resumeId: string): StylesheetRuntimeToken | undefined {
|
||||
if (!activeRuntime || !activeRuntimeToken) return;
|
||||
const state = activeRuntime.store.getState();
|
||||
if (state.resumeId !== resumeId || state.restoreLocked) return;
|
||||
activeRuntime.store.setState({ restoreLocked: true });
|
||||
return activeRuntimeToken;
|
||||
}
|
||||
|
||||
export function unlockStylesheetStoreAfterRestore(token: StylesheetRuntimeToken | undefined): boolean {
|
||||
if (!activeRuntime || !token || activeRuntimeToken !== token) return false;
|
||||
activeRuntime.store.setState({ restoreLocked: false });
|
||||
return true;
|
||||
}
|
||||
|
||||
export function replaceStylesheetStoreAfterRestore(input: {
|
||||
resumeId: string;
|
||||
initial: StylesheetCanonicalState;
|
||||
resumeData: ResumeData;
|
||||
token: StylesheetRuntimeToken | undefined;
|
||||
}): boolean {
|
||||
if (
|
||||
!activeRuntime ||
|
||||
!input.token ||
|
||||
activeRuntimeToken !== input.token ||
|
||||
activeRuntime.store.getState().resumeId !== input.resumeId ||
|
||||
!activeRuntime.store.getState().restoreLocked
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
initializeStylesheetStore(input);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function refreshStylesheetStore(resumeId: string, resumeData?: ResumeData) {
|
||||
if (!activeRuntime || activeRuntime.store.getState().resumeId !== resumeId) return;
|
||||
const canonical = await orpc.resume.stylesheet.getState.call({ id: resumeId });
|
||||
if (resumeData) activeRuntime.replaceResumeSnapshot(resumeData, canonical);
|
||||
else activeRuntime.rebaseCanonical(canonical);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { CompileWorkerRequest, CompileWorkerResponse } from "./protocol";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
|
||||
function request(source: string): CompileWorkerRequest {
|
||||
return {
|
||||
type: "compile",
|
||||
requestId: 1,
|
||||
editGeneration: 1,
|
||||
source: { languageVersion: 1, text: source },
|
||||
semanticTree: {
|
||||
key: "resume",
|
||||
kind: "resume",
|
||||
attributes: {},
|
||||
roles: [],
|
||||
children: [{ key: "name", kind: "name", attributes: {}, roles: [], children: [] }],
|
||||
},
|
||||
baseSettings: {
|
||||
picture: defaultResumeData.picture,
|
||||
template: defaultResumeData.metadata.template,
|
||||
design: defaultResumeData.metadata.design,
|
||||
typography: defaultResumeData.metadata.typography,
|
||||
page: defaultResumeData.metadata.page,
|
||||
layout: { sidebarWidth: defaultResumeData.metadata.layout.sidebarWidth },
|
||||
},
|
||||
pages: [{ pageKey: "page-1", width: 595.28, height: 841.89 }],
|
||||
};
|
||||
}
|
||||
|
||||
describe("stylesheet worker", () => {
|
||||
let handleMessage: ((event: MessageEvent<CompileWorkerRequest>) => void) | undefined;
|
||||
const postMessage = vi.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
handleMessage = undefined;
|
||||
postMessage.mockReset();
|
||||
vi.resetModules();
|
||||
vi.stubGlobal("self", {
|
||||
addEventListener: vi.fn((type: string, listener: (event: MessageEvent<CompileWorkerRequest>) => void) => {
|
||||
if (type === "message") handleMessage = listener;
|
||||
}),
|
||||
postMessage,
|
||||
});
|
||||
await import("./stylesheet.worker");
|
||||
});
|
||||
|
||||
it("returns diagnostics from variable resolution", () => {
|
||||
handleMessage?.(
|
||||
new MessageEvent("message", {
|
||||
data: request("@version 1;\nname { color: var(--missing); }\n"),
|
||||
}),
|
||||
);
|
||||
|
||||
const response = postMessage.mock.calls[0]?.[0] as CompileWorkerResponse | undefined;
|
||||
expect(response?.diagnostics).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ code: "UNRESOLVED_VARIABLE", severity: "error" })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns overlapping semantic diagnostics once", () => {
|
||||
handleMessage?.(
|
||||
new MessageEvent("message", {
|
||||
data: request('@version 1;\nsection[type="education"] { color: red; }\n'),
|
||||
}),
|
||||
);
|
||||
|
||||
const response = postMessage.mock.calls[0]?.[0] as CompileWorkerResponse | undefined;
|
||||
expect(response?.diagnostics.filter(({ code }) => code === "SELECTOR_NO_MATCH")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,21 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import type { CompileWorkerRequest, CompileWorkerResponse } from "./protocol";
|
||||
import { analyzeStylesheet, compileStylesheet } from "@reactive-resume/resume/stylesheet";
|
||||
import { compileStylesheet, resolveStylesheet } from "@reactive-resume/resume/stylesheet";
|
||||
import { collectCompiledColorTokens } from "./color-tokens";
|
||||
|
||||
self.addEventListener("message", ({ data }: MessageEvent<CompileWorkerRequest>) => {
|
||||
if (data.type !== "compile") return;
|
||||
const compiled = compileStylesheet(data.source);
|
||||
const diagnostics = compiled.program
|
||||
? [...compiled.diagnostics, ...analyzeStylesheet(compiled.program, data.semanticTree)]
|
||||
? [
|
||||
...compiled.diagnostics,
|
||||
...resolveStylesheet(compiled.program, data.semanticTree, {
|
||||
baseStyles: {},
|
||||
baseSettings: data.baseSettings,
|
||||
pages: data.pages,
|
||||
}).diagnostics,
|
||||
]
|
||||
: compiled.diagnostics;
|
||||
const response: CompileWorkerResponse = {
|
||||
type: "compile_result",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import {
|
||||
ArrowCounterClockwiseIcon,
|
||||
ArrowsInIcon,
|
||||
ArrowsOutIcon,
|
||||
ArrowUUpLeftIcon,
|
||||
@@ -43,7 +42,6 @@ export type StylesheetToolbarProps = {
|
||||
onUndo(): void;
|
||||
onRedo(): void;
|
||||
onFormat(): void;
|
||||
onReset(): void;
|
||||
onFocusToggle(): void;
|
||||
};
|
||||
|
||||
@@ -56,7 +54,6 @@ export function StylesheetToolbar({
|
||||
onUndo,
|
||||
onRedo,
|
||||
onFormat,
|
||||
onReset,
|
||||
onFocusToggle,
|
||||
}: StylesheetToolbarProps) {
|
||||
return (
|
||||
@@ -73,9 +70,6 @@ export function StylesheetToolbar({
|
||||
<ToolbarButton label={t`Format stylesheet`} disabled={disabled} onClick={onFormat}>
|
||||
<MagicWandIcon data-icon="inline-start" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton label={t`Reset to applied stylesheet`} disabled={disabled} onClick={onReset}>
|
||||
<ArrowCounterClockwiseIcon data-icon="inline-start" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton label={focused ? t`Exit focus mode` : t`Open focus mode`} onClick={onFocusToggle}>
|
||||
{focused ? <ArrowsInIcon data-icon="inline-start" /> : <ArrowsOutIcon data-icon="inline-start" />}
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { getPreflightTransferables } from "./protocol";
|
||||
import { createCompileWorkerClient, createPreflightWorkerClient } from "./worker-client";
|
||||
import { createCompileWorkerClient } from "./worker-client";
|
||||
|
||||
type Listener = (event: MessageEvent) => void;
|
||||
|
||||
@@ -23,15 +22,13 @@ function worker() {
|
||||
}
|
||||
},
|
||||
emitError(event: ErrorEvent) {
|
||||
for (const listener of listeners.get("error") ?? []) {
|
||||
listener(event);
|
||||
}
|
||||
for (const listener of listeners.get("error") ?? []) listener(event);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("stylesheet worker clients", () => {
|
||||
it("resolves older compiler results so callers can generation-check without aborting", async () => {
|
||||
describe("stylesheet compiler worker client", () => {
|
||||
it("resolves every compiler result so the editor can generation-check", async () => {
|
||||
const fake = worker();
|
||||
const client = createCompileWorkerClient(() => fake);
|
||||
const first = client.compile({ editGeneration: 1 } as never);
|
||||
@@ -54,181 +51,14 @@ describe("stylesheet worker clients", () => {
|
||||
await expect(pending).rejects.toThrow("Failed to load compiler worker");
|
||||
});
|
||||
|
||||
it("terminates and recreates a timed-out preflight worker", async () => {
|
||||
vi.useFakeTimers();
|
||||
const first = worker();
|
||||
const replacement = worker();
|
||||
const createWorker = vi.fn().mockReturnValueOnce(first).mockReturnValueOnce(replacement);
|
||||
const client = createPreflightWorkerClient(createWorker, 10);
|
||||
|
||||
const timedOut = client.preflight({ editGeneration: 1 } as never);
|
||||
first.emit({ type: "preflight_ready" });
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
await expect(timedOut).resolves.toMatchObject({
|
||||
result: { ok: false, code: "STYLESHEET_PREFLIGHT_TIMEOUT" },
|
||||
});
|
||||
expect(first.terminate).toHaveBeenCalledOnce();
|
||||
|
||||
const next = client.preflight({ editGeneration: 2 } as never);
|
||||
replacement.emit({ type: "preflight_ready" });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
replacement.emit({
|
||||
type: "preflight_result",
|
||||
requestId: 2,
|
||||
editGeneration: 2,
|
||||
result: { ok: true, pageCount: 1, byteCount: 4, diagnostics: [], pdf: new ArrayBuffer(4) },
|
||||
});
|
||||
await expect(next).resolves.toMatchObject({ requestId: 2 });
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("warms the preflight worker before a request starts its deadline", () => {
|
||||
it("terminates the compiler worker and rejects pending work on destroy", async () => {
|
||||
const fake = worker();
|
||||
const createWorker = vi.fn(() => fake);
|
||||
const client = createPreflightWorkerClient(createWorker, 5_000);
|
||||
|
||||
client.warmup();
|
||||
|
||||
expect(createWorker).toHaveBeenCalledOnce();
|
||||
expect(fake.addEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(fake.addEventListener).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
expect(fake.addEventListener).toHaveBeenCalledWith("error", expect.any(Function));
|
||||
expect(fake.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("waits for readiness without consuming the request deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fake = worker();
|
||||
const client = createPreflightWorkerClient(() => fake, 5, 20);
|
||||
const result = client.preflight({ editGeneration: 1 } as never);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5);
|
||||
expect(fake.terminate).not.toHaveBeenCalled();
|
||||
expect(fake.postMessage).not.toHaveBeenCalled();
|
||||
|
||||
fake.emit({ type: "preflight_ready" });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(fake.postMessage).toHaveBeenCalledOnce();
|
||||
await vi.advanceTimersByTimeAsync(5);
|
||||
await expect(result).resolves.toMatchObject({ result: { code: "STYLESHEET_PREFLIGHT_TIMEOUT" } });
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("rejects pending preflight work when the worker emits an error event", async () => {
|
||||
const fake = worker();
|
||||
const client = createPreflightWorkerClient(() => fake, 5_000);
|
||||
const pending = client.preflight({ editGeneration: 1 } as never);
|
||||
const outcome = pending.catch((error: unknown) => error);
|
||||
|
||||
fake.emit({ type: "preflight_ready" });
|
||||
await Promise.resolve();
|
||||
fake.emitError({ message: "Worker crashed" } as ErrorEvent);
|
||||
|
||||
expect(await outcome).toMatchObject({ message: "Worker crashed" });
|
||||
expect(fake.terminate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects structured resume-data failures without waiting for the timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fake = worker();
|
||||
const client = createPreflightWorkerClient(() => fake, 5_000);
|
||||
const pending = client.preflight({ editGeneration: 1 } as never);
|
||||
const outcome = pending.catch((error: unknown) => error);
|
||||
|
||||
fake.emit({ type: "preflight_ready" });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
fake.emit({
|
||||
type: "preflight_error",
|
||||
requestId: 1,
|
||||
editGeneration: 1,
|
||||
cause: {
|
||||
name: "ZodError",
|
||||
message: "Invalid resume data",
|
||||
issues: [{ path: ["customSections", 0, "items", 0, "company"] }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(await outcome).toMatchObject({
|
||||
name: "ZodError",
|
||||
issues: [{ path: ["customSections", 0, "items", 0, "company"] }],
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
expect(fake.terminate).not.toHaveBeenCalled();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("bounds readiness, recreates once, and rejects after the retry also times out", async () => {
|
||||
vi.useFakeTimers();
|
||||
const first = worker();
|
||||
const replacement = worker();
|
||||
const client = createPreflightWorkerClient(
|
||||
vi.fn().mockReturnValueOnce(first).mockReturnValueOnce(replacement),
|
||||
5,
|
||||
10,
|
||||
);
|
||||
const result = client.preflight({ editGeneration: 1 } as never);
|
||||
const rejection = expect(result).rejects.toThrow("did not become ready");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
expect(first.terminate).toHaveBeenCalledOnce();
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
await rejection;
|
||||
expect(replacement.terminate).toHaveBeenCalledOnce();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("does not recreate a warming worker after destroy", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fake = worker();
|
||||
const createWorker = vi.fn(() => fake);
|
||||
const client = createPreflightWorkerClient(createWorker, 5, 10);
|
||||
client.warmup();
|
||||
const client = createCompileWorkerClient(() => fake);
|
||||
const pending = client.compile({ editGeneration: 1 } as never);
|
||||
|
||||
client.destroy();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(createWorker).toHaveBeenCalledOnce();
|
||||
expect(fake.terminate).toHaveBeenCalledOnce();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("terminates stale preflight work when a newer request starts", async () => {
|
||||
const first = worker();
|
||||
const replacement = worker();
|
||||
const client = createPreflightWorkerClient(
|
||||
vi.fn().mockReturnValueOnce(first).mockReturnValueOnce(replacement),
|
||||
1_000,
|
||||
);
|
||||
const stale = client.preflight({ editGeneration: 1 } as never);
|
||||
first.emit({ type: "preflight_ready" });
|
||||
await Promise.resolve();
|
||||
const staleOutcome = stale.catch((error: unknown) => error);
|
||||
const current = client.preflight({ editGeneration: 2 } as never);
|
||||
|
||||
expect(first.terminate).toHaveBeenCalledOnce();
|
||||
replacement.emit({ type: "preflight_ready" });
|
||||
await Promise.resolve();
|
||||
replacement.emit({
|
||||
type: "preflight_result",
|
||||
requestId: 2,
|
||||
editGeneration: 2,
|
||||
result: { ok: true, pageCount: 1, byteCount: 4, diagnostics: [], pdf: new ArrayBuffer(4) },
|
||||
});
|
||||
expect(await staleOutcome).toEqual(expect.objectContaining({ message: expect.stringContaining("stale") }));
|
||||
await expect(current).resolves.toMatchObject({ requestId: 2 });
|
||||
});
|
||||
|
||||
it("transfers the generated PDF buffer", () => {
|
||||
const pdf = new ArrayBuffer(4);
|
||||
expect(
|
||||
getPreflightTransferables({
|
||||
type: "preflight_result",
|
||||
requestId: 1,
|
||||
editGeneration: 1,
|
||||
result: { ok: true, pageCount: 1, byteCount: 4, diagnostics: [], pdf },
|
||||
}),
|
||||
).toEqual([pdf]);
|
||||
await expect(pending).rejects.toThrow("terminated");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import type {
|
||||
CompileWorkerInput,
|
||||
CompileWorkerRequest,
|
||||
CompileWorkerResponse,
|
||||
PreflightWorkerError,
|
||||
PreflightWorkerInput,
|
||||
PreflightWorkerReady,
|
||||
PreflightWorkerRequest,
|
||||
PreflightWorkerResponse,
|
||||
} from "./protocol";
|
||||
import type { CompileWorkerInput, CompileWorkerRequest, CompileWorkerResponse } from "./protocol";
|
||||
|
||||
type WorkerListener = (event: MessageEvent<unknown>) => void;
|
||||
type WorkerErrorListener = (event: ErrorEvent) => void;
|
||||
@@ -67,164 +58,3 @@ export function createCompileWorkerClient(createWorker: () => StylesheetWorker)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const timeoutResult = (request: PreflightWorkerRequest): PreflightWorkerResponse => ({
|
||||
type: "preflight_result",
|
||||
requestId: request.requestId,
|
||||
editGeneration: request.editGeneration,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_TIMEOUT",
|
||||
message: "The PDF preflight exceeded its deadline.",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
|
||||
export function createPreflightWorkerClient(
|
||||
createWorker: () => StylesheetWorker,
|
||||
timeoutMs: number,
|
||||
readinessTimeoutMs = 10_000,
|
||||
) {
|
||||
let worker: StylesheetWorker | undefined;
|
||||
let requestId = 0;
|
||||
let ready = false;
|
||||
let destroyed = false;
|
||||
let readiness:
|
||||
| (Pending<StylesheetWorker> & {
|
||||
promise: Promise<StylesheetWorker>;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
})
|
||||
| undefined;
|
||||
const pending = new Map<number, Pending<PreflightWorkerResponse> & { timer?: ReturnType<typeof setTimeout> }>();
|
||||
|
||||
const onMessage: WorkerListener = ({ data }) => {
|
||||
if ((data as PreflightWorkerReady)?.type === "preflight_ready") {
|
||||
if (!worker || !readiness) return;
|
||||
clearTimeout(readiness.timer);
|
||||
ready = true;
|
||||
readiness.resolve(worker);
|
||||
readiness = undefined;
|
||||
return;
|
||||
}
|
||||
const workerError = data as PreflightWorkerError;
|
||||
if (workerError?.type === "preflight_error") {
|
||||
const request = pending.get(workerError.requestId);
|
||||
if (!request) return;
|
||||
if (request.timer) clearTimeout(request.timer);
|
||||
pending.delete(workerError.requestId);
|
||||
request.reject(
|
||||
Object.assign(new Error(workerError.cause.message), {
|
||||
name: workerError.cause.name,
|
||||
issues: workerError.cause.issues,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const response = data as PreflightWorkerResponse;
|
||||
if (response?.type !== "preflight_result") return;
|
||||
const request = pending.get(response.requestId);
|
||||
if (!request) return;
|
||||
if (request.timer) clearTimeout(request.timer);
|
||||
pending.delete(response.requestId);
|
||||
request.resolve(response);
|
||||
};
|
||||
const failPending = (error: Error) => {
|
||||
for (const request of pending.values()) {
|
||||
if (request.timer) clearTimeout(request.timer);
|
||||
request.reject(error);
|
||||
}
|
||||
pending.clear();
|
||||
};
|
||||
|
||||
const onError: WorkerErrorListener = (event) => {
|
||||
const error = new Error(event.message || "Stylesheet preflight worker failed.");
|
||||
terminate();
|
||||
failPending(error);
|
||||
};
|
||||
|
||||
const terminate = () => {
|
||||
if (!worker) return;
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
worker.terminate();
|
||||
worker = undefined;
|
||||
ready = false;
|
||||
if (readiness) {
|
||||
clearTimeout(readiness.timer);
|
||||
readiness.reject(new Error("Stylesheet preflight worker did not become ready."));
|
||||
readiness = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getReadyWorker = () => {
|
||||
if (destroyed) return Promise.reject(new Error("Stylesheet preflight worker was terminated."));
|
||||
if (worker && ready) return Promise.resolve(worker);
|
||||
if (readiness) return readiness.promise;
|
||||
worker = createWorker();
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("error", onError);
|
||||
let resolve!: (value: StylesheetWorker) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<StylesheetWorker>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
const timer = setTimeout(() => terminate(), readinessTimeoutMs);
|
||||
readiness = { promise, resolve, reject, timer };
|
||||
return promise;
|
||||
};
|
||||
|
||||
const waitUntilReady = async () => {
|
||||
try {
|
||||
return await getReadyWorker();
|
||||
} catch {
|
||||
return await getReadyWorker();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
warmup() {
|
||||
void waitUntilReady().catch(() => {});
|
||||
},
|
||||
preflight(input: PreflightWorkerInput): Promise<PreflightWorkerResponse> {
|
||||
if (pending.size > 0) {
|
||||
terminate();
|
||||
for (const stale of pending.values()) {
|
||||
if (stale.timer) clearTimeout(stale.timer);
|
||||
stale.reject(new Error("Discarded stale stylesheet preflight result."));
|
||||
}
|
||||
pending.clear();
|
||||
}
|
||||
const request: PreflightWorkerRequest = { ...input, type: "preflight", requestId: ++requestId };
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(request.requestId, { resolve, reject });
|
||||
void waitUntilReady()
|
||||
.then((readyWorker) => {
|
||||
const current = pending.get(request.requestId);
|
||||
if (!current) return;
|
||||
current.timer = setTimeout(() => {
|
||||
pending.delete(request.requestId);
|
||||
terminate();
|
||||
resolve(timeoutResult(request));
|
||||
}, timeoutMs);
|
||||
readyWorker.postMessage(request);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const current = pending.get(request.requestId);
|
||||
if (!current) return;
|
||||
pending.delete(request.requestId);
|
||||
reject(error instanceof Error ? error : new Error("Stylesheet preflight worker failed to start."));
|
||||
});
|
||||
});
|
||||
},
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
terminate();
|
||||
for (const request of pending.values()) {
|
||||
if (request.timer) clearTimeout(request.timer);
|
||||
request.reject(new Error("Stylesheet preflight worker was terminated."));
|
||||
}
|
||||
pending.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||
import { useResumeStore } from "@/features/resume/builder/draft";
|
||||
import {
|
||||
lockStylesheetStoreForRestore,
|
||||
replaceStylesheetStoreAfterRestore,
|
||||
unlockStylesheetStoreAfterRestore,
|
||||
} from "@/features/resume/stylesheet/store";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
import { getResumeErrorMessage } from "@/libs/error-message";
|
||||
import { formatRelativeTime } from "@/libs/locale";
|
||||
@@ -53,26 +48,13 @@ export function BuilderVersionHistory({ resumeId }: BuilderVersionHistoryProps)
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const token = lockStylesheetStoreForRestore(resumeId);
|
||||
if (!token) return;
|
||||
try {
|
||||
const restored = await restoreVersion({ resumeId, versionId });
|
||||
const applied = replaceStylesheetStoreAfterRestore({
|
||||
resumeId,
|
||||
resumeData: restored.resume.data,
|
||||
initial: restored.stylesheetState,
|
||||
token,
|
||||
});
|
||||
if (!applied) {
|
||||
unlockStylesheetStoreAfterRestore(token);
|
||||
return;
|
||||
}
|
||||
replaceResumeFromServer(restored.resume as Resume);
|
||||
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored.resume);
|
||||
replaceResumeFromServer(restored as Resume);
|
||||
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored);
|
||||
void queryClient.invalidateQueries({ queryKey: orpc.resume.listVersions.queryKey({ input: { resumeId } }) });
|
||||
toast.success(t`Your resume has been restored to the selected version.`);
|
||||
} catch (error) {
|
||||
unlockStylesheetStoreAfterRestore(token);
|
||||
toast.error(getResumeErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
@@ -17,16 +18,8 @@ const resumeMock = vi.hoisted(() => ({
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
data: typeof defaultResumeData;
|
||||
data: ResumeData;
|
||||
},
|
||||
stylesheet: {
|
||||
resumeId: "r1" as string | undefined,
|
||||
mode: "semantic" as "legacy" | "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname {" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
revision: 42,
|
||||
renderDataVersion: 7,
|
||||
},
|
||||
}));
|
||||
|
||||
type SectionBaseProps = {
|
||||
@@ -50,9 +43,6 @@ vi.mock("@/libs/resume/section-title-locale", () => ({
|
||||
vi.mock("@/features/resume/builder/draft", () => ({
|
||||
useResume: () => resumeMock.resume,
|
||||
}));
|
||||
vi.mock("@/features/resume/stylesheet/store", () => ({
|
||||
useStylesheetStore: (selector: (state: typeof resumeMock.stylesheet) => unknown) => selector(resumeMock.stylesheet),
|
||||
}));
|
||||
|
||||
const { ExportSectionBuilder } = await import("./export");
|
||||
|
||||
@@ -61,15 +51,12 @@ beforeAll(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resumeMock.resume = { id: "r1", name: "My Resume", slug: "my-resume", data: defaultResumeData };
|
||||
resumeMock.stylesheet = {
|
||||
resumeId: "r1",
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname {" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
revision: 42,
|
||||
renderDataVersion: 7,
|
||||
};
|
||||
resumeMock.resume = { id: "r1", name: "My Resume", slug: "my-resume", data };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -115,7 +102,7 @@ describe("ExportSectionBuilder", () => {
|
||||
expect(filename).toBe("My Resume.md");
|
||||
});
|
||||
|
||||
it("downloads canonical stylesheet content in JSON without concurrency metadata", async () => {
|
||||
it("downloads the current stylesheet source in JSON", async () => {
|
||||
renderExport();
|
||||
openDialog();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Download JSON" }));
|
||||
@@ -127,13 +114,7 @@ describe("ExportSectionBuilder", () => {
|
||||
expect((blob as Blob).type).toBe("application/json");
|
||||
expect(filename).toBe("My Resume.json");
|
||||
const exported = JSON.parse(await (blob as Blob).text());
|
||||
expect(exported.metadata.stylesheet).toEqual({
|
||||
mode: "semantic",
|
||||
source: resumeMock.stylesheet.source,
|
||||
applied: resumeMock.stylesheet.applied,
|
||||
});
|
||||
expect(JSON.stringify(exported)).not.toContain("revision");
|
||||
expect(JSON.stringify(exported)).not.toContain("renderDataVersion");
|
||||
expect(exported.metadata.stylesheet).toEqual(resumeMock.resume?.data.metadata.stylesheet);
|
||||
});
|
||||
|
||||
it("calls buildDocx and downloads the resulting blob when DOCX is clicked", async () => {
|
||||
@@ -155,12 +136,7 @@ describe("ExportSectionBuilder", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
expect(createResumePdfBlob).toHaveBeenCalledTimes(1);
|
||||
expect(createResumePdfBlob).toHaveBeenCalledWith(defaultResumeData, undefined, undefined, {
|
||||
stylesheet: {
|
||||
mode: "semantic",
|
||||
applied: resumeMock.stylesheet.applied,
|
||||
},
|
||||
});
|
||||
expect(createResumePdfBlob).toHaveBeenCalledWith(resumeMock.resume?.data, undefined, undefined);
|
||||
expect(downloadWithAnchor).toHaveBeenCalledTimes(1);
|
||||
expect(downloadWithAnchor.mock.calls[0]?.[1]).toBe("My Resume.pdf");
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { BuilderLayout } from "./-store/sidebar";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
import { useBuilderResumeUpdateSubscription, useResumeCleanup, useResumeStore } from "@/features/resume/builder/draft";
|
||||
import { initializeStylesheetStore, useStylesheetStore } from "@/features/resume/stylesheet/store";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
import { createNoindexFollowMeta } from "@/libs/seo";
|
||||
import { DesktopBuilderShell } from "./-components/desktop-builder-shell";
|
||||
@@ -21,9 +20,6 @@ export const Route = createFileRoute("/builder/$resumeId")({
|
||||
const [layout, resume] = await Promise.all([
|
||||
getBuilderLayout(),
|
||||
context.queryClient.ensureQueryData(orpc.resume.getById.queryOptions({ input: { id: params.resumeId } })),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.resume.stylesheet.getState.queryOptions({ input: { id: params.resumeId } }),
|
||||
),
|
||||
]);
|
||||
|
||||
return { layout, name: resume.name };
|
||||
@@ -40,17 +36,11 @@ function RouteComponent() {
|
||||
|
||||
const { resumeId } = Route.useParams();
|
||||
const { data: resume } = useSuspenseQuery(orpc.resume.getById.queryOptions({ input: { id: resumeId } }));
|
||||
const { data: stylesheet } = useSuspenseQuery(
|
||||
orpc.resume.stylesheet.getState.queryOptions({ input: { id: resumeId } }),
|
||||
);
|
||||
const initializeResumeStore = useResumeStore((state) => state.initialize);
|
||||
const mergeResumeMetadata = useResumeStore((state) => state.mergeResumeMetadata);
|
||||
const isReady = useResumeStore((state) => state.isReady);
|
||||
const initializedResumeId = useResumeStore((state) => state.resumeId);
|
||||
const isInitialized = isReady && initializedResumeId === resumeId;
|
||||
const isStylesheetInitialized = useStylesheetStore((state) => state.resumeId === resumeId);
|
||||
const stylesheetInitialization = useRef({ resume, stylesheet });
|
||||
stylesheetInitialization.current = { resume, stylesheet };
|
||||
|
||||
useResumeCleanup();
|
||||
useBuilderResumeUpdateSubscription();
|
||||
@@ -60,16 +50,6 @@ function RouteComponent() {
|
||||
initializeResumeStore(resume);
|
||||
}, [initializeResumeStore, isInitialized, resume]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized) return;
|
||||
const initial = stylesheetInitialization.current;
|
||||
return initializeStylesheetStore({
|
||||
resumeId,
|
||||
initial: initial.stylesheet,
|
||||
resumeData: initial.resume.data,
|
||||
});
|
||||
}, [isInitialized, resumeId]);
|
||||
|
||||
useEffect(() => {
|
||||
mergeResumeMetadata(resume);
|
||||
}, [
|
||||
@@ -85,7 +65,7 @@ function RouteComponent() {
|
||||
resume,
|
||||
]);
|
||||
|
||||
if (!isInitialized || !isStylesheetInitialized) return null;
|
||||
if (!isInitialized) return null;
|
||||
|
||||
return <BuilderLayoutShell initialLayout={initialLayout} />;
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ every stylesheet.
|
||||
<Step title="Paste one focused rule">
|
||||
Add the stylesheet to the editor. Start with one visual change so it is easy to review in the preview.
|
||||
</Step>
|
||||
<Step title="Wait for Applied">
|
||||
Reactive Resume checks the source and the PDF result. When the status changes to **Applied**, compare the preview
|
||||
and export if you are ready to share the resume.
|
||||
<Step title="Wait for Valid">
|
||||
Reactive Resume checks the source in the browser. When the status changes to **Valid**, compare the preview and
|
||||
export if you are ready to share the resume.
|
||||
</Step>
|
||||
<Step title="Build on the working rule">
|
||||
Add one related change at a time. The editor keeps your draft, undo history, and last valid stylesheet separately.
|
||||
Add one related change at a time. Your changes use the normal resume autosave and undo history.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -300,19 +300,19 @@ browser viewport.
|
||||
Supported media features are `width`, `min-width`, `max-width`, `height`, `min-height`, `max-height`, and
|
||||
`orientation: portrait` or `orientation: landscape`.
|
||||
|
||||
## Apply, diagnose, and recover safely
|
||||
## Diagnose and recover safely
|
||||
|
||||
The editor saves your draft even when it has an error. The preview and PDF export continue using the last stylesheet
|
||||
that compiled and passed PDF checks, so a mistake does not replace a working result.
|
||||
The editor saves the current source even when it has an error. A recoverable error ignores only the invalid declaration,
|
||||
value, selector, or rule; valid parts still appear in preview and PDF export. A fatal version or resource-limit error
|
||||
ignores the whole stylesheet and renders the resume with its base styles until you fix the source.
|
||||
|
||||
If a rule does not work:
|
||||
|
||||
1. Read the status below the editor. Errors include a line and column number when available.
|
||||
2. Check the selector's spelling, attribute value, placement, and template guard. A **selector matches nothing**
|
||||
warning usually means the resume does not contain that semantic node.
|
||||
3. Simplify the rule to one selector and one declaration, then wait for **Applied** before adding more.
|
||||
4. Use **Reset to applied stylesheet** to discard the current draft, or use the stylesheet undo and redo controls to
|
||||
restore an earlier source and applied pair.
|
||||
3. Simplify the rule to one selector and one declaration, then wait for **Valid** before adding more.
|
||||
4. Use the stylesheet undo and redo controls to restore an earlier source.
|
||||
|
||||
Select **Open focus mode** when you need a taller editor. On mobile, it opens a full-width sheet; switch to
|
||||
**Preview** to inspect the result.
|
||||
@@ -331,7 +331,7 @@ starting point. Exact IDs and template parts are intentionally specific to a res
|
||||
2. Open **Design -> Custom Styles** in the destination resume.
|
||||
3. Paste the stylesheet and review any warnings.
|
||||
4. Replace or remove exact IDs and template-part rules that do not apply.
|
||||
5. Wait for **Applied**, then compare the preview and exported PDF.
|
||||
5. Wait for **Valid**, then compare the preview and exported PDF.
|
||||
|
||||
Semantic CSS does not support classes, pseudo-elements, CSS Grid, arbitrary at-rules, `@import`, `@font-face`, `url()`,
|
||||
browser APIs, animations, filters, gradients, general box shadows, or external assets. Use the normal builder settings
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user