diff --git a/.gitignore b/.gitignore
index 43f7886..bde6117 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,3 +64,4 @@ apps/web/static/.audit/
# codehelper (generated local — do not commit)
.zed/
+matej.md
\ No newline at end of file
diff --git a/apps/web/src/lib/admin-ai-roles.ts b/apps/web/src/lib/admin-ai-roles.ts
index 83f3fb0..7e63dc4 100644
--- a/apps/web/src/lib/admin-ai-roles.ts
+++ b/apps/web/src/lib/admin-ai-roles.ts
@@ -41,13 +41,13 @@ export const AI_ROLE_META: Record<
docs_api: {
label: "Docs / API",
description:
- "Future docs/API assistant slot — /docs Ask stays rule-based and must never call this role.",
+ "Future docs/API assistant slot. /docs Ask stays rule-based and must never call this role.",
modelPlaceholder: "gpt-5.6-luna"
},
support: {
label: "Support",
description:
- "Ticket auto-reply AI fallback — configure provider/key/model here; enable delivery in Support knowledge → Auto-reply.",
+ "Ticket auto-reply AI fallback. Configure the provider/key/model here; enable delivery in Support knowledge → Auto-reply.",
modelPlaceholder: "gpt-5.6-luna"
}
};
diff --git a/apps/web/src/lib/assistant/engine.ts b/apps/web/src/lib/assistant/engine.ts
index 4e76db7..05db594 100644
--- a/apps/web/src/lib/assistant/engine.ts
+++ b/apps/web/src/lib/assistant/engine.ts
@@ -75,14 +75,14 @@ export function buildIdentityReply(): AssistantMessage {
return makeMessage({
role: "assistant",
kind: "text",
- text: "I am the System assistant — I help you navigate Descrybe, run supported actions after you confirm, and guide setup using built-in workflows."
+ text: "I am the System assistant. I can help you navigate Descrybe, run supported actions after you confirm, and guide you through setup."
});
}
export function buildApiExamplesMessage(): AssistantMessage {
const key = "dk_YOUR_API_KEY";
const text = [
- "Use your API key in the X-API-Key header (placeholder below — never paste a real secret into chat).",
+ "Use your API key in the X-API-Key header. The example below uses a placeholder. Never paste a real secret into chat.",
"",
"List attributes:",
`curl -s -H "X-API-Key: ${key}" "https://descrybe.io/api/v1/attributes?page=1&limit=25"`,
@@ -165,7 +165,7 @@ export function buildGuideMessages(intent: IntentDefinition): AssistantMessage[]
makeMessage({
role: "assistant",
kind: "text",
- text: `Opening ${intent.route} and highlighting the control to use. Follow the steps above — tell me when you are stuck.`
+ text: `Opening ${intent.route} and highlighting the control to use. Follow the steps above. Tell me if you get stuck.`
})
];
}
@@ -175,7 +175,7 @@ export function buildFailureSupportOffer(issueDetail: string): AssistantMessage
return makeMessage({
role: "assistant",
kind: "quick_replies",
- text: `That action failed${safe ? ` (${safe})` : ""}. You can open a support ticket with this error summary (no secrets).`,
+ text: `That action failed${safe ? ` (${safe})` : ""}. You can open a support ticket with this error summary. Do not include any secrets.`,
quickReplies: ["Open a support ticket", "Help", "Cancel"]
});
}
diff --git a/apps/web/src/lib/assistant/executor.ts b/apps/web/src/lib/assistant/executor.ts
index a4a0f8a..28e2193 100644
--- a/apps/web/src/lib/assistant/executor.ts
+++ b/apps/web/src/lib/assistant/executor.ts
@@ -134,7 +134,7 @@ export async function executeIntent(
const lines = [
`API key created${prefix ? ` (${prefix}…)` : ""}.`,
secret
- ? `Copy this secret now — it will not be shown again:\n${secret}`
+ ? `Copy this secret now. It will not be shown again:\n${secret}`
: "Secret was not returned (you may lack permission). Create one in Settings → API Keys.",
"",
"Example (placeholder if you already copied the secret elsewhere):",
@@ -317,7 +317,7 @@ export async function executeIntent(
message: [
`Based on ~${productCount} product(s) in your catalog, ${fit.name} fits (${cap}${credits}).`,
fit.description ? fit.description : "",
- "Open Billing to review usage or upgrade. Prices follow your live plan list — not invented here."
+ "Open Billing to review usage or upgrade. Prices come directly from your live plan list."
]
.filter(Boolean)
.join("\n"),
diff --git a/apps/web/src/lib/assistant/intents.ts b/apps/web/src/lib/assistant/intents.ts
index 8072bb4..996d937 100644
--- a/apps/web/src/lib/assistant/intents.ts
+++ b/apps/web/src/lib/assistant/intents.ts
@@ -507,7 +507,7 @@ export const INTENT_REGISTRY: IntentDefinition[] = [
guideSteps: [
{ title: "Open Settings → API Keys", detail: "Company admin required to create keys" },
{ title: "Click Create API Key", detail: "Name the key, then create" },
- { title: "Copy the secret once", detail: "It is shown only at creation — store it safely" }
+ { title: "Copy the secret once", detail: "It is shown only when created, so store it safely" }
]
},
{
@@ -704,11 +704,9 @@ export function intentById(id: IntentId): IntentDefinition | undefined {
}
export const QUICK_START_REPLIES = [
- "Getting started",
"Add a feed URL",
"Upload a CSV",
"Map fields",
- "Connect Shopify",
"API keys",
"Attributes",
"Start processing",
diff --git a/apps/web/src/lib/assistant/state.svelte.ts b/apps/web/src/lib/assistant/state.svelte.ts
index 2b5870d..1288907 100644
--- a/apps/web/src/lib/assistant/state.svelte.ts
+++ b/apps/web/src/lib/assistant/state.svelte.ts
@@ -57,7 +57,7 @@ function createAssistantController() {
makeMessage({
role: "assistant",
kind: "quick_replies",
- text: "Hi — I am the System assistant. I help you navigate Descrybe and run supported setup actions. What do you want to do?",
+ text: "Hi! I can help you navigate Descrybe and set things up. What would you like to do?",
quickReplies: [...QUICK_START_REPLIES]
})
];
@@ -261,7 +261,7 @@ function createAssistantController() {
makeMessage({
role: "assistant",
kind: "steps",
- text: "Next — mapping:",
+ text: "Next: mapping",
steps: intentById("map_fields")?.guideSteps
})
);
@@ -491,7 +491,7 @@ function createAssistantController() {
}
flow = { ...flow, stepId: "ask_body", slots: { ...flow.slots, subject } };
const hint = flow.slots.body_prefill
- ? "Describe the issue (a failure summary is already prepared — you can edit or replace it). No secrets."
+ ? "Describe the issue. A failure summary is already prepared, and you can edit or replace it. Do not include any secrets."
: "Describe the issue. No passwords, API keys, or personal data.";
push(
makeMessage({
diff --git a/apps/web/src/lib/components/CommandPalette.svelte b/apps/web/src/lib/components/CommandPalette.svelte
index f2e4519..40809c2 100644
--- a/apps/web/src/lib/components/CommandPalette.svelte
+++ b/apps/web/src/lib/components/CommandPalette.svelte
@@ -327,7 +327,7 @@
aria-modal="true"
aria-label={i18n.t("commandPalette.ariaLabel")}
tabindex="-1"
- class="relative z-[61] flex w-full max-w-lg flex-col overflow-hidden rounded-lg border bg-background shadow-lg"
+ class="relative z-[61] flex w-full max-w-lg flex-col overflow-hidden rounded-lg border bg-background shadow-lg lg:fixed lg:left-[17.5rem] lg:top-[4.5rem] lg:w-80 lg:max-w-none"
onkeydown={onPanelKeydown}
onclick={(event) => event.stopPropagation()}
>
@@ -350,7 +350,7 @@
autocomplete="off"
autocorrect="off"
spellcheck="false"
- class="h-12 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0"
+ class="h-12 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground focus-visible:!outline-none focus-visible:!outline-offset-0"
/>
import type { Snippet } from "svelte";
- import { goto } from "$app/navigation";
- import { api } from "$lib/api";
- import { trackEvent } from "$lib/analytics";
import { navUi } from "$lib/nav-ui.svelte";
import { commandPaletteShortcutLabelFromEnv } from "$lib/command-palette-search";
import { i18n } from "$lib/i18n";
- import { LogOut, Menu, Search } from "@lucide/svelte";
+ import { Menu, Search } from "@lucide/svelte";
- /** App chrome header — ThemeToggle is slotted from +layout (shared $lib/theme). */
+ /** App chrome header with controls supplied by the app layout. */
let { children }: { children?: Snippet } = $props();
- let loggingOut = $state(false);
const paletteShortcut = commandPaletteShortcutLabelFromEnv();
-
- async function logout() {
- if (loggingOut) return;
- loggingOut = true;
- try {
- await api("/api/auth/logout", { method: "POST" });
- } catch {
- /* session may already be gone */
- } finally {
- trackEvent("logout");
- await goto("/login");
- loggingOut = false;
- }
- }
-
- {loggingOut ? i18n.t("header.signingOut") : i18n.t("header.signOut")}
-
diff --git a/apps/web/src/lib/components/Nav.svelte b/apps/web/src/lib/components/Nav.svelte
index acb2139..4eb102d 100644
--- a/apps/web/src/lib/components/Nav.svelte
+++ b/apps/web/src/lib/components/Nav.svelte
@@ -6,7 +6,6 @@
import { activateFocusTrap, type FocusTrapHandle } from "$lib/a11y/focus-trap";
import { planCapabilities } from "$lib/plan-capabilities.svelte";
import { navItemAllowed } from "$lib/plan-capabilities";
- import { commandPaletteShortcutLabelFromEnv } from "$lib/command-palette-search";
import { i18n } from "$lib/i18n";
import {
LayoutDashboard,
@@ -262,8 +261,6 @@
let trap: FocusTrapHandle | null = null;
let isDesktop = $state(false);
let navMounted = $state(false);
- const paletteShortcut = commandPaletteShortcutLabelFromEnv();
-
$effect(() => {
if (typeof window === "undefined") return;
navMounted = true;
@@ -466,18 +463,6 @@
-
{i18n.t("assistant.title")}
-
{i18n.t("assistant.subtitle")}
{
e.preventDefault();
- submit();
+ void submit();
}}
>
{i18n.t("assistant.messageLabel")}
diff --git a/apps/web/src/lib/components/assistant/AssistantHost.svelte b/apps/web/src/lib/components/assistant/AssistantHost.svelte
index 1d34321..afef939 100644
--- a/apps/web/src/lib/components/assistant/AssistantHost.svelte
+++ b/apps/web/src/lib/components/assistant/AssistantHost.svelte
@@ -44,7 +44,7 @@
{#if assistant.open}
@@ -54,8 +54,8 @@
type="button"
class="{buttonClasses(
assistant.open ? 'secondary' : 'default',
- 'default',
- 'fixed bottom-4 right-4 z-[55] h-12 gap-2 rounded-full px-4 shadow-md sm:bottom-6 sm:right-6'
+ 'icon',
+ 'fixed bottom-4 right-4 z-[65] h-12 w-12 rounded-full shadow-md sm:bottom-6 sm:right-6'
)}"
data-assistant-target="assistant-fab"
data-tour="assistant-fab"
@@ -64,6 +64,5 @@
onclick={() => assistant.toggle()}
>
- {i18n.t("assistant.label")}
{/if}
diff --git a/apps/web/src/lib/components/docs/DocsAskGuide.svelte b/apps/web/src/lib/components/docs/DocsAskGuide.svelte
index ba4476f..513f400 100644
--- a/apps/web/src/lib/components/docs/DocsAskGuide.svelte
+++ b/apps/web/src/lib/components/docs/DocsAskGuide.svelte
@@ -53,7 +53,7 @@
function guideLineFor(node: DocsGuideNode): TranscriptEntry {
if (isQuestionNode(node)) {
- return { role: "guide", text: node.body?.trim() ? `${node.title} — ${node.body}` : node.title };
+ return { role: "guide", text: node.body?.trim() ? `${node.title}: ${node.body}` : node.title };
}
return { role: "guide", text: node.outcome || node.title };
}
@@ -235,7 +235,7 @@
@@ -294,7 +294,7 @@
class="inline-flex max-w-full items-center gap-1.5 text-left text-sm font-medium text-blue-700 hover:underline dark:text-blue-300"
onclick={() => followEndpoint(ep)}
>
- {ep.method} {ep.path}{ep.summary ? ` — ${ep.summary}` : ""}
+ {ep.method} {ep.path}{ep.summary ? `: ${ep.summary}` : ""}
{/each}
diff --git a/apps/web/src/lib/components/products/ProductTable.svelte b/apps/web/src/lib/components/products/ProductTable.svelte
index 0fd98e6..6dc477f 100644
--- a/apps/web/src/lib/components/products/ProductTable.svelte
+++ b/apps/web/src/lib/components/products/ProductTable.svelte
@@ -187,7 +187,7 @@ import { Check, Edit, MoreHorizontal, X } from "@lucide/svelte";
const missing = Object.values(checks as Record)
.filter((c) => !c?.passed)
.map((c) => c.label || "check");
- if (missing.length) return `${grade} ${score}/100 — missing: ${missing.join(", ")}`;
+ if (missing.length) return `${grade} ${score}/100; missing: ${missing.join(", ")}`;
}
return `${grade} ${score}/100`;
}
diff --git a/apps/web/src/lib/components/stores/StoreSetupWizard.svelte b/apps/web/src/lib/components/stores/StoreSetupWizard.svelte
index ac04883..86a6159 100644
--- a/apps/web/src/lib/components/stores/StoreSetupWizard.svelte
+++ b/apps/web/src/lib/components/stores/StoreSetupWizard.svelte
@@ -172,12 +172,12 @@
{#if canWoo}
selectChannel("woocommerce")}
>
-
+
{i18n.t("stores.wizard.channel.woocommerce")}
{i18n.t("stores.woo.description")} selectChannel("shopify")}
>
-
+
{i18n.t("stores.wizard.channel.shopify")}
{i18n.t("stores.shopify.description")} (null);
let popoverStyle = $state("");
let trap: FocusTrapHandle | null = null;
- let jumpOpen = $state(false);
type Hole = { top: number; left: number; width: number; height: number };
@@ -98,10 +97,6 @@
if (!tutorial.active) return;
if (event.key === "Escape") {
event.preventDefault();
- if (jumpOpen) {
- jumpOpen = false;
- return;
- }
tutorial.dismiss();
}
}
@@ -194,12 +189,6 @@
return () => clearInterval(id);
});
- $effect(() => {
- void tutorial.stepIndex;
- void tutorial.sectionPickerOpen;
- jumpOpen = false;
- });
-
const stepNumber = $derived(tutorial.stepIndex + 1);
const progressPct = $derived(
tutorial.sectionPickerOpen
@@ -232,7 +221,6 @@
const sectionLabel = $derived(i18n.t(`tutorial.section.${tutorial.step.sectionId}`));
async function jumpToSection(sectionId: TutorialSectionId) {
- jumpOpen = false;
await tutorial.startSection(sectionId);
}
@@ -398,40 +386,6 @@
{/if}
-
-
(jumpOpen = !jumpOpen)}
- >
- {i18n.t("tutorial.jumpTo")}
-
- {#if jumpOpen}
-
- {#each tutorial.sections as section (section.id)}
-
- void jumpToSection(section.id)}
- >
- {section.label}
-
-
- {/each}
-
- {/if}
-
-
0 && top + menuHeight > window.innerHeight - 8) {
top = Math.max(8, r.top - menuHeight - gap);
@@ -167,7 +168,7 @@
= 0) activeIndex = index;
}}
class={cn(
- "fixed z-[70] min-w-[10rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg",
+ "fixed z-[130] min-w-[10rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg",
className
)}
style={align === "end"
diff --git a/apps/web/src/lib/docs-guide/tree.ts b/apps/web/src/lib/docs-guide/tree.ts
index 6d88ab7..e595865 100644
--- a/apps/web/src/lib/docs-guide/tree.ts
+++ b/apps/web/src/lib/docs-guide/tree.ts
@@ -20,7 +20,7 @@ const TRY_IT_TIP =
export const DOCS_GUIDE_TREE: DocsGuideTree = {
version: 2,
disclaimer:
- "This is a guided router for the public API docs — not a chat bot and not an AI. Pick options to land on the right endpoints.",
+ "This is a guided router for the public API docs, not a chat bot or AI. Pick options to find the right endpoints.",
rootId: "root",
escapeLinks: [
{ label: "Browse API reference", href: "/docs#overview", kind: "docs" },
@@ -33,7 +33,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
id: "root",
pathKey: "start",
title: "What are you trying to do?",
- body: "I’ll route you to the right Descrybe public API docs (/api/v1). Pick one option — no free-text needed.",
+ body: "I’ll route you to the right Descrybe public API docs (/api/v1). Pick one option; no free text is needed.",
choices: [
{
id: "first",
@@ -99,7 +99,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
{
id: "process",
label: "Process / enrich products",
- description: "Two contracts — choose carefully",
+ description: "Two contracts; choose carefully",
nextId: "q.process"
},
{
@@ -116,7 +116,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
id: "q.feeds",
pathKey: "feeds",
synonyms: ["supplier feed", "import csv", "sync feed"],
- title: "Feeds — what do you need?",
+ title: "Feeds: what do you need?",
choices: [
{
id: "create",
@@ -158,7 +158,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
},
{
id: "chooser",
- label: "Not sure — compare both",
+ label: "Not sure? Compare both",
nextId: "answer.process.chooser"
}
]
@@ -169,7 +169,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
id: "q.export",
pathKey: "export",
synonyms: ["channel feed", "google shopping", "export csv"],
- title: "Export feeds — what do you need?",
+ title: "Export feeds: what do you need?",
choices: [
{
id: "setup",
@@ -323,10 +323,10 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
body: "1) Create or copy a company API key (dk_…). 2) Jump to Authorize and enter Authorization: Bearer dk_… (preferred) or X-API-Key. 3) Confirm the server URL matches your environment. 4) Open any operation below, fill params/body from the examples, and click TRY. Unauthenticated smoke: GET /health. Authenticated smoke: GET /products.",
authNote: AUTH_NOTE,
endpoints: [
- endpoint("GET", "/health", "No key required — confirm the viewer reaches the API"),
+ endpoint("GET", "/health", "No key required; confirm the viewer reaches the API"),
endpoint("GET", "/products", "Authenticated smoke list (page=1&limit=1)"),
- endpoint("POST", "/products/process", "EAN batch — after Authorize"),
- endpoint("POST", "/process", "Jobs process — after Authorize")
+ endpoint("POST", "/products/process", "EAN batch after Authorize"),
+ endpoint("POST", "/process", "Jobs process after Authorize")
],
links: [
{ label: "Authorize panel", href: docsAuthHref(), kind: "docs" },
@@ -421,7 +421,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
synonyms: ["ean", "items[].ean", "products/process"],
title: "Public process (EAN batch)",
outcome: "Start and poll enrichment using POST/GET /products/process.",
- body: "POST body uses items[].ean (preferred). Responses use HTTP 200 with { data: { process_id, … } }. Poll GET until COMPLETED. items[].description is generated HTML product copy (headings, paragraphs, lists) whose length follows the category description formula — not a one- or two-sentence blurb. Some categories request very long multi-section articles. Handler also accepts raw_product_ids as an alternate on this same path.",
+ body: "POST body uses items[].ean (preferred). Responses use HTTP 200 with { data: { process_id, … } }. Poll GET until COMPLETED. items[].description is generated HTML product copy (headings, paragraphs, lists) whose length follows the category description formula, not a one- or two-sentence blurb. Some categories request very long multi-section articles. Handler also accepts raw_product_ids as an alternate on this same path.",
authNote: AUTH_NOTE,
endpoints: [
endpoint("POST", "/products/process", "Start processing by EAN"),
@@ -437,7 +437,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
{ label: "Authorize & Try", href: docsAuthHref(), kind: "docs" },
{ label: "Processing UI", href: "/processing", kind: "app" }
],
- warning: "Not an alias of POST/GET /process — different body and envelope.",
+ warning: "Not an alias of POST/GET /process; it uses a different body and envelope.",
relatedIds: ["answer.process.jobs", "answer.process.chooser", "answer.products"]
},
@@ -474,7 +474,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
synonyms: ["which process", "dual contract"],
title: "Which process API should I use?",
outcome: "Pick EAN batch for integrations; pick /process for dashboard-style jobs.",
- body: "1) Public process: POST/GET /products/process — items[].ean, HTTP 200 { data: { process_id } }. 2) Jobs: POST/GET /process — raw_product_ids, flat JSON, 202 on start. Mixing envelopes is the most common integration bug.",
+ body: "1) Public process: POST/GET /products/process uses items[].ean and returns HTTP 200 { data: { process_id } }. 2) Jobs: POST/GET /process uses raw_product_ids, flat JSON, and returns 202 on start. Mixing envelopes is the most common integration bug.",
authNote: AUTH_NOTE,
endpoints: [
endpoint("POST", "/products/process", "EAN batch"),
@@ -637,7 +637,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
{ label: "Authorize", href: docsAuthHref(), kind: "docs" },
{ label: "Products UI", href: "/products", kind: "app" }
],
- warning: "Reset is destructive to processed state — confirm before calling in production.",
+ warning: "Reset is destructive to processed state. Confirm before calling it in production.",
relatedIds: ["answer.products", "answer.process.jobs"]
},
@@ -676,7 +676,7 @@ export const DOCS_GUIDE_TREE: DocsGuideTree = {
pathKey: "billing",
synonyms: ["credits", "plan", "upgrade", "limits"],
title: "Billing and plan limits",
- outcome: "Plan and credit limits are enforced server-side on processing — manage them in the app, not via /api/v1.",
+ outcome: "Plan and credit limits are enforced server-side during processing. Manage them in the app, not via /api/v1.",
body: "The public API-key surface does not expose checkout. Use Plans / Billing in the web app. Insufficient credits or free-tier gates typically surface as processing failures rather than OpenAPI operations.",
links: [
{ label: "Plans", href: "/plans", kind: "app" },
diff --git a/apps/web/src/lib/i18n/messages/en.ts b/apps/web/src/lib/i18n/messages/en.ts
index 4bba2e5..f7531d9 100644
--- a/apps/web/src/lib/i18n/messages/en.ts
+++ b/apps/web/src/lib/i18n/messages/en.ts
@@ -122,7 +122,7 @@ export const en: MessageDict = {
"auth.reset.tokenInvalid":
"This reset link is invalid or expired. Request a new one from the forgot-password page.",
"auth.reset.linkRecognized":
- "Reset link recognized. Enter a new password below — the secret is not shown on this page.",
+ "Reset link recognized. Enter a new password below. The secret is not shown on this page.",
"auth.reset.tokenLabel": "Reset token",
"auth.reset.passwordHint":
"At least 8 characters. No other complexity rules.",
@@ -143,19 +143,19 @@ export const en: MessageDict = {
"auth.invite.verifyDescription":
"This email already has a Descrybe account. Enter your current password to join this company. You keep your own workspace and can switch between companies.",
"auth.invite.recoverDescription":
- "This email has an account with no active company (for example after being removed). Choose a new password to rejoin — you also get your own workspace.",
+ "This email has an account with no active company (for example after being removed). Choose a new password to rejoin and get your own workspace.",
"auth.invite.setPasswordDescription":
"Choose a password for your migrated Descrybe account (at least 8 characters).",
"auth.invite.checking": "Checking invite…",
"auth.invite.forEmail": "Invite for {email}.",
"auth.invite.linkRecognized":
- "Invite link recognized. Enter a password below to continue — the secret is not shown on this page.",
+ "Invite link recognized. Enter a password below to continue. The secret is not shown on this page.",
"auth.invite.verifyLinkRecognized":
- "Invite link recognized. Enter your current account password below — the invite secret is not shown on this page.",
+ "Invite link recognized. Enter your current account password below. The invite secret is not shown on this page.",
"auth.invite.recoverLinkRecognized":
- "Invite link recognized. Choose a new password below to restore access — the invite secret is not shown on this page.",
+ "Invite link recognized. Choose a new password below to restore access. The invite secret is not shown on this page.",
"auth.invite.resetLinkRecognized":
- "Reset link recognized. Enter a password below to continue — the secret is not shown on this page.",
+ "Reset link recognized. Enter a password below to continue. The secret is not shown on this page.",
"auth.invite.tokenLabel": "Invite token",
"auth.invite.resetTokenLabel": "Reset token",
"auth.invite.tokenHelp":
@@ -183,7 +183,7 @@ export const en: MessageDict = {
"auth.invite.emailMismatchDefault":
"You're signed in as a different email than this invite.",
"auth.invite.expiredFooter":
- "Expired link? Ask a company admin to re-issue — there is no self-serve resend API.",
+ "Expired link? Ask a company admin to reissue it because there is no self-serve resend API.",
"auth.invite.adminUsersLink": "Admin → Users",
"auth.invite.doneTitle": "You're on the team",
"auth.invite.doneSetPasswordTitle": "Password saved",
@@ -195,7 +195,7 @@ export const en: MessageDict = {
"auth.invite.companySettings": "Company settings",
"auth.invite.goToSignIn": "Go to sign in",
"auth.invite.afterSignInNote":
- "After sign-in you land in your workspace — the greenfield setup tour is skipped.",
+ "After signing in, you land in your workspace and skip the greenfield setup tour.",
"auth.invite.mismatchTitle": "Wrong account for this invite",
"auth.invite.mismatchDescription":
"This link is for a different email than the one you're signed in with. Sign out to continue as the invited user, or stay signed in and ask an admin to re-issue the invite.",
@@ -347,9 +347,9 @@ export const en: MessageDict = {
"settings.apiKeyLabel": "API key",
"settings.keyName": "Key name",
"settings.storeKeySafe":
- "Store it somewhere safe — you cannot view the full secret again.",
+ "Store it somewhere safe because you cannot view the full secret again.",
"settings.apiKeysIntro":
- "Create a company API key to call the public /api/v1 endpoints. The full secret is shown only once when you create it — copy it immediately.",
+ "Create a company API key to call the public /api/v1 endpoints. The full secret is shown only once when you create it, so copy it immediately.",
"settings.apiKeysUseWhere":
"Send it as Authorization: Bearer (preferred) or X-API-Key.",
"settings.apiKeysDocsLink": "API authentication docs",
@@ -361,7 +361,7 @@ export const en: MessageDict = {
"dashboard.demoSandbox": "Demo sandbox",
"dashboard.demoEmptyTitle": "Demo sandbox is empty",
"dashboard.demoEmptyHint":
- "Demo sandbox is empty — connect a feed to see catalog stats.",
+ "The demo sandbox is empty. Connect a feed to see catalog stats.",
"dashboard.overviewTitle": "Overview",
"dashboard.quickLinks": "Quick links",
"dashboard.whatsNew": "What's new",
@@ -379,7 +379,7 @@ export const en: MessageDict = {
"dashboard.emptyPaygHint":
"Descrybe brings a catalog in, enriches and processes it, then exports or pushes to stores. Use Getting started below.",
"dashboard.emptyCreditsHint":
- "Descrybe brings a catalog in, enriches and processes it, then exports or pushes to stores. You have {credits} — start with Getting started below.",
+ "Descrybe brings a catalog in, enriches and processes it, then exports or pushes to stores. You have {credits}. Start with Getting started below.",
"dashboard.workflowTitle": "Catalog workflow",
"dashboard.recentActivity": "Recent activity",
"dashboard.loadFailed": "Failed to load dashboard",
@@ -443,7 +443,7 @@ export const en: MessageDict = {
"dashboard.jobsRunningPlural": "{count} jobs running",
"dashboard.activeJobs": "{count} active job",
"dashboard.activeJobsPlural": "{count} active jobs",
- "dashboard.noJobsEmpty": "No jobs yet — import products first.",
+ "dashboard.noJobsEmpty": "No jobs yet. Import products first.",
"dashboard.noJobsReady":
"No recent jobs. Start one from Products when you are ready.",
"dashboard.startJob": "Start a job",
@@ -454,9 +454,9 @@ export const en: MessageDict = {
"dashboard.exportsTemplates": "Templates & download",
"activation.title": "Getting started",
"activation.description":
- "Descrybe brings a catalog in, enriches and processes it, then exports or pushes to stores. Finish the current step, then continue — the demo tour explains screens without changing checklist progress.",
+ "Descrybe brings a catalog in, enriches and processes it, then exports or pushes to stores. Finish the current step, then continue. The demo tour explains screens without changing checklist progress.",
"activation.descriptionShort":
- "Start with the highlighted step below — one action at a time. Store connect is optional.",
+ "Start with the highlighted step below, one action at a time. Store connect is optional.",
"activation.nextCta": "Do this step",
"activation.valueProp":
"Descrybe brings a catalog in, enriches and processes it, then exports or pushes to your stores.",
@@ -468,12 +468,12 @@ export const en: MessageDict = {
"activation.corePath":
"Catalog in → Enrich / process → Export or push to stores",
"activation.storeWizardHint":
- "Not required for feeds or exports — skip unless you want to push products to a live shop.",
+ "This is not required for feeds or exports. Skip it unless you want to push products to a live shop.",
"activation.storeWizardCta": "Open guided store setup",
"activation.focus.map":
"Next: open Map on a feed, match columns, then save. Add a feed first if the list is empty.",
"activation.focus.sync":
- "Next: Sync a sample after mappings are saved. Use Sync on a mapped feed — avoid syncing unmapped sources.",
+ "Next: Sync a sample after mappings are saved. Use Sync on a mapped feed, and avoid syncing unmapped sources.",
"activation.focus.dismiss": "Dismiss hint",
"activation.dismiss": "Dismiss checklist",
"hypercare.report.title": "Need help after the move?",
@@ -484,11 +484,11 @@ export const en: MessageDict = {
"hypercare.report.adminTriage": "Open hypercare queue",
"etl.gaps.title": "What still needs setup",
"etl.gaps.message":
- "Some items weren't copied when we moved your workspace. Nothing is broken — recreate or reconnect as needed.",
+ "Some items weren't copied when we moved your workspace. Nothing is broken; recreate or reconnect as needed.",
"etl.gaps.dismiss": "Dismiss",
"etl.gaps.apiKeys.title": "API keys",
"etl.gaps.apiKeys.body":
- "Previous API keys weren't copied. Create a new key to restore API access — the secret is shown only once.",
+ "Previous API keys weren't copied. Create a new key to restore API access. The secret is shown only once.",
"etl.gaps.apiKeys.cta": "Open API keys",
"etl.gaps.blobs.title": "Uploaded files",
"etl.gaps.blobs.body":
@@ -500,13 +500,13 @@ export const en: MessageDict = {
"etl.gaps.jobs.body":
"Earlier job history wasn't imported. New jobs you start here will appear normally.",
"etl.gaps.jobs.emptyHint":
- "Processing history wasn't imported — an empty list is expected.",
+ "Processing history wasn't imported, so an empty list is expected.",
"etl.gaps.jobs.migratedHint":
"{count} jobs were imported from the previous workspace.",
"etl.gaps.jobs.cta": "Open Processing",
"etl.gaps.woo.title": "Store credentials",
"etl.gaps.woo.body":
- "WooCommerce and Shopify passwords may need to be re-entered after the move — reconnect your store; sync is not broken.",
+ "WooCommerce and Shopify passwords may need to be re-entered after the move. Reconnect your store; sync is not broken.",
"etl.gaps.woo.cta": "Open Stores",
"etl.gaps.settings.title": "Company settings",
"etl.gaps.settings.body":
@@ -516,7 +516,7 @@ export const en: MessageDict = {
"activation.stepsLabel": "Activation steps",
"activation.pausedTitle": "Getting started is paused",
"activation.pausedBody":
- "Resume anytime — from product fields through export. Store connect stays optional.",
+ "Resume anytime, from product fields through export. Store connect stays optional.",
"activation.resume": "Resume checklist",
"activation.step.enable-fields.title": "Turn on product fields",
"activation.step.enable-fields.body":
@@ -586,11 +586,11 @@ export const en: MessageDict = {
"Only company admins can {action}. Ask a company admin for help.",
"errors.companyAdminDenied.actionDefault": "do this",
"docs.tryIt.memberCannotCreate":
- "Only company admins can create API keys. Keys from the previous platform were not migrated — paste a key below, or ask an admin to create a new one.",
+ "Only company admins can create API keys. Keys from the previous platform were not migrated. Paste a key below, or ask an admin to create a new one.",
"docs.tryIt.noKeysLegacyAdmin":
- "API keys from the previous platform were not migrated. Create a new key via Use my API key or Settings → API keys — the secret is shown only once.",
+ "API keys from the previous platform were not migrated. Create a new key via Use my API key or Settings → API keys. The secret is shown only once.",
"docs.tryIt.noKeysLegacyMember":
- "No API keys listed. Keys from the previous platform were not migrated — ask a company admin to create one, or paste a dk_ key.",
+ "No API keys listed. Keys from the previous platform were not migrated. Ask a company admin to create one, or paste a dk_ key.",
"errors.couldNotUpdateLanguage": "Could not update language",
"toast.support.replyTitle": "Support reply",
"toast.support.replyBody": "Staff replied to your support ticket.",
@@ -621,7 +621,7 @@ export const en: MessageDict = {
"tutorial.continue": "Continue",
"tutorial.finishTour": "Finish tour",
"tutorial.missingTargetHint":
- "That control isn’t on this screen yet. You can still Continue — the tour never requires a save or sync.",
+ "That control isn’t on this screen yet. You can still Continue because the tour never requires a save or sync.",
"tutorial.section.welcome": "Welcome",
"tutorial.section.catalog": "Catalog setup",
"tutorial.section.sources": "Feeds",
@@ -632,24 +632,24 @@ export const en: MessageDict = {
"tutorial.section.done": "Done",
"tutorial.step.welcome.title": "Welcome to Descrybe",
"tutorial.step.welcome.body":
- "Descrybe’s core loop is Feeds → Process → Export / Stores. This guided demo highlights those screens on your real dashboard — without requiring you to save, sync, enable fields, or create feeds. Pause anytime; resume from the header. Jump to a section when you only need help with one topic.",
+ "Descrybe’s core loop is Feeds → Process → Export / Stores. This guided demo highlights those screens on your real dashboard without requiring you to save, sync, enable fields, or create feeds. Pause anytime; resume from the header. Jump to a section when you only need help with one topic.",
"tutorial.step.welcome.tip":
- "Demo tour — your data stays unchanged until you choose to act outside the tour.",
+ "Demo tour: your data stays unchanged until you choose to act outside the tour.",
"tutorial.step.welcome.nextLabel": "Start tour",
"tutorial.step.welcome.missingHint":
- "You’re on the dashboard — continue when ready.",
+ "You’re on the dashboard. Continue when ready.",
"tutorial.step.dashboard-overview.title": "Your command center",
"tutorial.step.dashboard-overview.body":
- "The catalog workflow strip shows the path you’ll use every day: Feeds → Map → Sync → Process. Getting started tracks real workspace progress (separate from this demo). When you’re ready for live work, use Manage feeds or Start processing — then export or connect a store.",
+ "The catalog workflow strip shows the path you’ll use every day: Feeds → Map → Sync → Process. Getting started tracks real workspace progress (separate from this demo). When you’re ready for live work, use Manage feeds or Start processing, then export or connect a store.",
"tutorial.step.dashboard-overview.tip":
- "Completing this tour does not mark checklist steps done — only real workspace progress does.",
+ "Completing this tour does not mark checklist steps done; only real workspace progress does.",
"tutorial.step.dashboard-overview.missingHint":
"The workflow strip and checklist appear after the dashboard loads.",
"tutorial.step.enable-fields.title": "Standard fields",
"tutorial.step.enable-fields.body":
- "Standard Fields are the product columns Descrybe maps, processes, and exports — title, description, GTIN, images, and more. Enable recommended turns on a sensible default set for most catalogs. You can refine the list later without breaking existing mappings.",
+ "Standard Fields are the product columns Descrybe maps, processes, and exports, including title, description, GTIN, images, and more. Enable recommended turns on a sensible default set for most catalogs. You can refine the list later without breaking existing mappings.",
"tutorial.step.enable-fields.tip":
- "In the demo you don’t need to click Enable recommended — Continue when you understand the idea.",
+ "In the demo you don’t need to click Enable recommended. Continue when you understand the idea.",
"tutorial.step.enable-fields.warning":
"Enabling or disabling fields affects mapping and processing for this company. Prefer Enable recommended once, then tweak carefully.",
"tutorial.step.enable-fields.missingHint":
@@ -663,7 +663,7 @@ export const en: MessageDict = {
"Expand Catalog in the sidebar to open Categories or Attributes after the tour.",
"tutorial.step.connect-source.title": "Add a product source",
"tutorial.step.connect-source.body":
- "Feeds are how supplier catalogs enter Descrybe — CSV, XML, or URL-based files. Add Feed creates a new source; each row shows status (ready for mapping, mapped, active). Map opens the field-matching screen for that feed.",
+ "Feeds are how supplier catalogs enter Descrybe as CSV, XML, or URL-based files. Add Feed creates a new source; each row shows status (ready for mapping, mapped, active). Map opens the field-matching screen for that feed.",
"tutorial.step.connect-source.tip":
"You don’t need to add a feed for the demo. When ready for real setup, add one sample file first.",
"tutorial.step.connect-source.warning":
@@ -674,16 +674,16 @@ export const en: MessageDict = {
"tutorial.step.map.body":
"On a feed’s Map screen you match supplier columns to Descrybe standard fields. Auto-map suggests matches from column names; confirm fuzzy matches, then fix anything wrong. Save Mappings stores the map so sync and processing know how to read each row.",
"tutorial.step.map.tip":
- "Demo tip: open Map on a feed when you have one — you are not required to save.",
+ "Demo tip: open Map on a feed when you have one. You are not required to save.",
"tutorial.step.map.warning":
- "Don’t Save Mappings until Auto-map looks right — a bad map makes bad products.",
+ "Don’t Save Mappings until Auto-map looks right because a bad map makes bad products.",
"tutorial.step.map.missingHint":
"Add a feed and click Map to see the mapping UI; or Continue to keep learning.",
"tutorial.step.sync-sample.title": "Sync a sample (when ready)",
"tutorial.step.sync-sample.body":
"After mappings are saved, Sync + Process sample pulls a small batch and runs processing so you can verify titles, images, and attributes before a full run. Full Sync now on the feeds list is for larger imports once the sample looks good.",
"tutorial.step.sync-sample.tip":
- "Skip Sync during the demo — use it only after Save Mappings on a real feed.",
+ "Skip Sync during the demo. Use it only after Save Mappings on a real feed.",
"tutorial.step.sync-sample.warning":
"Syncing writes products into this company. Prefer a sample first; avoid Sync now on unmapped feeds.",
"tutorial.step.sync-sample.missingHint":
@@ -692,42 +692,42 @@ export const en: MessageDict = {
"tutorial.step.process.body":
"Products lists synced and processed catalog items. Filter by status, open a row for details, and use EPREL badges when energy-label data exists. This is where you spot mapping mistakes before export or store sync.",
"tutorial.step.process.tip":
- "Browsing products is read-only for exploration — editing a row changes live catalog data.",
+ "Browsing products is read-only for exploration, but editing a row changes live catalog data.",
"tutorial.step.process.missingHint":
"Wait for the products table, or Continue if the catalog is still empty.",
"tutorial.step.processing-hub.title": "Background processing",
"tutorial.step.processing-hub.body":
"Jobs under Processing show AI and pipeline progress: completed runs and failures. Start jobs from the dashboard or products when you intentionally want to spend credits on a batch.",
"tutorial.step.processing-hub.tip":
- "Jobs consume credits — watch Billing before large runs.",
+ "Jobs consume credits, so check Billing before large runs.",
"tutorial.step.processing-hub.missingHint":
"Open Jobs from the Processing section in the sidebar if the page is empty.",
"tutorial.step.stores-hub.title": "Stores & platforms",
"tutorial.step.stores-hub.body":
- "After products are processed, Stores connects WooCommerce or Shopify so you can push cleaned catalog data to your shop. Use this when a marketplace or CMS is your destination — or skip to Export feeds if partners only need a file/URL.",
+ "After products are processed, Stores connects WooCommerce or Shopify so you can push cleaned catalog data to your shop. Use this when a marketplace or CMS is your destination, or skip to Export feeds if partners only need a file/URL.",
"tutorial.step.stores-hub.tip":
- "Reconnect banners appear if a token expires — fix those before relying on sync.",
+ "Reconnect banners appear if a token expires. Resolve them before relying on sync.",
"tutorial.step.stores-hub.missingHint":
"Open Stores from the sidebar to see connection cards.",
"tutorial.step.export.title": "Export cleaned products",
"tutorial.step.export.body":
- "Export feeds publish cleaned products as XML or CSV for channels, partners, or your store. Create an export, choose format and filters, then share the feed URL or download. Exports read processed catalog data — map and process first for best results.",
+ "Export feeds publish cleaned products as XML or CSV for channels, partners, or your store. Create an export, choose format and filters, then share the feed URL or download. Exports read processed catalog data, so map and process first for best results.",
"tutorial.step.export.tip":
"You don’t need to create an export during the demo.",
"tutorial.step.export.warning":
- "Creating or deleting export feeds affects what partners can download — avoid deleting production feeds casually.",
+ "Creating or deleting export feeds affects what partners can download. Avoid deleting production feeds casually.",
"tutorial.step.export.missingHint":
"Wait for Export feeds to load, or open Exports from the Feeds section in the sidebar.",
"tutorial.step.billing-hub.title": "Credits & billing",
"tutorial.step.billing-hub.body":
"Billing shows your plan, wallet, and remaining credits. Processing and some AI features draw from this balance. Keep an eye on usage before large catalog runs.",
"tutorial.step.billing-hub.tip":
- "Demo accounts may show unlimited or large balances — treat production credits carefully.",
+ "Demo accounts may show unlimited or large balances. Treat production credits carefully.",
"tutorial.step.billing-hub.missingHint":
"Open Billing from the Account section in the sidebar.",
"tutorial.step.grow-hub.title": "Campaigns, SEO & reviews",
"tutorial.step.grow-hub.body":
- "In the Marketing section: Campaigns generate marketing content, Content calendar plans posts, SEO checks listing readiness, and Reviews ties into WooCommerce. These build on a healthy mapped catalog — finish Feeds → Process → Export / Stores before leaning on them.",
+ "In the Marketing section: Campaigns generate marketing content, Content calendar plans posts, SEO checks listing readiness, and Reviews ties into WooCommerce. These build on a healthy mapped catalog, so finish Feeds → Process → Export / Stores before leaning on them.",
"tutorial.step.grow-hub.tip":
"Jump back here when you’re ready to market, not during first mapping.",
"tutorial.step.grow-hub.missingHint":
@@ -736,7 +736,7 @@ export const en: MessageDict = {
"tutorial.step.tour-done.body":
"That’s the product map: Feeds → Map → Sync → Process → Export / Stores. Use Getting started on the dashboard for real setup. Resume or Restart this tour from the header anytime, or Jump to… a section when you need a refresher. Skip tour ends the demo without changing your checklist.",
"tutorial.step.tour-done.tip":
- "Demo complete — no cleanup needed. Your account was not modified by Continue alone.",
+ "Demo complete. No cleanup is needed. Your account was not modified by Continue alone.",
"tutorial.step.tour-done.nextLabel": "Finish tour",
"tutorial.step.tour-done.missingHint": "Return to the dashboard to finish.",
"news.uxImprovements.title": "User Experience Improvements",
@@ -837,7 +837,7 @@ export const en: MessageDict = {
"admin.sales.col.created": "Created",
"sales.contact.title": "Contact sales",
"sales.contact.lead":
- "Tell us about your catalog and capacity needs — we will prepare a custom plan you can pay online.",
+ "Tell us about your catalog and capacity needs. We will prepare a custom plan you can pay online.",
"sales.contact.formTitle": "Send a message",
"sales.contact.formLead":
"Enterprise and custom deals are prepared by our team, then paid via Stripe Checkout.",
@@ -850,8 +850,8 @@ export const en: MessageDict = {
"sales.contact.submit": "Send message",
"sales.contact.submitting": "Sending…",
"sales.contact.error": "Could not send your message. Please try again.",
- "sales.contact.toast.success": "Message sent — we will follow up soon.",
- "sales.contact.thanksTitle": "Thanks — we got your message",
+ "sales.contact.toast.success": "Message sent. We will follow up soon.",
+ "sales.contact.thanksTitle": "Thanks, we got your message",
"sales.contact.thanksBody":
"A platform admin will review your request and can send a Checkout link for a custom plan.",
"sales.contact.bookDemo": "Book a live demo",
@@ -860,7 +860,7 @@ export const en: MessageDict = {
"admin.overview.eyebrow": "Command center",
"admin.overview.title": "Platform overview",
"admin.overview.description":
- "Live platform pulse from analytics summary — jump to ops, insight, and support.",
+ "Live platform pulse from analytics summary. Jump to ops, insight, and support.",
"admin.overview.tokens": "Tokens",
"admin.overview.tokensHint": "All-time · period",
"admin.overview.credits": "Credits",
@@ -894,7 +894,7 @@ export const en: MessageDict = {
"admin.overview.signal.stuckTitle": "{count} stuck job",
"admin.overview.signal.stuckTitlePlural": "{count} stuck jobs",
"admin.overview.signal.stuckDetail":
- "Running longer than 2 hours — review and reset if needed.",
+ "Running longer than 2 hours. Review and reset if needed.",
"admin.overview.signal.stuckCta": "Open stuck jobs",
"admin.overview.signal.failedTitle": "{count} failed in last {days}d",
"admin.overview.signal.failedDetail":
@@ -1083,7 +1083,7 @@ export const en: MessageDict = {
"OAuth, EPREL, Pinecone, Stripe, feeds",
"admin.settings.tenantVsPlatform": "Tenant vs platform",
"admin.settings.hostSecrets":
- "Server database, session, and signing secrets — configure on the host only",
+ "Server database, session, and signing secrets; configure on the host only",
"admin.settings.linkAi": "AI",
"admin.settings.linkEmail": "Email",
"admin.settings.linkBilling": "Billing",
@@ -1122,7 +1122,7 @@ export const en: MessageDict = {
"admin.settings.priceScaleYr": "Scale yearly price ID",
"admin.settings.creditPackPrices": "AI credit pack price IDs (one-time)",
"admin.settings.creditPackPricesHint":
- "Creates/updates Stripe Products + one-time Prices using the configured secret key (sk_test_ = test Dashboard, sk_live_ = live). Mock must be off. Price IDs are written into the fields below — Save Integrations if you also change other Stripe values.",
+ "Creates/updates Stripe Products + one-time Prices using the configured secret key (sk_test_ = test Dashboard, sk_live_ = live). Mock must be off. Price IDs are written into the fields below. Save Integrations if you also change other Stripe values.",
"admin.settings.syncCreditPacks": "Sync Stripe packs",
"admin.settings.syncCreditPacksOk":
"Synced {count} credit pack price(s) ({mode} mode).",
@@ -1183,7 +1183,7 @@ export const en: MessageDict = {
"admin.stuck.filterAria": "Filter jobs by status",
"admin.orphan.title": "Orphan processed",
"admin.orphan.description":
- "Cutover ops #7 — report stale catalog rows first (dry-run), then confirm delete only if needed.",
+ "Cutover ops #7: report stale catalog rows first (dry-run), then confirm delete only if needed.",
"admin.orphan.cutoverHint":
"Safe by default: refresh and dry-run never delete. Confirm delete sends confirm=true only after you approve the dialog.",
"admin.orphan.refreshReport": "Refresh report",
@@ -1201,7 +1201,7 @@ export const en: MessageDict = {
"admin.orphan.cleanBadge": "Clean",
"admin.orphan.needsCleanupBadge": "Needs cleanup",
"admin.orphan.noneFound":
- "No orphan processed rows — cutover ops #7 report is clean.",
+ "No orphan processed rows. The cutover ops #7 report is clean.",
"admin.orphan.noSamples": "No sample rows in this report.",
"admin.orphan.colProcessed": "Processed ID",
"admin.orphan.colCompany": "Company",
@@ -1236,7 +1236,7 @@ export const en: MessageDict = {
"admin.knowledge.noMatchingArticles": "No matching articles",
"admin.knowledge.noArticles": "No knowledge articles",
"admin.knowledge.emptyCategoryMsg":
- 'Category "{category}" is ready — add an article shell for a content agent to fill.',
+ 'Category "{category}" is ready. Add an article shell for a content agent to fill.',
"admin.knowledge.emptyArticlesMsg":
"Add FAQ-style shells with categories and keywords. Leave body placeholders for content agents.",
"admin.knowledge.col.title": "Title",
@@ -1279,7 +1279,7 @@ export const en: MessageDict = {
"admin.plans.optionLabel": "{name} ({credits} credits) · {kind}",
"admin.permissions.noteStrong": "Plan entitlements only.",
"admin.permissions.noteBody":
- "These toggles set what this package may include. Platform-wide on/off masters are on the {global} tab — a plan can enable a feature that still stays off when the platform switch is disabled.",
+ "These toggles set what this package may include. Platform-wide on/off masters are on the {global} tab. A plan can enable a feature that still stays off when the platform switch is disabled.",
"admin.permissions.globalTab": "Global switches",
"admin.permissions.title": "Plan permissions",
"admin.permissions.descCount": "{enabled}/{total} enabled",
@@ -1340,10 +1340,9 @@ export const en: MessageDict = {
"Migrated legacy navigation: catalog, feeds, billing, and settings; no Background Tasks, stores, or marketing.",
"admin.profile.free.label": "Free",
"admin.profile.free.description":
- "Public Free ladder — AI, API keys, live email, and own-key AI off.",
+ "Public Free ladder: AI, API keys, live email, and own-key AI off.",
"admin.profile.starter.label": "Starter",
- "admin.profile.starter.description":
- "Public Starter — AI on; own-key AI off.",
+ "admin.profile.starter.description": "Public Starter: AI on; own-key AI off.",
"admin.profile.growth.label": "Growth",
"admin.profile.growth.description": "All catalog features on.",
"admin.profile.business.label": "Business",
@@ -1407,7 +1406,7 @@ export const en: MessageDict = {
"admin.billing.runCycles": "Run due billing cycles",
"admin.billing.companiesTitle": "Companies",
"admin.billing.companiesDesc":
- "Credit balances and plan status — assign plans or adjust credits.",
+ "Credit balances and plan status. Assign plans or adjust credits.",
"admin.billing.searchCompaniesAria": "Search companies",
"admin.billing.planStatusAria": "Plan status",
"admin.billing.filter.all": "All companies",
@@ -1519,7 +1518,7 @@ export const en: MessageDict = {
"admin.knowledge.active": "Active",
"admin.settings.card.aiRolesTitle": "Platform AI roles",
"admin.settings.card.aiRolesDesc":
- "Processing, vectorization, docs/API, and support — separate keys/models",
+ "Processing, vectorization, docs/API, and support with separate keys/models",
"admin.settings.roleOff": " (off)",
"admin.settings.roleNotSet": " · not set",
"admin.settings.someRolesUnavailable":
@@ -1542,9 +1541,9 @@ export const en: MessageDict = {
"admin.settings.stripeSet": "Stripe set",
"admin.settings.stripeOff": "Stripe off",
"admin.settings.password": "Password",
- "admin.settings.passwordSavedHint": "(saved — leave blank to keep)",
+ "admin.settings.passwordSavedHint": "(saved; leave blank to keep)",
"admin.settings.secretKey": "Secret key",
- "admin.settings.secretConfiguredHint": "(configured — leave blank to keep)",
+ "admin.settings.secretConfiguredHint": "(configured; leave blank to keep)",
"admin.settings.passwordConfigured": "Password configured",
"admin.settings.noSmtpPassword": "No SMTP password",
"admin.settings.enabled": "Enabled",
@@ -1626,7 +1625,7 @@ export const en: MessageDict = {
"admin.support.disableAuto": "Disable auto for this ticket",
"admin.support.aiDraftTitle": "AI draft (approve or edit)",
"admin.support.aiDraftDesc":
- "Draft-only mode left an internal note. Edit if needed, then send to the customer — or discard and take over manually.",
+ "Draft-only mode left an internal note. Edit if needed, then send to the customer, or discard it and take over manually.",
"admin.support.confidence": "Confidence {value}",
"admin.support.statusAfterSend": "Status after send",
"admin.support.approveSend": "Approve & send",
@@ -1674,8 +1673,8 @@ export const en: MessageDict = {
"admin.settings.updatedAt": "Updated {when}",
"admin.settings.tenantDesc":
"Company AI keys and marketing email stay under Integrations. Session and encryption secrets stay in the server environment and are not edited here.",
- "admin.settings.perCompanyAiEmail": "Per-company AI/email —",
- "admin.settings.plansCredits": "Plans and credits —",
+ "admin.settings.perCompanyAiEmail": "Per-company AI/email:",
+ "admin.settings.plansCredits": "Plans and credits:",
"admin.settings.fromDatabase": "From database",
"admin.settings.fromEnvironment": "From environment",
"admin.settings.sourceBadge": "Source: {source}",
@@ -1699,11 +1698,11 @@ export const en: MessageDict = {
"admin.settings.storedSecret": "Stored secret: {masked}",
"admin.settings.clearClientSecret": "Clear stored client secret",
"admin.settings.eprelDesc":
- "Free public EU energy-label data during product processing. On by default for all plans — no activation or paid upgrade required. Use the controls below only to disable or tune the client.",
+ "Free public EU energy-label data during product processing. It is on by default for all plans, with no activation or paid upgrade required. Use the controls below only to disable or tune the client.",
"admin.settings.optionalKeySaved": "Optional API key saved",
"admin.settings.noApiKeyNeeded": "No API key needed",
"admin.settings.optionalApiKey": "Optional API key",
- "admin.settings.optionalApiKeySavedHint": "(saved — leave blank to keep)",
+ "admin.settings.optionalApiKeySavedHint": "(saved; leave blank to keep)",
"admin.settings.optionalApiKeyPublicHint": "(public API works without one)",
"admin.settings.clearEprelKey": "Clear stored EPREL API key",
"admin.settings.pineconeDesc":
@@ -1715,7 +1714,7 @@ export const en: MessageDict = {
"admin.settings.stripeDesc":
"Optional overrides of server Stripe configuration. Prefer password fields; values are never re-shown after save.",
"admin.settings.stripeMock":
- "Force Stripe mock mode (non-production only — disables real charges)",
+ "Force Stripe mock mode (non-production only; disables real charges)",
"admin.settings.webhookSecret": "Webhook secret",
"admin.settings.clearStripeSecret": "Clear Stripe secret key",
"admin.settings.clearStripeWebhook": "Clear Stripe webhook secret",
@@ -1735,7 +1734,7 @@ export const en: MessageDict = {
"admin.settings.sendFailed": "Send failed",
"admin.settings.saveMailFailed": "Could not save mail settings",
"admin.settings.smtpNotConfigured":
- "SMTP is not configured — enable and save mail settings first.",
+ "SMTP is not configured. Enable and save mail settings first.",
"admin.settings.smtpTestFailed": "SMTP test failed",
"admin.settings.testEmailFailed": "Could not send test email",
"admin.settings.saveIntegrationFailed": "Could not save integration settings",
@@ -1744,7 +1743,7 @@ export const en: MessageDict = {
"admin.settings.aiSaveFailed": "Could not save {label} AI settings",
"admin.settings.aiTestOk": "{label}: connection ok",
"admin.settings.aiTestSkipped":
- "{label}: skipped — save credentials (and enable the role) to test.",
+ "{label}: skipped. Save credentials (and enable the role) to test.",
"admin.settings.aiTestFailed": "{label}: connection failed",
"admin.settings.aiTestError": "Could not test {label} connection",
"admin.settings.aiRole.processing.label": "Processing",
@@ -1755,10 +1754,10 @@ export const en: MessageDict = {
"Embeddings for search / Pinecone indexing (match index dimensions).",
"admin.settings.aiRole.docs_api.label": "Docs / API",
"admin.settings.aiRole.docs_api.desc":
- "Future docs/API assistant slot — /docs Ask stays rule-based and must never call this role.",
+ "Future docs/API assistant slot. /docs Ask stays rule-based and must never call this role.",
"admin.settings.aiRole.support.label": "Support",
"admin.settings.aiRole.support.desc":
- "Ticket auto-reply AI fallback — configure provider/key/model here; enable delivery in Support knowledge → Auto-reply.",
+ "Ticket auto-reply AI fallback. Configure the provider/key/model here; enable delivery in Support knowledge → Auto-reply.",
"admin.settings.providerOption.openai": "OpenAI",
"admin.settings.providerOption.openrouter": "OpenRouter",
"admin.settings.providerOption.azure": "Azure OpenAI",
@@ -1766,7 +1765,7 @@ export const en: MessageDict = {
"admin.settings.providerOption.custom": "Custom / other",
"admin.support.title": "Support Center",
"admin.support.description":
- "Staff inbox — claim from the queue, filter by scope, open threads, and resolve",
+ "Staff inbox: claim from the queue, filter by scope, open threads, and resolve",
"admin.support.unavailable":
"Support queue isn’t available. Contact your platform administrator if this persists.",
"admin.support.noTickets": "No tickets in this queue.",
@@ -1786,11 +1785,11 @@ export const en: MessageDict = {
"admin.knowledge.eyebrow": "Platform admin",
"admin.knowledge.title": "Support knowledge",
"admin.knowledge.description":
- "Structure help articles for FAQ auto-match. Content agents fill bodies — keep facts accurate.",
+ "Structure help articles for FAQ auto-match. Content agents fill bodies, so keep facts accurate.",
"admin.knowledge.notReady": "Support knowledge not ready",
"admin.knowledge.noTemplates": "No reply templates",
"admin.knowledge.articleDesc":
- "Markdown body supports images via secure upload. Do not invent product facts — leave placeholders for content agents.",
+ "Markdown body supports images via secure upload. Do not invent product facts; leave placeholders for content agents.",
"admin.knowledge.templateDesc":
"Allowlisted placeholders: subject, category.",
"admin.knowledge.searchPlaceholder": "Search title, slug, keywords…",
@@ -1914,7 +1913,7 @@ export const en: MessageDict = {
"feeds.title": "Your Feeds",
"feeds.loadingDesc": "Loading your supplier feeds…",
"feeds.desc":
- "{page} on this page · {total} total feeds — supplier CSV/XML URLs and uploads. Connect from Stores for guided options.",
+ "{page} on this page · {total} total feeds from supplier CSV/XML URLs and uploads. Connect from Stores for guided options.",
"feeds.storesHub": "Stores hub",
"feeds.addFeed": "Add Feed",
"feeds.searchPlaceholder": "Search feeds...",
@@ -1936,7 +1935,7 @@ export const en: MessageDict = {
"feeds.mappingsSaved": "Field mappings saved",
"feeds.mapped": "Mapped",
"feeds.notMapped": "Not mapped",
- "feeds.notMappedTitle": "No field mappings yet — open Map to configure",
+ "feeds.notMappedTitle": "No field mappings yet. Open Map to configure",
"feeds.productsFromFeed": "{count} products from this feed",
"feeds.noProductsYet": "No products imported from this feed yet",
"feeds.syncing": "Syncing…",
@@ -1963,10 +1962,10 @@ export const en: MessageDict = {
"feeds.field.type": "Type",
"feeds.field.file": "CSV file",
"feeds.fileHint":
- "Max 5 MiB. After create, map columns then sync — same flow as URL feeds.",
+ "Max 5 MiB. After creating the feed, map columns and then sync, just as with URL feeds.",
"feeds.field.interval": "Auto-sync interval (minutes)",
"feeds.field.intervalHint":
- "How often Descrybe should pull this feed automatically while it is active. Use Sync on the feed list for a one-off pull — that does not change this schedule.",
+ "How often Descrybe should pull this feed automatically while it is active. Use Sync on the feed list for a one-off pull; that does not change this schedule.",
"feeds.createFeed": "Create Feed",
"feeds.editTitle": "Edit Feed",
"feeds.editDesc":
@@ -2033,10 +2032,9 @@ export const en: MessageDict = {
"feeds.stats.totalProducts": "Imported products",
"feeds.stats.acrossFeeds": "Raw rows across feeds (not catalog + queue)",
"feeds.stats.processed": "In catalog",
- "feeds.stats.readyCatalog":
- "Processed catalog rows — not a share of imported",
+ "feeds.stats.readyCatalog": "Processed catalog rows, not a share of imported",
"feeds.stats.unprocessed": "Awaiting process",
- "feeds.stats.waiting": "Raw queue — not a share of imported",
+ "feeds.stats.waiting": "Raw queue, not a share of imported",
"feeds.format.howTo": "How to structure your feed",
"feeds.format.required": "Required:",
"feeds.preview.title": "Feed source preview",
@@ -2103,7 +2101,7 @@ export const en: MessageDict = {
"mapping.requiredFields": "Required fields",
"mapping.requiredMapped": "{mapped} of {total} mapped",
"mapping.requiredProgressDone":
- "All required fields mapped — ready for the next step.",
+ "All required fields are mapped and ready for the next step.",
"mapping.requiredProgressHint":
"Fields marked * must be mapped before this feed can sync products.",
"mapping.requiredProgressAria":
@@ -2139,7 +2137,7 @@ export const en: MessageDict = {
"mapping.missingRequiredTitle":
"Still need to connect these required fields:",
"mapping.applyTip":
- "Tip: Apply suggestions above — many required fields match common source names.",
+ "Tip: Apply the suggestions above because many required fields match common source names.",
"mapping.formatLabel": "Format:",
"mapping.itemPathLabel": "Item path:",
"mapping.reExtract": "Re-extract",
@@ -2160,7 +2158,7 @@ export const en: MessageDict = {
"mapping.noExactAlias":
"No exact/alias suggestions to apply. Review fuzzy matches below.",
"mapping.noSuggestionsExtract":
- "No suggestions available — extract the schema first.",
+ "No suggestions available. Extract the schema first.",
"mapping.schemaExtracted":
"Schema extracted: {fields} fields from {rows} sample row(s).",
"mapping.saved": "Field mappings saved{version}.",
@@ -2173,9 +2171,9 @@ export const en: MessageDict = {
"mapping.kindHighConfidence": "high-confidence",
"mapping.kindFuzzy": "fuzzy",
"mapping.fuzzyConfirmFlash":
- "{count} fuzzy match need confirm — use Confirm fuzzy below or per-row chips.",
+ "{count} fuzzy match needs confirmation. Use Confirm fuzzy below or the per-row chips.",
"mapping.fuzzyConfirmFlashPlural":
- "{count} fuzzy matches need confirm — use Confirm fuzzy below or per-row chips.",
+ "{count} fuzzy matches need confirmation. Use Confirm fuzzy below or the per-row chips.",
"mapping.formatHelp.title": "How to structure your feed",
"mapping.formatHelp.required": "Required:",
"mapping.formatHelp.requiredBody":
@@ -2185,7 +2183,7 @@ export const en: MessageDict = {
"mapping.formatHelp.specs":
"Specs as Label: value pairs (e.g. {ram}). Dimensions: {height}, {width}, {depth}, {mass}",
"mapping.formatHelp.also":
- "Also supported: prices, stock, images, category, warranty, EPREL ID, HTML descriptions, extra images, video URL, and more — download the full sample.",
+ "Also supported: prices, stock, images, category, warranty, EPREL ID, HTML descriptions, extra images, video URL, and more. Download the full sample.",
"mapping.formatHelp.downloadXml": "Download sample XML",
"mapping.formatHelp.downloadCsv": "Download sample CSV",
"mapping.formatHelp.xmlExample": "XML example",
@@ -2237,7 +2235,7 @@ export const en: MessageDict = {
"mapping.preview.rawSample": "Raw sample snippet",
"imports.title": "Uploads",
"imports.description":
- "CSV imports saved for this workspace — products, categories, and attributes.",
+ "CSV imports saved for this workspace include products, categories, and attributes.",
"imports.importProducts": "Import products",
"imports.emptyTitle": "No uploads yet",
"imports.emptyMessage":
@@ -2254,7 +2252,7 @@ export const en: MessageDict = {
"imports.actions": "Upload actions",
"imports.loadFailed": "Failed to load uploads",
"imports.metadataOnlyNotice":
- "{count} upload records are metadata-only — file bytes were not migrated. Re-upload or resync storage if you need the originals.",
+ "{count} upload records are metadata-only because file bytes were not migrated. Re-upload or resync storage if you need the originals.",
"imports.metadataOnlyBadge": "Metadata only (blob not migrated)",
"imports.deleteConfirm":
"Delete “{name}”? This removes the upload record (and local blob when present).",
@@ -2265,7 +2263,7 @@ export const en: MessageDict = {
"reviews.tab": "Reviews",
"reviews.syncTitle": "Reviews import (store → Descrybe)",
"reviews.syncDesc":
- "Inbound import — pull WooCommerce product reviews into Descrybe for campaign and reputation workflows",
+ "Inbound import: pull WooCommerce product reviews into Descrybe for campaign and reputation workflows",
"reviews.syncQueued": "Reviews sync queued",
"reviews.statusBadge": "Reviews: {status}",
"reviews.syncedCount": "{count} synced",
@@ -2290,7 +2288,7 @@ export const en: MessageDict = {
"reviews.queueFailed": "Could not queue reviews sync",
"campaigns.title": "Campaigns",
"campaigns.description":
- "Build seasonal email campaigns from your catalog — choose a season, pick products, set the audience, then generate.",
+ "Build seasonal email campaigns from your catalog. Choose a season, pick products, set the audience, then generate.",
"campaigns.new": "New campaign",
"campaigns.loading": "Loading campaigns…",
"campaigns.loadFailed": "Failed to load campaigns",
@@ -2309,7 +2307,7 @@ export const en: MessageDict = {
"campaigns.emptyTitle": "No campaigns yet",
"campaigns.noMatches": "No matches",
"campaigns.emptyMessage":
- "Create a Black Friday, Christmas, or custom campaign in a few steps — seasonal defaults are ready to start from.",
+ "Create a Black Friday, Christmas, or custom campaign in a few steps. Seasonal defaults are ready to use.",
"campaigns.noMatchesMessage": "Try a different name, season, or status.",
"campaigns.create": "Create campaign",
"campaigns.col.season": "Season",
@@ -2320,7 +2318,7 @@ export const en: MessageDict = {
"Choose a season, select products and audience, write the prompt, then generate, test, and schedule.",
"campaigns.editTitle": "Edit campaign",
"campaigns.editDesc":
- "Update the season, products, and audience — then regenerate, send a test, or schedule.",
+ "Update the season, products, and audience, then regenerate, send a test, or schedule.",
"campaignWizard.loading": "Loading campaign wizard…",
"campaignWizard.apiMissing":
"Campaigns API is temporarily unavailable. You can explore the wizard; save / generate / send need a reachable /api/campaigns.",
@@ -2336,14 +2334,14 @@ export const en: MessageDict = {
"campaignWizard.step.send": "Send",
"campaignWizard.pickSeason": "Pick a season",
"campaignWizard.pickSeasonHelp":
- "Start from a template — you can edit everything later.",
+ "Start from a template. You can edit everything later.",
"campaignWizard.name": "Campaign name",
"campaignWizard.namePlaceholder": "Black Friday 2026",
"campaignWizard.catalogTitle": "Categories & products",
"campaignWizard.catalogHelp":
"Select what this email should feature. Categories alone are enough for a quick start.",
"campaignWizard.categories": "Categories",
- "campaignWizard.noCategories": "No categories yet — add some under Catalog.",
+ "campaignWizard.noCategories": "No categories yet. Add some under Catalog.",
"campaignWizard.categoriesSelected": "{count} category selected",
"campaignWizard.categoriesSelectedPlural": "{count} categories selected",
"campaignWizard.productsOptional": "Products (optional)",
@@ -2364,7 +2362,7 @@ export const en: MessageDict = {
"Customers who bought products in the audience categories (from synced orders).",
"campaignWizard.audience.notPurchased": "Not purchased yet",
"campaignWizard.audience.notPurchasedHint":
- "Contacts who have not bought those categories — great for win-back.",
+ "Contacts who have not bought those categories, ideal for win-back.",
"campaignWizard.ordersHint":
"Purchased / not-purchased targeting appears after WooCommerce orders are synced.",
"campaignWizard.audienceCategories": "Audience categories",
@@ -2380,7 +2378,7 @@ export const en: MessageDict = {
"campaignWizard.planFree": "Free",
"campaignWizard.planPaid": "Paid",
"campaignWizard.planLine": "Plan:",
- "campaignWizard.aiLocked": "— Generate AI is locked.",
+ "campaignWizard.aiLocked": "Generate AI is locked.",
"campaignWizard.generateAi": "Generate AI",
"campaignWizard.templatePreview": "Template preview",
"campaignWizard.saveDraft": "Save draft",
@@ -2443,7 +2441,7 @@ export const en: MessageDict = {
"marketing.preparing": "Preparing…",
"marketing.prepareNamed": "Prepare {name} campaign",
"marketing.footer":
- "Email campaigns stay at {link} — this page only prepares catalog export feeds.",
+ "Email campaigns stay at {link}. This page only prepares catalog export feeds.",
"common.done": "Done",
"common.copied": "Copied to clipboard.",
"common.copyFailed": "Could not copy. Select the text and copy manually.",
@@ -2487,18 +2485,18 @@ export const en: MessageDict = {
"settings.emailAlertsBody":
"For now, configure transactional mail under Company → Email integration. In-app alerts above still apply.",
"settings.apiKeysLegacyNotice":
- "Legacy API keys were not migrated. Create a new key to restore API access — the secret is shown only once.",
+ "Legacy API keys were not migrated. Create a new key to restore API access. The secret is shown only once.",
"settings.apiKeysAdminOnly":
"Only company admins can create, rename, or revoke API keys. You can view existing key prefixes.",
"settings.apiKeysCopyUnavailableNotice":
- "After you create a key, copy the full secret immediately (or from the row menu while this page stays open). Existing keys only store a prefix — create a new key if you lost the secret.",
+ "After you create a key, copy the full secret immediately (or from the row menu while this page stays open). Existing keys only store a prefix, so create a new key if you lost the secret.",
"settings.apiKeysForbidden":
"You don't have permission to view API keys. Ask a company admin for help.",
"settings.noApiKeys": "No API keys yet",
"settings.noApiKeysHelpAdmin":
"Legacy API keys were not migrated. Create a new key to authenticate Descrybe API requests (X-API-Key). The full secret is shown only once.",
"settings.noApiKeysHelpMember":
- "No API keys for this company yet. Legacy keys were not migrated — ask a company admin to create one.",
+ "No API keys for this company yet. Legacy keys were not migrated. Ask a company admin to create one.",
"settings.unnamedKey": "Unnamed Key",
"settings.neverUsed": "Never used",
"settings.apiKeyActions": "API key actions",
@@ -2525,7 +2523,7 @@ export const en: MessageDict = {
"settings.apiKeyRevokeFailed": "Could not revoke API key",
"settings.apiKeyCopied": "API key copied.",
"settings.createApiKeyOnceHelp":
- "Copy this key now — it won't be shown again. Use it as Authorization: Bearer or X-API-Key on /api/v1.",
+ "Copy this key now because it won't be shown again. Use it as Authorization: Bearer or X-API-Key on /api/v1.",
"settings.createApiKeyNameHelp":
"Name the key so you can tell it apart later. After create, send it as Authorization: Bearer (preferred) or X-API-Key.",
"settings.keyNamePlaceholder": "e.g. Production",
@@ -2707,10 +2705,10 @@ export const en: MessageDict = {
"flash.settings.copyFailed":
"Could not copy. Select the text and copy manually.",
"flash.products.accepted":
- "Accepted {count} product(s) — moved to Processed.",
+ "Accepted {count} product(s) and moved them to Processed.",
"flash.products.updated": "Product updated.",
"flash.products.enrichmentAccepted":
- "Enrichment accepted — moved to Processed.",
+ "Enrichment accepted and moved to Processed.",
"flash.products.originalEmpty":
"Original value is empty; enrichment cannot be cleared via PATCH.",
"flash.products.selectExport": "Select at least one product to export.",
@@ -2745,11 +2743,11 @@ export const en: MessageDict = {
"flash.export.selectFormat": "Please select an export feed.",
"flash.files.deleted": "Upload deleted.",
"flash.mapping.noSuggestions":
- "No suggestions available — check source field names.",
+ "No suggestions available. Check the source field names.",
"flash.mapping.allMapped":
"All suggestions are already mapped. Review confidence chips in the table.",
"flash.mapping.extractFirst":
- "No suggestions available — extract the schema first.",
+ "No suggestions available. Extract the schema first.",
"flash.mapping.pathRequired":
"Enter a product element path before mapping fields.",
"flash.billing.checkoutCanceled":
@@ -2774,8 +2772,7 @@ export const en: MessageDict = {
"flash.ai.connectionOk": "Connection OK ({detail}).",
"flash.ai.connectionByok": ", your own key",
"flash.email.saved": "Email provider saved. Verify domain, then send a test.",
- "flash.email.testDryRun":
- "Test recorded in dry-run mode — no email was sent.",
+ "flash.email.testDryRun": "Test recorded in dry-run mode. No email was sent.",
"flash.email.testSent":
"Test email sent. The from address is marked verified when delivery succeeds.",
"flash.fields.apiUnavailable":
@@ -2822,7 +2819,7 @@ export const en: MessageDict = {
"flash.admin.cleanup":
"Cleanup: {jobs} jobs failed, {products} products reset.",
"flash.admin.orphanDryRun":
- "Dry-run only — {total} orphan(s) would be deleted. Nothing removed.",
+ "Dry-run only: {total} orphan(s) would be deleted. Nothing was removed.",
"flash.admin.orphanDeleted":
"Deleted {deleted} orphan row(s). Remaining: {remaining}.",
@@ -2841,7 +2838,7 @@ export const en: MessageDict = {
"flash.admin.featuresAllOff": "All features disabled for this plan.",
"flash.admin.profileApplied": "Applied {label} profile to {name}.",
"flash.admin.overridesCleared":
- "Cleared overrides — using plan-name defaults.",
+ "Cleared overrides. Using plan-name defaults.",
"empty.plans.none": "No plans found.",
"empty.plans.noMatch": "No plans match this filter.",
"empty.plans.selectPackage": "Select a package to edit features.",
@@ -2859,7 +2856,7 @@ export const en: MessageDict = {
"empty.products.filterTitleUnprocessed": "No unprocessed matches",
"empty.products.filterTitle": "No products match",
"empty.products.filterMessage":
- "Try a different search, category, or feed — or clear filters to see everything.",
+ "Try a different search, category, or feed, or clear filters to see everything.",
"empty.products.unprocessedTitle": "No unprocessed products yet",
"empty.products.unprocessedMessage":
"Connect a supplier feed or upload a CSV to import products into your catalog.",
@@ -2887,12 +2884,12 @@ export const en: MessageDict = {
"Something went wrong loading your exports. Retry, or check the error above for details.",
"empty.export.noneTitle": "No export feeds yet",
"empty.export.noneMessage":
- "Create a Google Shopping, Meta, or custom CSV/XML feed to choose which products and fields go out. Generate a public URL for Merchant Center or any partner — or export a selected subset from Products.",
+ "Create a Google Shopping, Meta, or custom CSV/XML feed to choose which products and fields go out. Generate a public URL for Merchant Center or any partner, or export a selected subset from Products.",
"empty.export.noMatchTitle": "No matching feeds",
"empty.export.noMatchMessage": "Try a different search term.",
"empty.files.noneTitle": "No uploads yet",
"empty.files.noneMessage":
- "CSV imports for products, categories, and attributes appear here after you upload them. After a platform migration, file contents (blobs) were not copied — re-upload if you need those files.",
+ "CSV imports for products, categories, and attributes appear here after you upload them. After a platform migration, file contents (blobs) were not copied. Re-upload if you need those files.",
"empty.files.uploadCta": "Upload CSV",
"empty.export.fromProductsCta": "Export from Products",
"empty.dashboard.noJobsTitle": "No recent jobs",
@@ -2927,7 +2924,7 @@ export const en: MessageDict = {
"This API isn’t on this backend yet. You can’t manage Export ID fields here until /api/structured-descriptions is wired.",
"empty.structured.noneTitle": "No structured description fields yet",
"empty.structured.noneMessage":
- "Add a field here, or create Export IDs in a category description formula — they’ll show up for use in export feeds.",
+ "Add a field here, or create Export IDs in a category description formula. They’ll show up for use in export feeds.",
"empty.support.unavailableTitle": "Support unavailable",
"empty.support.noneTitle": "No tickets yet",
"empty.support.unavailableMessage":
@@ -2994,7 +2991,7 @@ export const en: MessageDict = {
"empty.admin.noCompaniesMatchFilter": "No companies match this filter.",
"blast.title": "Confirm email blast",
"blast.dryRunDescription":
- "Dry-run mode is active — no real emails will leave Descrybe. Confirm to record the dry-run send.",
+ "Dry-run mode is active, so no real emails will leave Descrybe. Confirm to record the dry-run send.",
"blast.liveDescription":
"This will send real emails to your audience. Type I understand to continue.",
"blast.recipients": "Recipients in this request: {count}",
@@ -3003,7 +3000,7 @@ export const en: MessageDict = {
"blast.runDryRun": "Run dry-run",
"blast.send": "Send blast",
"marketing.footerBefore": "Email campaigns stay at",
- "marketing.footerAfter": "— this page only prepares catalog export feeds.",
+ "marketing.footerAfter": "This page only prepares catalog export feeds.",
"support.title": "Support Center",
"support.description":
"Open a ticket for billing, bugs, or account help. Staff replies show here and as in-app notifications.",
@@ -3050,7 +3047,7 @@ export const en: MessageDict = {
"support.new.category": "Category",
"support.new.priority": "Priority",
"support.new.optional": "Optional details",
- "support.new.optionalHint": "(SKU, tags — helps routing)",
+ "support.new.optionalHint": "(SKU and tags help routing)",
"support.new.relatedSku": "Related SKU",
"support.new.relatedSkuPlaceholder": "e.g. ABC-123",
"support.new.relatedSkuHelp":
@@ -3068,7 +3065,7 @@ export const en: MessageDict = {
"We will reply in this thread. Watch for in-app notifications.",
"support.new.viewTicket": "View ticket",
"support.new.unavailable":
- "Support is temporarily unavailable. Your message was not submitted — try again shortly.",
+ "Support is temporarily unavailable. Your message was not submitted. Try again shortly.",
"support.new.unavailableTitle": "Support unavailable",
"support.new.createFailed": "Could not create ticket",
"support.ticket.fallbackTitle": "Support ticket",
@@ -3116,7 +3113,7 @@ export const en: MessageDict = {
"We believe this is sorted. Reply to reopen, or leave a quick rating below.",
"support.progress.quickAnswer.title": "Quick answer sent",
"support.progress.quickAnswer.message":
- "We shared an automated reply below. If it does not solve it, reply here — a human will take over.",
+ "We shared an automated reply below. If it does not solve the issue, reply here and a human will take over.",
"support.progress.onIt.title": "We're on it",
"support.progress.onIt.handedMessage":
"A support teammate has your ticket and will reply in this thread.",
@@ -3124,7 +3121,7 @@ export const en: MessageDict = {
"Support is reviewing your message. You will see updates in this thread.",
"support.progress.received.title": "Ticket received",
"support.progress.received.message":
- "Thanks — we have your request. Automated help may appear shortly, or a teammate will reply.",
+ "Thanks, we have your request. Automated help may appear shortly, or a teammate will reply.",
"support.category.billing": "Billing",
"support.category.billing.help":
"Invoices, payment methods, plan changes, and Stripe receipts.",
@@ -3148,18 +3145,18 @@ export const en: MessageDict = {
"Export feeds, channels, and sync destinations.",
"support.category.migration": "Migration / data",
"support.category.migration.help":
- "Missing or wrong data after cutover — products, feeds, stores, billing, or team.",
+ "Missing or wrong data after cutover, such as products, feeds, stores, billing, or team.",
"support.category.other": "Other",
"support.category.other.help":
- "Anything else — we will route it to the right teammate.",
+ "Anything else. We will route it to the right teammate.",
"support.priority.low": "Low",
"support.priority.low.help":
- "Nice-to-have or non-blocking — we will get to it in turn.",
+ "Nice-to-have or non-blocking. We will get to it in turn.",
"support.priority.normal": "Normal",
"support.priority.normal.help": "Standard request. Most tickets use this.",
"support.priority.high": "High",
"support.priority.high.help":
- "Blocking work or revenue impact — use sparingly.",
+ "Blocking work or affecting revenue. Use sparingly.",
"support.csat.thanks": "Thanks for your feedback",
"support.csat.unavailable":
"Rating is temporarily unavailable. Please try again later.",
@@ -3173,7 +3170,7 @@ export const en: MessageDict = {
"support.csat.formHelp":
"This ticket is {status}. Rate your experience (1–5). Optional comment helps us improve.",
"support.csat.rating": "Rating",
- "support.csat.scoreAria": "{value} — {label}",
+ "support.csat.scoreAria": "{value}: {label}",
"support.csat.whatWentWrong": "What went wrong? (optional)",
"support.csat.whatWentWrongPlaceholder": "A short note helps us follow up.",
"support.csat.anythingElse": "Anything else? (optional)",
@@ -3269,7 +3266,7 @@ export const en: MessageDict = {
"products.empty.noUnprocessedMatches": "No unprocessed matches",
"products.empty.noMatch": "No products match",
"products.empty.filtersHint":
- "Try a different search, category, or feed — or clear filters to see everything.",
+ "Try a different search, category, or feed, or clear filters to see everything.",
"products.empty.noUnprocessed.title": "No unprocessed products yet",
"products.empty.noUnprocessed.message":
"Connect a supplier feed or upload a CSV to import products into your catalog.",
@@ -3310,7 +3307,7 @@ export const en: MessageDict = {
"products.processing.justCancelled": "The just-started job was cancelled.",
"products.processing.viewInProcessing": "View in Processing",
"products.processing.confirm": "Confirm processing",
- "products.processing.freePlanTitle": "Free plan — processing without AI",
+ "products.processing.freePlanTitle": "Free plan: processing without AI",
"products.processing.freePlanBody":
"Normalize, specs, fill, and EPREL run at no credit cost. AI titles/descriptions require a paid plan or credits.",
"products.enrichment.acceptTitle": "Accept enrichment?",
@@ -3324,9 +3321,9 @@ export const en: MessageDict = {
"products.enrichment.resetConfirm": "Confirm reset to unprocessed",
"products.enrichment.resetSelected":
"Reset {count} selected product(s) to unprocessed? They will reappear as unprocessed items.",
- "products.enrichment.accepted": "Enrichment accepted — moved to Processed.",
+ "products.enrichment.accepted": "Enrichment accepted and moved to Processed.",
"products.enrichment.rejected":
- "Enrichment rejected — product returned to unprocessed.",
+ "Enrichment rejected. The product was returned to unprocessed.",
"products.enrichment.acceptFailed": "Could not accept enrichment",
"products.enrichment.rejectFailed": "Could not reject enrichment",
"products.enrichment.acceptUndone": "Accept undone",
@@ -3367,8 +3364,8 @@ export const en: MessageDict = {
"products.enrichment.help.category":
"Assigned category. Green = category set; gray = missing.",
"products.enrichment.chipTitle": "{label}: {state}. {help}",
- "products.enrichment.complete": "Complete — {parts}",
- "products.enrichment.missingSummary": "Missing {missing} — {parts}",
+ "products.enrichment.complete": "Complete: {parts}",
+ "products.enrichment.missingSummary": "Missing {missing}: {parts}",
"products.issue.title": "title",
"products.issue.gtin": "GTIN",
"products.issue.category": "category",
@@ -3430,13 +3427,13 @@ export const en: MessageDict = {
"attributes.csvNeedColumns":
'CSV must include "category_id" and "attribute_key" columns',
"attributes.type.string": "Text",
- "attributes.type.stringHint": "Free text — names, models, short notes",
+ "attributes.type.stringHint": "Free text for names, models, and short notes",
"attributes.type.number": "Number",
"attributes.type.numberHint":
"Numeric value; add a unit when useful (cm, kg, W)",
"attributes.type.boolean": "Yes / No",
"attributes.type.booleanHint":
- "True or false — e.g. has GPS, wireless charging",
+ "True or false, e.g. has GPS or wireless charging",
"attributes.type.date": "Date",
"attributes.type.dateHint": "A calendar date (release, warranty end, …)",
"attributes.type.list": "Dropdown",
@@ -3627,7 +3624,7 @@ export const en: MessageDict = {
"fields.deleteGroupConfirm":
"This action cannot be undone. This will permanently delete the group. Any fields in this group will need to be reassigned.",
"fields.type.string": "Text",
- "fields.type.stringHint": "Free text — names, codes, short notes",
+ "fields.type.stringHint": "Free text for names, codes, and short notes",
"fields.type.number": "Number",
"fields.type.numberHint": "Numeric value; add a unit when useful (EUR, pcs)",
"fields.type.boolean": "Yes / No",
@@ -3811,13 +3808,13 @@ export const en: MessageDict = {
"attributes.assigning": "Assigning…",
"attributes.uploadAndAssign": "Upload & Assign",
"attributes.type.string.label": "Text",
- "attributes.type.string.hint": "Free text — names, models, short notes",
+ "attributes.type.string.hint": "Free text for names, models, and short notes",
"attributes.type.number.label": "Number",
"attributes.type.number.hint":
"Numeric value; add a unit when useful (cm, kg, W)",
"attributes.type.boolean.label": "Yes / No",
"attributes.type.boolean.hint":
- "True or false — e.g. has GPS, wireless charging",
+ "True or false, e.g. has GPS or wireless charging",
"attributes.type.date.label": "Date",
"attributes.type.date.hint": "A calendar date (release, warranty end, …)",
"attributes.type.list.label": "Dropdown",
@@ -3836,8 +3833,8 @@ export const en: MessageDict = {
"standardFields.tab.groups": "Groups",
"standardFields.heading": "Standard Fields",
"standardFields.headingHint":
- "Enable fields for mapping and processing — one toggle per row.",
- "standardFields.enableRecommended": "Enable recommended",
+ "Enable fields for mapping and processing, with one toggle per row.",
+ "standardFields.enableRecommended": "Enable recommended fields",
"standardFields.enableSelected": "Enable selected ({count})",
"standardFields.disableSelected": "Disable selected",
"standardFields.addField": "Add Field",
@@ -3941,7 +3938,8 @@ export const en: MessageDict = {
"standardFields.deleteGroupDesc":
"This action cannot be undone. This will permanently delete the group. Any fields in this group will need to be reassigned.",
"standardFields.type.string.label": "Text",
- "standardFields.type.string.hint": "Free text — names, codes, short notes",
+ "standardFields.type.string.hint":
+ "Free text for names, codes, and short notes",
"standardFields.type.number.label": "Number",
"standardFields.type.number.hint":
"Numeric value; add a unit when useful (EUR, pcs)",
@@ -3997,7 +3995,7 @@ export const en: MessageDict = {
"standardFields.fallback.specs": "Specifications",
"standardFields.fallback.specifications": "Specifications (legacy)",
"standardFields.tip.gtin":
- "Global Trade Item Number — barcode/EAN/UPC used to identify the product.",
+ "Global Trade Item Number: the barcode/EAN/UPC used to identify the product.",
"standardFields.tip.title":
"Customer-facing product name shown in catalogs and channels.",
"standardFields.tip.brand": "Manufacturer or brand name (not the retailer).",
@@ -4019,8 +4017,9 @@ export const en: MessageDict = {
"standardFields.tip.product_url":
"Public product page URL on your store or site.",
"standardFields.tip.official_link": "Manufacturer or brand product page URL.",
- "standardFields.tip.sku": "Stock Keeping Unit — your internal item code.",
- "standardFields.tip.mpn": "Manufacturer Part Number — supplier's part code.",
+ "standardFields.tip.sku": "Stock Keeping Unit: your internal item code.",
+ "standardFields.tip.mpn":
+ "Manufacturer Part Number: the supplier's part code.",
"standardFields.tip.category": "Product category or taxonomy path.",
"standardFields.tip.availability":
"In stock / out of stock / preorder style status.",
@@ -4037,7 +4036,7 @@ export const en: MessageDict = {
"Legacy specs field; prefer specs when both exist.",
"export.dialog.title": "Export Selected Products",
"export.dialog.description":
- "Export {count} selected product(s). Pick the export feed that defines format and fields — like choosing what to sync, but for a download or partner feed.",
+ "Export {count} selected product(s). Pick the export feed that defines the format and fields, much like choosing what to sync for a download or partner feed.",
"export.dialog.loadFailed":
"Failed to load export feed options. Please try again.",
"export.dialog.selectPlaceholder": "Select an export feed…",
@@ -4085,7 +4084,7 @@ export const en: MessageDict = {
"Map feeds and clean up product data without AI credits.",
"billing.header.enterprise": "Unlimited products and AI capacity.",
"billing.header.payg":
- "Pay as you go — credits come from your company wallet.",
+ "Pay as you go. Credits come from your company wallet.",
"billing.header.remaining": "{remaining} AI credits remaining this period.",
"billing.na": "N/A",
"billing.range.7d": "last 7 days",
@@ -4137,7 +4136,7 @@ export const en: MessageDict = {
"billing.chartRange": "Range: {range}.",
"billing.nothingToChartTitle": "Nothing to chart yet",
"billing.nothingToChartBody":
- "Add products or connect a feed — usage will show up here.",
+ "Add products or connect a feed. Usage will show up here.",
"billing.dailyChartsHint":
"Daily charts are available for the last 7 days, 30 days, or the current billing cycle.",
"billing.noNewProducts": "No new products in {range}",
@@ -4154,7 +4153,7 @@ export const en: MessageDict = {
"billing.managePlanOnPlans": "Manage plan on Plans",
"billing.recovery.pastDueTitle": "Payment past due",
"billing.recovery.pastDueAdmin":
- "Your subscription payment failed. Update payment in the Customer Portal — access stays open during this grace period.",
+ "Your subscription payment failed. Update payment in the Customer Portal. Access stays open during this grace period.",
"billing.recovery.pastDueMember":
"Billing needs attention. Ask a company admin to update payment in the Customer Portal. Access stays open during this grace period.",
"billing.recovery.openPortal": "Open Customer Portal",
@@ -4162,7 +4161,7 @@ export const en: MessageDict = {
"billing.recovery.goToBilling": "Go to Billing",
"billing.recovery.missingPlanTitle": "No plan assigned",
"billing.recovery.missingPlanMessage":
- "This company has no active plan (often after a skipped migration). Assign a plan via Checkout or ask support — do not assume Unlimited capacity.",
+ "This company has no active plan (often after a skipped migration). Assign a plan via Checkout or ask support. Do not assume Unlimited capacity.",
"billing.recovery.choosePlan": "Choose a plan",
"billing.upgrade": "Upgrade",
"billing.askCompanyAdmin": "Ask a company admin",
@@ -4194,7 +4193,7 @@ export const en: MessageDict = {
"Paid AI steps that use the managed model spend company credits.",
"exports.title": "Export Feeds",
"exports.description":
- "Choose what to publish per channel — which products and fields — then share a public feed URL (no login).",
+ "Choose which products and fields to publish per channel, then share a public feed URL (no login).",
"exports.descriptionFiltered":
"Public Google Shopping, Meta, and custom feed links. Showing {filtered} of {total} feeds.",
"exports.loadFailed": "Failed to load export feeds",
@@ -4208,28 +4207,29 @@ export const en: MessageDict = {
"exports.howToTitle": "How to choose what to export",
"exports.howTo.choose": "What goes in the feed",
"exports.howTo.chooseBody":
- "— set source feed and product statuses when you create or edit a feed so the public URL only includes those products.",
+ "Set source feed and product statuses when you create or edit a feed so the public URL only includes those products.",
"exports.howTo.subset": "One-off subset",
- "exports.howTo.subsetBody": "— on",
+ "exports.howTo.subsetBody": "On",
"exports.howTo.productsLink": "Products",
"exports.howTo.subsetTail":
", select rows and Export to download only those products using a feed’s format and fields.",
"exports.howTo.google": "Google Shopping",
"exports.howTo.googleBody":
- "— create a CSV or XML preset, Generate, copy the public URL into Merchant Center → Products → Feeds → scheduled fetch.",
+ "Create a CSV or XML preset, select Generate, then copy the public URL into Merchant Center → Products → Feeds → scheduled fetch.",
"exports.howTo.meta": "Meta catalog",
- "exports.howTo.metaBody": "— CSV preset for Commerce Manager data sources.",
+ "exports.howTo.metaBody":
+ "Use a CSV preset for Commerce Manager data sources.",
"exports.howTo.custom": "Custom CSV/XML",
"exports.howTo.customBody":
- "— any partner; same public URL works for cron / webhook-style pollers.",
+ "Use it with any partner; the same public URL works for cron / webhook-style pollers.",
"exports.howTo.rest": "REST",
- "exports.howTo.restBody": "— API keys under",
+ "exports.howTo.restBody": "Find API keys under",
"exports.howTo.settingsApiKeys": "Settings → API keys",
"exports.howTo.restTail": "(legacy keys were not migrated) pull",
"exports.howTo.openapi": "OpenAPI",
"exports.feedStatus": "Feed Status",
"exports.generating":
- "Generating export — this can take a moment for large catalogs.",
+ "Generating export. This can take a moment for large catalogs.",
"exports.generationFailed":
"Generation failed. See the error above, then try Refresh Feed again.",
"exports.exportedProducts": "Exported {count} products",
@@ -4245,7 +4245,7 @@ export const en: MessageDict = {
"exports.tryAgain": "Try again",
"exports.emptyTitle": "No export feeds yet",
"exports.emptyBody":
- "Create a Google Shopping, Meta, or custom CSV/XML feed to choose which products and fields go out. Generate a public URL for Merchant Center or any partner — or export a selected subset from Products.",
+ "Create a Google Shopping, Meta, or custom CSV/XML feed to choose which products and fields go out. Generate a public URL for Merchant Center or any partner, or export a selected subset from Products.",
"exports.emptyCta": "Create Google Shopping feed",
"exports.noMatchTitle": "No matching feeds",
"exports.noMatchBody": "Try a different search term.",
@@ -4272,7 +4272,7 @@ export const en: MessageDict = {
"exports.format": "Format",
"exports.sourceFeed": "Source feed (optional)",
"exports.sourceFeedHelp":
- "Limit this feed to products from one input feed, or leave as All products — this is how you choose what the public URL includes.",
+ "Limit this feed to products from one input feed, or leave it as All products. This determines what the public URL includes.",
"exports.allProducts": "All products",
"exports.rootElement": "Root element",
"exports.itemElement": "Item element",
@@ -4359,7 +4359,7 @@ export const en: MessageDict = {
"processing.refresh": "Refresh",
"processing.loadFailed": "Failed to load processing jobs",
"processing.tooManyRequests":
- "Too many requests — wait a moment and try again.",
+ "Too many requests. Wait a moment and try again.",
"processing.serverError":
"{fallback} (server error {status}). Try again shortly.",
"processing.httpError": "{fallback} (HTTP {status})",
@@ -4402,7 +4402,7 @@ export const en: MessageDict = {
"processing.actions.processAriaCount":
"Process {count} selected product{plural} on this page",
"processing.actions.whatToProcess": "What to process",
- "processing.actions.freeNoAiTitle": "Free plan — no AI credits",
+ "processing.actions.freeNoAiTitle": "Free plan with no AI credits",
"processing.actions.freeNoAiBody":
"Normalize, specs, fill, and EPREL still run. AI titles/descriptions need a paid plan or credits.",
"processing.actions.comparePlans": "Compare plans",
@@ -4456,11 +4456,11 @@ export const en: MessageDict = {
"unsubscribe.done": "You have been unsubscribed.",
"stores.title": "Stores & sources",
"stores.description":
- "Connect any store or supplier in one place — Woo, Shopify, feed URL, or CSV. Live stores: products Descrybe → store; orders/reviews store → Descrybe. Export when the catalog is ready.",
+ "Connect stores and product sources, then publish your catalog through exports or the API.",
"stores.allInputFeeds": "All input feeds",
"stores.exportFeeds": "Export feeds",
"stores.migrationNote":
- "After a Descrybe platform migration, WooCommerce and Shopify credentials may need to be re-entered — that is a reconnect step, not a broken sync.",
+ "After a Descrybe platform migration, WooCommerce and Shopify credentials may need to be re-entered. That is a reconnect step, not a broken sync.",
"stores.docsPrefix": "Docs:",
"stores.docsApiOverview": "API overview",
"stores.docsSuffix":
@@ -4508,9 +4508,9 @@ export const en: MessageDict = {
"stores.wizard.banner.generic":
"Guided setup: finish this step, then return to the wizard.",
"stores.wizard.banner.test":
- "Guided setup — test the connection, then return to the wizard.",
+ "Guided setup: test the connection, then return to the wizard.",
"stores.wizard.banner.sync":
- "Guided setup — queue the first product sync, then return to the wizard.",
+ "Guided setup: queue the first product sync, then return to the wizard.",
"stores.wizard.banner.return": "Back to wizard",
"stores.wizard.banner.dismiss": "Dismiss",
"stores.woo.title": "WooCommerce",
@@ -4541,9 +4541,9 @@ export const en: MessageDict = {
"stores.export.cta": "Open exports",
"stores.apiKeys.title": "API keys",
"stores.apiKeys.description":
- "Legacy API keys were not migrated. Create a new key in Settings to restore /api/v1 access — the secret is shown only once.",
+ "Legacy API keys were not migrated. Create a new key in Settings to restore /api/v1 access. The secret is shown only once.",
"stores.apiKeys.descriptionMember":
- "Company API keys for /api/v1. Legacy keys were not migrated — only admins can create or revoke; you can view prefixes in Settings.",
+ "Company API keys for /api/v1. Legacy keys were not migrated. Only admins can create or revoke keys; you can view prefixes in Settings.",
"stores.apiKeys.cta": "Manage API keys",
"stores.apiKeys.ctaView": "View API keys",
"stores.status.needsReconnect": "Needs reconnect",
@@ -4628,10 +4628,10 @@ export const en: MessageDict = {
"No Shopify credentials on file yet. Ask a company admin to connect the store. Sync and order browse stay available after they save and test the connection.",
"shopify.enterToken": "Enter credentials",
"shopify.noCredentials":
- "No Shopify credentials on file. Enter your *.myshopify.com domain and Dev Dashboard Client ID + secret (or a legacy Admin API token), then Save and Test Connection. After a platform migration, legacy secrets are not carried over — reconnect with a fresh token.",
+ "No Shopify credentials on file. Enter your *.myshopify.com domain and Dev Dashboard Client ID + secret (or a legacy Admin API token), then Save and Test Connection. After a platform migration, legacy secrets are not carried over. Reconnect with a fresh token.",
"shopify.allConnectors": "All store connectors",
"shopify.setupDesc":
- "New Shopify apps are created in the Dev Dashboard (not Develop apps in Admin). Paste Client ID + secret below — Descrybe exchanges them for Admin API tokens and refreshes before they expire.",
+ "New Shopify apps are created in the Dev Dashboard (not Develop apps in Admin). Paste the Client ID + secret below. Descrybe exchanges them for Admin API tokens and refreshes them before they expire.",
"shopify.setup.oauthNote":
"Still manual: create/install the app in Shopify and paste Client ID + secret here. Descrybe does not host a one-click App Store install. Legacy permanent shpat_ tokens from older Admin custom apps still work.",
"shopify.setup.scopesTitle": "Required Admin API scopes",
@@ -4674,7 +4674,7 @@ export const en: MessageDict = {
"I have a legacy Admin API access token (shpat_…)",
"shopify.setup.hideLegacyToken": "Hide legacy token field",
"shopify.field.shopDomainHelp":
- "Admin API host only — use the *.myshopify.com name, not a custom storefront domain.",
+ "Admin API host only. Use the *.myshopify.com name, not a custom storefront domain.",
"shopify.field.clientId": "Client ID",
"shopify.field.clientSecret": "Client secret",
"shopify.field.clientSecretHelp":
@@ -4683,10 +4683,10 @@ export const en: MessageDict = {
"Only for custom apps created in Shopify Admin before January 2026. New apps should use Client ID + secret instead.",
"shopify.placeholder.clientId": "Client ID from Dev Dashboard",
"shopify.placeholder.clientIdKeep":
- "Client ID on file — enter a new one to replace",
+ "Client ID on file; enter a new one to replace it",
"shopify.placeholder.clientSecret": "Client secret",
"shopify.placeholder.clientSecretKeep":
- "Secret on file — enter a new one to replace",
+ "Secret on file; enter a new one to replace it",
"shopify.setup.scope.write_products": "write_products",
"shopify.setup.scope.write_productsWhy":
"Create and update products in Shopify (outbound push)",
@@ -4696,7 +4696,7 @@ export const en: MessageDict = {
"shopify.setup.scope.read_orders": "read_orders",
"shopify.setup.scope.read_ordersWhy": "Import orders into Descrybe (inbound)",
"shopify.setup.step1":
- "If you connected Shopify before a platform migration: create a new Dev Dashboard app (or reconnect with a fresh legacy token) — old saved secrets are not carried over.",
+ "If you connected Shopify before a platform migration, create a new Dev Dashboard app (or reconnect with a fresh legacy token). Old saved secrets are not carried over.",
"shopify.setup.step2.before": "Open the",
"shopify.setup.step2.path": "Shopify Dev Dashboard",
"shopify.setup.step2.after":
@@ -4727,17 +4727,17 @@ export const en: MessageDict = {
"shopify.dryRunLabel": "Dry run (no live Shopify calls)",
"shopify.ordersTitle": "Orders import (Shopify → Descrybe)",
"shopify.ordersDesc":
- "Shopify → Descrybe — pull orders and line items into Descrybe (does not write back to Shopify).",
+ "Shopify → Descrybe pulls orders and line items into Descrybe (it does not write back to Shopify).",
"shopify.ordersNeedsReconnect":
"Orders import cannot run until credentials are reconnected after migration. On the Connection tab: Save, then Test Connection.",
"shopify.notesTitle": "Notes",
"shopify.notesDesc": "Shopify connector capabilities",
"shopify.notes.productsLabel": "Products (Descrybe → Shopify)",
"shopify.notes.productsBody":
- "— processed Descrybe products are created or updated in Shopify (SKU match via GraphQL when no cached Shopify product id). Re-sync can overwrite matched remote fields.",
+ "Processed Descrybe products are created or updated in Shopify (SKU match via GraphQL when no cached Shopify product id). Re-sync can overwrite matched remote fields.",
"shopify.notes.ordersLabel": "Orders (Shopify → Descrybe)",
"shopify.notes.ordersBody":
- "— pulled for reporting and future audience workflows.",
+ "Pulled for reporting and future audience workflows.",
"shopify.notes.reviewsLabel": "Reviews",
"shopify.notes.reviewsBody":
"-- not supported. Shopify has no first-party product reviews Admin API (third-party review apps are out of scope here).",
@@ -4750,14 +4750,14 @@ export const en: MessageDict = {
"Only company admins can change WooCommerce connection and mapping settings. Sync and browse stay available.",
"woo.enterCredentials": "Enter credentials",
"woo.noCredentials":
- "No WooCommerce credentials on file. Enter your store URL, create a Read/Write REST API key in WordPress, paste Consumer Key and Secret below, then Save and Test Connection. After a platform migration, legacy secrets are not carried over — reconnect with a fresh key.",
+ "No WooCommerce credentials on file. Enter your store URL, create a Read/Write REST API key in WordPress, paste the Consumer Key and Secret below, then Save and Test Connection. After a platform migration, legacy secrets are not carried over. Reconnect with a fresh key.",
"woo.lockedTitle": "WooCommerce connection locked",
"woo.noCredentialsMember":
"No WooCommerce credentials are on file yet. Ask a company admin to connect the store. Sync, orders, and reviews stay available after they save and test the connection.",
"woo.setupDesc":
"Follow these steps to create a WooCommerce REST API key in WordPress and connect it here. Descrybe never creates keys on your store for you.",
"woo.setup.step1":
- "If you connected WooCommerce before a platform migration: create a new REST API key — old saved credentials are not usable.",
+ "If you connected WooCommerce before a platform migration, create a new REST API key. Old saved credentials are not usable.",
"woo.setup.step2.before": "Enter your store",
"woo.setup.step2.path": "HTTPS URL",
"woo.setup.step2.after":
@@ -4784,7 +4784,7 @@ export const en: MessageDict = {
"woo.setup.step6.and": "and",
"woo.setup.step6.reviews": "reviews",
"woo.setup.step6.inboundTail":
- "WooCommerce → Descrybe (inbound pull). Sync runs in the background — check status badges on this page.",
+ "WooCommerce → Descrybe (inbound pull). Sync runs in the background; check the status badges on this page.",
"woo.setup.openKeys": "Open REST API keys",
"woo.setup.addKey": "Add key in WordPress",
"woo.setup.openKeysHint":
@@ -4831,7 +4831,7 @@ export const en: MessageDict = {
"Test the connection, then refresh to load WooCommerce attributes.",
"woo.ordersTitle": "Orders import (WooCommerce → Descrybe)",
"woo.ordersDesc":
- "WooCommerce → Descrybe — pull orders and line items into Descrybe (does not write back to the store).",
+ "WooCommerce → Descrybe pulls orders and line items into Descrybe (it does not write back to the store).",
"woo.ordersNeedsReconnect":
"Orders import cannot run until credentials are reconnected after migration. On the Connection tab: Save, then Test Connection.",
"woo.syncSettingsTitle": "Sync Settings",
@@ -4998,7 +4998,7 @@ export const en: MessageDict = {
"feeds.activated": "Feed activated.",
"feeds.deactivated": "Feed deactivated.",
"feeds.syncTimeout":
- "Sync timed out waiting for the server. The job may still finish — open Sync History to check status.",
+ "Sync timed out waiting for the server. The job may still finish. Open Sync History to check its status.",
"feeds.createdMapHint": "Feed created. Map fields before syncing.",
"feeds.createdPlain": "Feed created.",
"feeds.createdMapDesc": "Map source columns to product fields, then sync.",
@@ -5036,7 +5036,7 @@ export const en: MessageDict = {
"products.table.cancelCategory": "Cancel category edit",
"products.table.categoryRequired": "Choose a category",
"products.table.noCategoriesAvailable":
- "No categories yet — create one under Catalog",
+ "No categories yet. Create one under Catalog",
"products.table.qualityUnavailable": "Quality unavailable",
"products.table.full": "Full",
"products.table.empty": "Empty",
@@ -5053,14 +5053,14 @@ export const en: MessageDict = {
"products.edit.noEnrichedName": "No enriched name",
"products.edit.resetName": "Reset enriched name to the original",
"products.edit.cannotClearName":
- "Original is empty — cannot clear enriched name via PATCH",
+ "The original is empty, so the enriched name cannot be cleared via PATCH",
"products.edit.showPreview": "Show preview",
"products.edit.editHtml": "Edit HTML",
"products.edit.noEnrichedDescription": "No enriched description",
"products.edit.resetDescription":
"Reset enriched description to the original",
"products.edit.cannotClearDescription":
- "Original is empty — cannot clear enriched description via PATCH",
+ "The original is empty, so the enriched description cannot be cleared via PATCH",
"products.edit.unknownFeed": "Unknown feed",
"products.edit.noProcessedName": "No processed name available",
"products.edit.feedNamed": "Feed · {name}",
@@ -5293,7 +5293,7 @@ export const en: MessageDict = {
"categories.activeOverride": "· active override for this category",
"categories.usingCompanyDefault": "· using company / built-in default",
"categories.usingCompanyDefaultForLang":
- "· empty for this language — company / built-in default applies",
+ "· empty for this language; company / built-in default applies",
"categories.aiPromptSavedLang": "AI prompt saved for {lang}.",
"categories.aiPromptClearedLang":
"AI prompt cleared for {lang} (company default will apply).",
@@ -5364,7 +5364,7 @@ export const en: MessageDict = {
"news.keyFeatures": "Key Features:",
"news.badge.new": "New",
"news.footer":
- "That’s everything for now — check back after the next release.",
+ "That’s everything for now. Check back after the next release.",
"news.category.enhancement": "Enhancement",
"news.category.feature": "Feature",
"news.category.integration": "Integration",
@@ -5468,8 +5468,8 @@ export const en: MessageDict = {
"Manage, process, and optimize your product catalog. Connect a store or import CSV via",
"products.pageDescriptionBasic":
"Manage, process, and optimize your product catalog. Import via Feeds or upload a CSV.",
- "products.keyboard.esc": "— close the product edit panel or dialogs",
- "products.keyboard.toggle": "— toggle this tip",
+ "products.keyboard.esc": "Close the product edit panel or dialogs",
+ "products.keyboard.toggle": "Toggle this tip",
"products.keyboard.reviewPrefix": "In Needs Review panel:",
"products.keyboard.acceptAll": "accept all",
"products.keyboard.rejectShort": "reject",
@@ -5492,7 +5492,7 @@ export const en: MessageDict = {
"products.enrichment.resetTitle": "Reset to unprocessed?",
"products.selectedOnPage": "{count} on this page selected",
"products.selectionPageOnly":
- "Showing {shown} of {total} — selection is this page only",
+ "Showing {shown} of {total}; selection is this page only",
"products.edit.coverageHint":
"How many of Name, Description, Attributes, and Category are present (feed or processed).",
"products.edit.noEprelShort": "No EPREL",
@@ -5624,7 +5624,7 @@ export const en: MessageDict = {
"Booleans only · env {env} · RPM {rpm} · batch {batch} · retries {retries}",
"admin.diagnostics.cutoverTitle": "Cutover readiness",
"admin.diagnostics.cutoverHint":
- "Presence flags only — goose version hints, worker age, plans missing. No live Stripe/SMTP.",
+ "Presence flags only: goose version hints, worker age, and missing plans. No live Stripe/SMTP.",
"admin.diagnostics.cutoverGoose": "Goose",
"admin.diagnostics.cutoverGooseVersion":
"version_max {max} · expected ≥ {min}",
@@ -5640,13 +5640,13 @@ export const en: MessageDict = {
"admin.diagnostics.cutoverApiKeysUnavailable":
"API key inventory unavailable",
"admin.diagnostics.cutoverApiKeysHint":
- "Legacy keys were not migrated — tenants must create a new dk_ key (no fake import).",
+ "Legacy keys were not migrated. Tenants must create a new dk_ key (no fake import).",
"admin.diagnostics.cutoverApiKeysLink": "List companies needing reissue",
"admin.diagnostics.cutoverApplied": "applied",
"admin.diagnostics.cutoverPending": "pending",
"admin.diagnostics.etlInventoryTitle": "ETL gap inventory",
"admin.diagnostics.etlInventoryHint":
- "Read-only COUNTs — no fake import. Blob bytes and default job history stay unmigrated unless ops ran optional domain jobs.",
+ "Read-only COUNTs with no fake import. Blob bytes and default job history stay unmigrated unless ops ran optional domain jobs.",
"admin.diagnostics.etlInventoryFiles": "Files metadata-only",
"admin.diagnostics.etlInventoryFilesDetail":
"metadata-only / total file rows",
@@ -5668,7 +5668,7 @@ export const en: MessageDict = {
"Cleanup jobs running longer than 2 hours.",
"admin.diagnostics.toolOrphanTitle": "Orphan processed",
"admin.diagnostics.toolOrphanDesc":
- "Cutover ops #7 — dry-run report, then confirm delete.",
+ "Cutover ops #7: run a dry-run report, then confirm deletion.",
"admin.diagnostics.toolAnalyticsTitle": "Analytics",
"admin.diagnostics.toolAnalyticsDesc": "Trends and platform usage charts.",
"admin.diagnostics.toolBootstrapTitle": "Bootstrap",
@@ -5734,13 +5734,13 @@ export const en: MessageDict = {
"How Descrybe collects, uses, and protects account data, product catalogs, and supplier feeds when you use our product data platform.",
"seo.terms.title": "Terms of Service | Descrybe",
"seo.terms.description":
- "Terms for using Descrybe — feed import, catalog enrichment, AI-assisted content, exports, and WooCommerce sync for your business.",
- "seo.features.title": "Features — Feeds, enrichment, and export | Descrybe",
+ "Terms for using Descrybe, including feed import, catalog enrichment, AI-assisted content, exports, and WooCommerce sync for your business.",
+ "seo.features.title": "Features: Feeds, enrichment, and export | Descrybe",
"seo.features.description":
"See how Descrybe imports supplier feeds, maps fields to your taxonomy, enriches product data, and ships catalogs via export feeds, WooCommerce, or API.",
"seo.contactSales.title": "Contact sales | Descrybe",
"seo.contactSales.description":
- "Tell us about your catalog and capacity needs — we will prepare a custom plan you can pay online.",
+ "Tell us about your catalog and capacity needs. We will prepare a custom plan you can pay online.",
"seo.unsubscribe.title": "Unsubscribe | Descrybe",
"seo.unsubscribe.description":
"Stop marketing emails from Descrybe using the unsubscribe link in the message we sent you.",
@@ -5938,14 +5938,14 @@ export const en: MessageDict = {
"plans.title": "Choose your plan",
"plans.sub.onPrefix": "You're on",
"plans.sub.enterpriseSuffix":
- "— unlimited products and AI capacity. No self-serve upgrade needed.",
+ "Unlimited products and AI capacity. No self-serve upgrade needed.",
"plans.sub.paygSuffix":
- "— pay as you go. Compare public plans below, or talk to sales for Enterprise.",
+ "Pay as you go. Compare public plans below, or talk to sales for Enterprise.",
"plans.sub.creditsMid": "with {remaining} AI credits remaining.",
"plans.sub.creditsSuffix":
"Upgrade Starter → Growth → Business with Checkout, or talk to sales for Enterprise.",
"plans.sub.none":
- "No plan is assigned yet (for example after a skipped migration). Choose a plan below — capacity is not Unlimited until Checkout or an admin assigns one.",
+ "No plan is assigned yet (for example after a skipped migration). Choose a plan below. Capacity is not Unlimited until Checkout or an admin assigns one.",
"plans.stripeHint": "Self-serve upgrades use Stripe Checkout.",
"plans.checkoutUnavailable":
"Self-serve checkout is unavailable until Stripe is configured. Contact sales to get a plan.",
@@ -5974,15 +5974,15 @@ export const en: MessageDict = {
"Soft banners warn you first. When you run out of AI credits or hit your SKU cap, jobs that need that capacity are blocked until you free space, wait for the next cycle, or upgrade.",
"plans.faq.selfServe.q": "Can I self-serve upgrade?",
"plans.faq.selfServe.a":
- "Company admins can self-serve Starter, Growth, and Business via Checkout on a plan card. Members should ask a company admin — Checkout requires the admin role. Enterprise is always sales-led. Platform admins can still assign plans under Billing Admin.",
+ "Company admins can self-serve Starter, Growth, and Business via Checkout on a plan card. Members should ask a company admin because Checkout requires the admin role. Enterprise is always sales-led. Platform admins can still assign plans under Billing Admin.",
"plans.faq.enterprise.q": "Enterprise & millions of SKUs",
"plans.faq.enterprise.a":
- "Custom capacity, bring-your-own AI key, SLA, and account management are sales-led. Use Contact sales to send a request — we prepare a custom plan and Stripe Checkout link. You can also book time via Calendly. Enterprise is not self-serve at multi-million SKU scale. The app shows Unlimited for SKU and AI capacity.",
+ "Custom capacity, bring-your-own AI key, SLA, and account management are sales-led. Use Contact sales to send a request. We prepare a custom plan and Stripe Checkout link. You can also book time via Calendly. Enterprise is not self-serve at multi-million SKU scale. The app shows Unlimited for SKU and AI capacity.",
"plans.faq.pricing.q": "Where is public pricing?",
"plans.faq.pricing.aBefore": "See the marketing comparison on the",
"plans.faq.pricing.pricingPage": "Pricing page",
"plans.faq.pricing.aMid":
- "— CTAs are Get started (register) or Contact sales. Manage usage anytime under",
+ "CTAs are Get started (register) or Contact sales. Manage usage anytime under",
"plans.faq.pricing.aAfter": ".",
"plans.custom.title": "Need a custom plan?",
"plans.custom.body":
@@ -6004,8 +6004,8 @@ export const en: MessageDict = {
"Platform uses Descrybe credits and our key. Popular or Custom uses your OpenAI-compatible key (the provider call itself does not spend platform credits; feature credit rules still apply).",
"ai.whoPays": "Who pays for the model?",
"ai.mode.internal": "Platform (Descrybe credits / key)",
- "ai.mode.popular": "Your key — popular provider",
- "ai.mode.custom": "Your key — custom endpoint",
+ "ai.mode.popular": "Your key with a popular provider",
+ "ai.mode.custom": "Your key with a custom endpoint",
"ai.useCompanyKey":
"Use this company key for processing, SEO AI, and campaign AI",
"ai.popular.title": "Popular provider",
@@ -6019,7 +6019,7 @@ export const en: MessageDict = {
"ai.field.baseUrl": "Base URL",
"ai.apiKey.title": "API key",
"ai.apiKey.desc":
- "Leave blank to keep the existing key. Raw secrets are never returned — only a masked last4.",
+ "Leave blank to keep the existing key. Raw secrets are never returned; only a masked last4 is shown.",
"ai.storedKey": "Stored key: {masked}",
"ai.field.apiKey": "API key",
"ai.placeholder.keepKey": "•••• leave blank to keep",
@@ -6030,7 +6030,7 @@ export const en: MessageDict = {
"ai.prompts.descBefore":
"Customize system and user prompts for product copy, SEO meta, and campaign emails. Insert variables like",
"ai.prompts.descMid":
- "— they are filled at runtime. Brand kit voice is available as",
+ "They are filled at runtime. Brand kit voice is available as",
"ai.prompts.descAfter": ".",
"ai.field.feature": "Feature",
"ai.badge.custom": "Custom",
@@ -6083,7 +6083,7 @@ export const en: MessageDict = {
"email.field.from": "From",
"email.field.fromName": "From name",
"email.field.replyTo": "Reply-To",
- "email.resendKeySaved": "Resend API key (saved — leave blank to keep)",
+ "email.resendKeySaved": "Resend API key (saved; leave blank to keep)",
"email.resendKey": "Resend API key",
"email.field.smtpHost": "SMTP host",
"email.field.port": "Port",
@@ -6103,7 +6103,7 @@ export const en: MessageDict = {
"email.flash.verifyFailed": "Verification failed",
"email.flash.testFailed": "Test send failed",
"email.flash.blastDryRun":
- "Delivery check recorded in dry-run mode — no email was sent.",
+ "Delivery check recorded in dry-run mode. No email was sent.",
"email.flash.blastDone":
"Delivery check finished: {sent} sent, {failed} failed.",
"email.flash.blastFailed": "Blast failed",
@@ -6137,7 +6137,7 @@ export const en: MessageDict = {
"seo.fixable": "Fixable",
"seo.clearFilter": "Clear filter",
"seo.findings": "Findings",
- "seo.noFindings": "No findings for this filter — nice work.",
+ "seo.noFindings": "No findings for this filter. Nice work.",
"seo.col.issue": "Issue",
"seo.col.entity": "Entity",
"seo.col.severity": "Severity",
@@ -6228,8 +6228,7 @@ export const en: MessageDict = {
"assistant.resetTitle": "Reset",
"assistant.closeAria": "Close assistant",
"assistant.messageLabel": "Message",
- "assistant.placeholder":
- "e.g. add a feed URL, API keys, attributes, processing…",
+ "assistant.placeholder": "Describe what to configure…",
"assistant.sendAria": "Send",
"site.bookDemo": "Book a Demo",
"site.demoShort": "Demo",
@@ -6673,7 +6672,7 @@ export const en: MessageDict = {
"billing.packApproxProducts": "~{count} AI products",
"billing.buyPack": "Buy",
"billing.checkoutCompletePack":
- "Credit pack purchased — {credits} AI credits added.",
+ "Credit pack purchased. {credits} AI credits added.",
"billing.packCheckoutFailed": "Could not start credit pack checkout.",
"empty.default.noItems": "Nothing here yet.",
"settings.tabLockedHint": "Not on your plan",
@@ -6685,10 +6684,10 @@ export const en: MessageDict = {
"Your current plan does not include this part of Descrybe. Upgrade to unlock it, or ask a company admin if you cannot change billing.",
"plan.upgrade.sectionOff.title": "This feature is temporarily unavailable",
"plan.upgrade.sectionOff.message":
- "This part of Descrybe is turned off for the platform right now. It is not an upgrade issue — try again later or contact support.",
+ "This part of Descrybe is turned off for the platform right now. It is not an upgrade issue. Try again later or contact support.",
"plan.upgrade.sectionOff.cta": "Back to dashboard",
"plan.upgrade.stores.message":
- "WooCommerce and Shopify connectors need a plan that includes Stores. Upgrade to connect those stores — or keep working with feeds and the API on your current plan.",
+ "WooCommerce and Shopify connectors need a plan that includes Stores. Upgrade to connect those stores, or keep working with feeds and the API on your current plan.",
"plan.upgrade.stillWorks.title": "What still works on your plan",
"plan.upgrade.stillWorks.feeds.label": "Input feeds",
"plan.upgrade.stillWorks.feeds.description":
@@ -6701,7 +6700,7 @@ export const en: MessageDict = {
"Monitor AI and catalog jobs in Processing - available on your plan even without store connectors.",
"plan.upgrade.stillWorks.api.label": "API keys",
"plan.upgrade.stillWorks.api.description":
- "Create a key in Settings to use /api/v1 — the secret is shown only once.",
+ "Create a key in Settings to use /api/v1. The secret is shown only once.",
"plan.upgrade.stillWorks.docs.label": "API docs",
"plan.upgrade.stillWorks.docs.description":
"Read the API overview and integrate without WooCommerce or Shopify.",
@@ -6794,12 +6793,12 @@ export const en: MessageDict = {
"storeReconnect.titleChannel": "Reconnect {channel} after migration",
"storeReconnect.titleSingular": "Reconnect store after migration",
"storeReconnect.messageWithItems":
- "Store API credentials were not carried over (or cannot be used) after the platform migration. This is not a broken sync — reconnect each store, then Test Connection before queueing a sync.",
+ "Store API credentials were not carried over (or cannot be used) after the platform migration. This is not a broken sync. Reconnect each store, then Test Connection before queueing a sync.",
"storeReconnect.messageSingle":
- "{channel} API credentials were not carried over (or cannot be used) after the platform migration. This is not a broken sync — paste fresh credentials, Save, then Test Connection before queueing a sync.",
+ "{channel} API credentials were not carried over (or cannot be used) after the platform migration. This is not a broken sync. Paste fresh credentials, Save, then Test Connection before queueing a sync.",
"storeReconnect.channelFallback": "Store",
"storeReconnect.askAdmin":
- "Ask a company admin to reconnect — only admins can save store credentials.",
+ "Ask a company admin to reconnect because only admins can save store credentials.",
"storeReconnect.reconnectItem": "Reconnect {label}",
"admin.storeReconnect.channel.other": "Other",
"admin.storeReconnect.channel.woocommerce": "WooCommerce",
@@ -6824,7 +6823,7 @@ export const en: MessageDict = {
"admin.storeReconnect.metric.showing": "Rows shown",
"admin.storeReconnect.metric.total": "Gaps total",
"admin.storeReconnect.cutoverHint":
- "Read-only inventory. Merchants reconnect via /stores/wizard or the channel Connection tab — this page does not mutate credentials.",
+ "Read-only inventory. Merchants reconnect via /stores/wizard or the channel Connection tab. This page does not modify credentials.",
"admin.storeReconnect.loadFailed":
"Could not load store reconnect inventory.",
"admin.storeReconnect.refresh": "Refresh",
@@ -6842,15 +6841,15 @@ export const en: MessageDict = {
"productSync.directionTitle": "Products: Descrybe → store",
"productSync.outbound": "Descrybe → {channel}",
"productSync.outboundBody":
- "— pushes processed Descrybe products to the store (create or update matched remote products). Does not import the store catalog into Descrybe.",
+ "Pushes processed Descrybe products to the store (creates or updates matched remote products). Does not import the store catalog into Descrybe.",
"productSync.firstSync": "First sync",
"productSync.firstSyncBody":
- "— unmatched products are created in {channel}. Later syncs update products already linked by id or match strategy.",
+ "Unmatched products are created in {channel}. Later syncs update products already linked by ID or match strategy.",
"productSync.reSync": "Re-sync",
"productSync.reSyncBody":
- "— matched remote products can be overwritten with Descrybe content ({count} linked so far).",
+ "Matched remote products can be overwritten with Descrybe content ({count} linked so far).",
"productSync.queued": "Product sync queued",
- "productSync.live.queued": "Queued — waiting for worker",
+ "productSync.live.queued": "Queued and waiting for worker",
"productSync.live.running": "Sync in progress",
"productSync.live.watching":
"Watching sync status… counts appear when the run finishes.",
@@ -6860,7 +6859,7 @@ export const en: MessageDict = {
"Sync finished with some failures: {total} total · {created} created · {updated} updated · {failed} failed · {skipped} skipped",
"productSync.live.failed": "Sync failed{detail}.",
"productSync.live.timedOut":
- "Still running — refresh later for final status and counts.",
+ "Still running. Refresh later for the final status and counts.",
"productSync.live.ordersSuccess": "Orders sync finished ({status}).",
"productSync.live.reviewsSuccess": "Reviews sync finished ({status}).",
"productSync.retry": "Retry sync",
@@ -6874,7 +6873,7 @@ export const en: MessageDict = {
"Product sync cannot run until credentials are reconnected after migration. On the Connection tab: Save, then Test Connection.",
"productSync.scheduleTitle": "Automatic product sync schedule",
"productSync.scheduleHelp":
- "Like feed sync intervals: set how often Descrybe pushes products to the store, or pause the schedule. Manual queue still works while paused. Store webhooks are not used — updates are polled/queued only.",
+ "Like feed sync intervals: set how often Descrybe pushes products to the store, or pause the schedule. Manual queue still works while paused. Store webhooks are not used; updates are only polled or queued.",
"productSync.scheduleInterval": "Sync interval (hours)",
"productSync.scheduleIntervalHint":
"1–168 hours. Default is 6 hours when unset on the server.",
@@ -6891,11 +6890,11 @@ export const en: MessageDict = {
"productSync.scheduleAdminOnly": "Company admin required",
"productSync.delivery.title": "Updates use polling, not store webhooks",
"productSync.delivery.body":
- "Descrybe does not receive Shopify or WooCommerce webhooks. Product pushes run when you queue them or on the automatic schedule. This page watches progress by polling connection status — not via live store push events.",
+ "Descrybe does not receive Shopify or WooCommerce webhooks. Product pushes run when you queue them or on the automatic schedule. This page watches progress by polling connection status, not through live store push events.",
"productSync.delivery.inboundNote":
"Orders and reviews also use queued pulls (scheduled or manual), not live webhooks.",
"productSync.delivery.hubNote":
- "Store connectors sync on a schedule or when you queue a job — not via live store webhooks.",
+ "Store connectors sync on a schedule or when you queue a job, not through live store webhooks.",
"productSync.delivery.badge.manual": "Manual queue",
"productSync.delivery.badge.scheduled": "Scheduled every {hours}h",
"productSync.delivery.badge.scheduledDefault": "Scheduled (server default)",
@@ -6981,7 +6980,7 @@ export const en: MessageDict = {
"Everything except billing, teammates and API keys.",
"settings.permissions.role.integrator": "Third-party developer",
"settings.permissions.role.integrator.desc":
- "Products, categories, attributes, fields, feeds, exports, processing and API keys — for system integrations. Fine-tune to turn any area off.",
+ "Products, categories, attributes, fields, feeds, exports, processing, and API keys for system integrations. Fine-tune to turn any area off.",
"settings.permissions.role.catalog_editor": "Catalog editor",
"settings.permissions.role.catalog_editor.desc":
"Products, categories, attributes and fields. No feeds, stores or settings.",
@@ -6999,7 +6998,7 @@ export const en: MessageDict = {
"You picked areas by hand, so this matches no standard role.",
"settings.permissions.advanced": "Fine-tune individual areas",
"settings.permissions.fineTuneHint":
- "Areas only — expand Fine-tune when you need tabs or actions inside an area.",
+ "Areas only. Expand Fine-tune when you need tabs or actions inside an area.",
"settings.permissions.showFineTune": "Fine-tune ({count})",
"settings.permissions.hideFineTune": "Hide fine-tune",
"admin.nav.aiCalls": "AI calls",
diff --git a/apps/web/src/lib/nav-ui.svelte.ts b/apps/web/src/lib/nav-ui.svelte.ts
index 2cd96ae..2b02c2c 100644
--- a/apps/web/src/lib/nav-ui.svelte.ts
+++ b/apps/web/src/lib/nav-ui.svelte.ts
@@ -3,34 +3,34 @@ let mobileOpen = $state(false);
let commandPaletteOpen = $state(false);
export const navUi = {
- get mobileOpen() {
- return mobileOpen;
- },
- openMobile() {
- mobileOpen = true;
- },
- closeMobile() {
- mobileOpen = false;
- },
- toggleMobile() {
- mobileOpen = !mobileOpen;
- },
- get commandPaletteOpen() {
- return commandPaletteOpen;
- },
- openCommandPalette() {
- mobileOpen = false;
- commandPaletteOpen = true;
- },
- closeCommandPalette() {
- commandPaletteOpen = false;
- },
- toggleCommandPalette() {
- if (commandPaletteOpen) {
- commandPaletteOpen = false;
- return;
- }
- mobileOpen = false;
- commandPaletteOpen = true;
- }
+ get mobileOpen() {
+ return mobileOpen;
+ },
+ openMobile() {
+ mobileOpen = true;
+ },
+ closeMobile() {
+ mobileOpen = false;
+ },
+ toggleMobile() {
+ mobileOpen = !mobileOpen;
+ },
+ get commandPaletteOpen() {
+ return commandPaletteOpen;
+ },
+ openCommandPalette() {
+ mobileOpen = false;
+ commandPaletteOpen = true;
+ },
+ closeCommandPalette() {
+ commandPaletteOpen = false;
+ },
+ toggleCommandPalette() {
+ if (commandPaletteOpen) {
+ commandPaletteOpen = false;
+ return;
+ }
+ mobileOpen = false;
+ commandPaletteOpen = true;
+ },
};
diff --git a/apps/web/src/lib/seo.ts b/apps/web/src/lib/seo.ts
index 541035d..30f3d67 100644
--- a/apps/web/src/lib/seo.ts
+++ b/apps/web/src/lib/seo.ts
@@ -17,7 +17,7 @@ export type PageSeo = {
};
export const HOME_SEO: PageSeo = {
- title: "Descrybe — Turn supplier feeds into ready product pages",
+ title: "Descrybe: Turn supplier feeds into ready product pages",
description:
"Connect CSV or XML supplier feeds, map them to your categories and attributes, generate titles and descriptions that match your rules, then export or sync to WooCommerce.",
path: "/",
@@ -26,9 +26,9 @@ export const HOME_SEO: PageSeo = {
};
export const PRICING_SEO: PageSeo = {
- title: "Pricing — Free, Starter, Growth, Business | Descrybe",
+ title: "Pricing: Free, Starter, Growth, Business | Descrybe",
description:
- "Start free with 100 products and feed mapping. Paid plans add AI titles and descriptions, more SKUs, export feeds, and WooCommerce sync — from $49/month. Enterprise for unlimited catalogs.",
+ "Start free with 100 products and feed mapping. Paid plans add AI titles and descriptions, more SKUs, export feeds, and WooCommerce sync from $49/month. Enterprise supports unlimited catalogs.",
path: "/pricing",
image: DEFAULT_OG_IMAGE,
imageAlt: DEFAULT_OG_IMAGE_ALT
@@ -46,14 +46,14 @@ export const PRIVACY_SEO: PageSeo = {
export const TERMS_SEO: PageSeo = {
title: "Terms of Service | Descrybe",
description:
- "Terms for using Descrybe — feed import, catalog enrichment, AI-assisted content, exports, and WooCommerce sync for your business.",
+ "Terms for using Descrybe, including feed import, catalog enrichment, AI-assisted content, exports, and WooCommerce sync for your business.",
path: "/terms",
image: DEFAULT_OG_IMAGE,
imageAlt: DEFAULT_OG_IMAGE_ALT
};
export const FEATURES_SEO: PageSeo = {
- title: "Features — Feeds, enrichment, and export | Descrybe",
+ title: "Features: Feeds, enrichment, and export | Descrybe",
description:
"See how Descrybe imports supplier feeds, maps fields to your taxonomy, enriches product data, and ships catalogs via export feeds, WooCommerce, or API.",
path: "/features",
diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte
index 7a78054..273859b 100644
--- a/apps/web/src/routes/+layout.svelte
+++ b/apps/web/src/routes/+layout.svelte
@@ -8,6 +8,12 @@
import SupportNotificationBell from "$lib/components/SupportNotificationBell.svelte";
import TaskStatusIndicator from "$lib/components/TaskStatusIndicator.svelte";
import Toaster from "$lib/components/ui/Toaster.svelte";
+ import {
+ DropdownMenu,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator
+ } from "$lib/components/ui";
import TutorialOverlay from "$lib/components/tutorial/TutorialOverlay.svelte";
import AssistantHost from "$lib/components/assistant/AssistantHost.svelte";
import CompanySwitcher from "$lib/components/CompanySwitcher.svelte";
@@ -23,6 +29,7 @@
import { browser } from "$app/environment";
import { goto } from "$app/navigation";
import { page } from "$app/state";
+ import { trackEvent } from "$lib/analytics";
import { api, isUnauthorized } from "$lib/api";
import { safeInternalNext } from "$lib/safe-next";
import { AUTH_PATHS, MARKETING_PATHS } from "$lib/site";
@@ -51,7 +58,7 @@
import { tutorial } from "$lib/tutorial";
import { billingRecovery, type PlanLike } from "$lib/billing-display";
import { isCompanyAdmin } from "$lib/company-admin";
- import { GraduationCap, ListTree, Menu } from "@lucide/svelte";
+ import { GraduationCap, ListTree, LogOut, Menu, Moon, Settings, Sun } from "@lucide/svelte";
let { children } = $props();
@@ -61,6 +68,21 @@
let me = $state(null);
let meRequested = $state(false);
+ let loggingOut = $state(false);
+
+ async function logout() {
+ if (loggingOut) return;
+ loggingOut = true;
+ try {
+ await api("/api/auth/logout", { method: "POST" });
+ } catch {
+ /* Session may already be gone. */
+ } finally {
+ trackEvent("logout");
+ await goto("/login");
+ loggingOut = false;
+ }
+ }
const SYSTEM_MODE_POLL_MS = 60_000;
@@ -438,31 +460,7 @@
{/if}
{/if}
{#if !tutorial.active}
- {#if tutorial.canResume}
- void tutorial.resume()}
- >
-
- {i18n.t("dashboard.resumeTutorial")}
-
- {:else if tutorial.canRestart}
- void tutorial.restart()}
- >
-
- {i18n.t("dashboard.restartTutorial")}
-
- {:else}
+ {#if !tutorial.canResume && !tutorial.canRestart}
{i18n.t("dashboard.startTutorial")}
{/if}
- void tutorial.openSectionPicker()}
- >
-
- {i18n.t("dashboard.tourTopics")}
-
{/if}
-
{#if me?.user}
- {#if me.impersonating || me.dev_user_switch}
-
+ {#snippet trigger({ open, toggle })}
+
+ {userInitial}
+
+ {/snippet}
+
+
+
+
{i18n.t("settings.profileHeading")}
+
{me.user.email}
+
+
+ theme.setTheme("light")}
+ >
+
+
+ theme.setTheme("dark")}
+ >
+
+
+
+
+
+ void goto("/settings")}>
+
+ {i18n.t("nav.settings")}
+
+ void tutorial.openSectionPicker()}>
+
+ {i18n.t("dashboard.tourTopics")}
+
+
+ void logout()}
>
- {me.user.email}
-
- {/if}
-
- {userInitial}
-
+
+ {loggingOut ? i18n.t("header.signingOut") : i18n.t("header.signOut")}
+
+
{/if}
diff --git a/apps/web/src/routes/attributes/+page.svelte b/apps/web/src/routes/attributes/+page.svelte
index 1e099df..2ad8d58 100644
--- a/apps/web/src/routes/attributes/+page.svelte
+++ b/apps/web/src/routes/attributes/+page.svelte
@@ -1114,14 +1114,14 @@
{i18n.t("attributes.bulk.uploadHint")}
- attribute_key — {i18n.t("attributes.bulk.col.key")}
- name — {i18n.t("attributes.bulk.col.name")}
+ attribute_key : {i18n.t("attributes.bulk.col.key")}
+ name : {i18n.t("attributes.bulk.col.name")}
- value_type — {i18n.t("attributes.bulk.col.valueType")}
+ value_type : {i18n.t("attributes.bulk.col.valueType")}
- category_id — {i18n.t("attributes.bulk.col.categoryId")}
- unit / example — {i18n.t("attributes.bulk.col.unitExample")}
- parent_key — {i18n.t("attributes.bulk.col.parentKey")}
+ category_id : {i18n.t("attributes.bulk.col.categoryId")}
+ unit / example : {i18n.t("attributes.bulk.col.unitExample")}
+ parent_key : {i18n.t("attributes.bulk.col.parentKey")}
diff --git a/apps/web/src/routes/dashboard/+page.svelte b/apps/web/src/routes/dashboard/+page.svelte
index f31a199..78bee59 100644
--- a/apps/web/src/routes/dashboard/+page.svelte
+++ b/apps/web/src/routes/dashboard/+page.svelte
@@ -15,7 +15,6 @@
import ListSkeleton from "$lib/components/ListSkeleton.svelte";
import StatCardsSkeleton from "$lib/components/StatCardsSkeleton.svelte";
import DashboardStats from "$lib/components/DashboardStats.svelte";
- import NewsFeed from "$lib/components/NewsFeed.svelte";
import UpgradeBanner from "$lib/components/UpgradeBanner.svelte";
import ActivationChecklist from "$lib/components/ActivationChecklist.svelte";
import MigratedEtlGapsPanel from "$lib/components/MigratedEtlGapsPanel.svelte";
@@ -882,12 +881,6 @@
{/if}
-
-
- {i18n.t("dashboard.whatsNew")}
-
-
-
diff --git a/apps/web/src/routes/docs/+page.svelte b/apps/web/src/routes/docs/+page.svelte
index d1d07f0..7cc1030 100644
--- a/apps/web/src/routes/docs/+page.svelte
+++ b/apps/web/src/routes/docs/+page.svelte
@@ -579,7 +579,7 @@
Public
/api/v1
- uses Bearer or X-API-Key — not your dashboard session cookie. Prefer
+ uses Bearer or X-API-Key, not your dashboard session cookie. Prefer
Authorization: Bearer.
diff --git a/apps/web/src/routes/feeds/+page.svelte b/apps/web/src/routes/feeds/+page.svelte
index e9709df..4cca630 100644
--- a/apps/web/src/routes/feeds/+page.svelte
+++ b/apps/web/src/routes/feeds/+page.svelte
@@ -690,7 +690,7 @@
success = i18n.t("flash.feed.syncFinishedNamed", { name: feed.name ?? id, status, products });
notifySuccess(i18n.t("toast.feed.syncFinished"), {
alert: "sync_done",
- description: `"${feed.name ?? id}" — ${products}. Process a sample when ready.`,
+ description: `"${feed.name ?? id}": ${products}. Process a sample when ready.`,
actions: [
{ label: i18n.t("feeds.viewProducts"), href: `/products?feed_id=${id}` },
{ label: i18n.t("feeds.processing"), href: "/processing" }
@@ -811,12 +811,7 @@
}
-
+
{#snippet actions()}
void goto("/stores")}>{i18n.t("feeds.storesHub")}
openAdd()} data-tour="feeds-add" data-assistant-target="feeds-add">
diff --git a/apps/web/src/routes/products/+page.svelte b/apps/web/src/routes/products/+page.svelte
index 1509159..dad0536 100644
--- a/apps/web/src/routes/products/+page.svelte
+++ b/apps/web/src/routes/products/+page.svelte
@@ -588,8 +588,8 @@
const typeLabel = types.join(", ");
success =
!canUseAI && processingType === "full"
- ? `Processing started — ${receipt} (normalize/specs/fill — AI skipped on Free).`
- : `Processing started — ${receipt} (${typeLabel}).`;
+ ? `Processing started: ${receipt} (normalize/specs/fill; AI skipped on Free).`
+ : `Processing started: ${receipt} (${typeLabel}).`;
notifySuccess(i18n.t("toast.products.jobStarted"), {
description: !canUseAI
? `${receipt} · free-tier steps (AI skipped). Track progress in Processing.`
@@ -1035,7 +1035,7 @@
});
success = result.message
? `${result.message} (${receipt})`
- : `Reset complete — ${receipt}.`;
+ : `Reset complete: ${receipt}.`;
notifySuccess(i18n.t("toast.products.resetComplete"), {
description: receipt,
actions: [PROCESSING_RECEIPT_ACTION],
diff --git a/apps/web/src/routes/settings/+page.svelte b/apps/web/src/routes/settings/+page.svelte
index 9077688..03a825d 100644
--- a/apps/web/src/routes/settings/+page.svelte
+++ b/apps/web/src/routes/settings/+page.svelte
@@ -64,6 +64,7 @@
} from "$lib/types";
import PageShell from "$lib/components/PageShell.svelte";
import Alert from "$lib/components/Alert.svelte";
+ import NewsFeed from "$lib/components/NewsFeed.svelte";
import MemberPermissionsDialog from "$lib/components/settings/MemberPermissionsDialog.svelte";
import Spinner from "$lib/components/Spinner.svelte";
import EmptyState from "$lib/components/EmptyState.svelte";
@@ -1125,6 +1126,11 @@
+
+
+ {i18n.t("dashboard.whatsNew")}
+
+
diff --git a/apps/web/src/routes/standard-fields/+page.svelte b/apps/web/src/routes/standard-fields/+page.svelte
index 83abf06..690a374 100644
--- a/apps/web/src/routes/standard-fields/+page.svelte
+++ b/apps/web/src/routes/standard-fields/+page.svelte
@@ -572,7 +572,7 @@
data-assistant-target="enable-recommended"
onclick={() => void enableRecommended()}
>
- Enable recommended
+ {i18n.t("standardFields.enableRecommended")}
{#if selectedIds.length > 0}
{:else}
-
-
-
+
+
+
@@ -689,18 +689,10 @@
{groupNameOf(groupIdOf(field))}
- {#if isEnabled(field)}
- {i18n.t("common.on")}
- {:else}
- {i18n.t("common.off")}
- {/if}
{#if isRequired(field)}
- {i18n.t("common.required")}
+ {i18n.t("common.required")}
{:else}
- {i18n.t("common.optional")}
- {/if}
- {#if isSystem(field)}
- {i18n.t("common.system")}
+ {i18n.t("common.optional")}
{/if}
@@ -759,9 +751,9 @@
-
-
-
+
+
+
diff --git a/apps/web/src/routes/stores/+page.svelte b/apps/web/src/routes/stores/+page.svelte
index c53df00..fd129e7 100644
--- a/apps/web/src/routes/stores/+page.svelte
+++ b/apps/web/src/routes/stores/+page.svelte
@@ -24,7 +24,6 @@
import PageShell from "$lib/components/PageShell.svelte";
import EmptyState from "$lib/components/EmptyState.svelte";
import StoreReconnectBanner from "$lib/components/stores/StoreReconnectBanner.svelte";
- import StoreSyncDeliveryBanner from "$lib/components/stores/StoreSyncDeliveryBanner.svelte";
import { planCapabilities } from "$lib/plan-capabilities.svelte";
import { upgradeMessageForFeature } from "$lib/plan-upgrade-message";
import PlanUpgradePanel from "$lib/components/PlanUpgradePanel.svelte";
@@ -229,24 +228,8 @@
- {:else}
-
- {i18n.t("stores.migrationNote")}
-
{/if}
-
-
-
-
-
- {i18n.t("stores.docsPrefix")}
- {i18n.t("stores.docsApiOverview")}
- {i18n.t("stores.docsSuffix")}
-
-
{#if !canHub}
+
-
-
-
- {i18n.t("stores.startTitle")}
-
- {canAdmin ? i18n.t("stores.startBody") : i18n.t("stores.startBodyMember")}
-
-
- {#if canAdmin && canHub}
-
- void goto("/stores/wizard")}
- data-tour="stores-guided-setup"
- aria-label={i18n.t("stores.wizard.guidedSetupAria")}
- >
- {i18n.t("stores.wizard.guidedSetup")}
-
-
-
- {:else if !canAdmin}
-
-
-
- {i18n.t("common.askAdmin")}
-
-
-
- {/if}
-
-
+
+ {i18n.t("stores.docsPrefix")}
+ {i18n.t("stores.docsApiOverview")}
+ {i18n.t("stores.docsSuffix")}
+