feat(i18n): localize data-layer labels (lib/data + lib/services) — English base complete

All display labels in MARKETS/ENTITY_TYPES/FIELDS/ENRICHMENTS/FREQUENCIES/DELIVERY_FORMATS + SERVICES/FLOW_LABEL are now {en,sv} with stable IDs/values (pricing logic, EntityType/Frequency unions, config keys unchanged). Added entityLabel()/FREQUENCY_LABEL/FORMAT_LABEL. Updated all render sites (configurator, tjanster, beskriv, katalog/[id]) to resolve via useT(). Also fixed 2 pre-existing tsc nits (auth BufferSource cast, orders-store sql.json param type). tsc 0, build green. 0pt is now fully English-by-default with SV toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 11:47:42 +02:00
co-authored by Claude Opus 4.8
parent 60455ab61a
commit 125b2e1bbe
8 changed files with 121 additions and 88 deletions
+2 -2
View File
@@ -74,11 +74,11 @@ export default function DescribePage() {
color: service === s.id ? "var(--background)" : "var(--foreground)",
}}
>
{s.name}
{t(s.name)}
</button>
))}
</div>
{svc ? <p className="mt-3 text-sm text-muted-foreground">{svc.covers} <span className="text-muted-foreground/60">→ {svc.deliverable}</span></p> : null}
{svc ? <p className="mt-3 text-sm text-muted-foreground">{t(svc.covers)} <span className="text-muted-foreground/60">→ {t(svc.deliverable)}</span></p> : null}
</div>
<div className="mt-8">
+15 -12
View File
@@ -14,6 +14,9 @@ import {
ENRICHMENTS,
FREQUENCIES,
DELIVERY_FORMATS,
FREQUENCY_LABEL,
FORMAT_LABEL,
entityLabel,
computeEstimate,
formatSEK,
type EntityType,
@@ -137,12 +140,12 @@ export default function ConfiguratorPage() {
<div className="flex flex-wrap gap-2">
{SERVICES.map((s) => (
<Chip key={s.id} active={service === s.id} onClick={() => setService(s.id)}>
{s.name}
{t(s.name)}
</Chip>
))}
</div>
{serviceById(service) ? (
<p className="mt-3 text-xs text-muted-foreground">{serviceById(service)!.covers}</p>
<p className="mt-3 text-xs text-muted-foreground">{t(serviceById(service)!.covers)}</p>
) : null}
</Block>
@@ -169,7 +172,7 @@ export default function ConfiguratorPage() {
<div className="flex gap-2">
{ENTITY_TYPES.map((e) => (
<Chip key={e.id} active={type === e.id} onClick={() => setType(e.id)}>
{e.id}
{t(e.label)}
</Chip>
))}
</div>
@@ -203,13 +206,13 @@ export default function ConfiguratorPage() {
onChange={() => toggle(fieldIds, f.id, setFieldIds)}
/>
<span className="flex-1">
<span className="text-sm">{f.name}</span>
<span className="ml-2 text-xs text-muted-foreground">{f.sub}</span>
<span className="text-sm">{t(f.name)}</span>
<span className="ml-2 text-xs text-muted-foreground">{t(f.sub)}</span>
{f.locked ? (
<span className="ml-2 font-mono text-[10px] uppercase text-muted-foreground">{t({ en: "locked", sv: "låst" })}</span>
) : null}
</span>
<ProvenanceBadge level={f.level as ProvLevel} source={f.source} freshness={f.freshness} />
<ProvenanceBadge level={f.level as ProvLevel} source={t(f.source)} freshness={t(f.freshness)} />
</label>
);
})}
@@ -224,7 +227,7 @@ export default function ConfiguratorPage() {
active={enrichmentIds.includes(e.id)}
onClick={() => toggle(enrichmentIds, e.id, setEnrichmentIds)}
>
{e.label}
{t(e.label)}
</Chip>
))}
</div>
@@ -243,7 +246,7 @@ export default function ConfiguratorPage() {
<div className="mt-3 flex gap-2">
{FREQUENCIES.map((fr) => (
<Chip key={fr} active={frequency === fr} onClick={() => setFrequency(fr)}>
{fr}
{t(FREQUENCY_LABEL[fr])}
</Chip>
))}
</div>
@@ -280,7 +283,7 @@ export default function ConfiguratorPage() {
<div className="flex flex-wrap gap-2">
{DELIVERY_FORMATS.map((f) => (
<Chip key={f} active={formats.includes(f)} onClick={() => toggle(formats, f, setFormats)}>
{f}
{t(FORMAT_LABEL[f] ?? { en: f, sv: f })}
</Chip>
))}
</div>
@@ -297,13 +300,13 @@ export default function ConfiguratorPage() {
</div>
<dl className="divide-y divide-border">
{[
[t({ en: "Service", sv: "Tjänst" }), serviceById(service)?.name ?? service],
[t({ en: "Service", sv: "Tjänst" }), serviceById(service) ? t(serviceById(service)!.name) : service],
[t({ en: "Markets", sv: "Marknader" }), markets.join(" · ") || "—"],
[t({ en: "Type", sv: "Typ" }), type],
[t({ en: "Type", sv: "Typ" }), t(entityLabel(type))],
[t({ en: "Fields", sv: "Fält" }), String(FIELDS.filter((f) => f.locked || fieldIds.includes(f.id)).length)],
[t({ en: "Enrichment", sv: "Berikning" }), String(enrichmentIds.length)],
[t({ en: "Volume", sv: "Volym" }), new Intl.NumberFormat("sv-SE").format(volume)],
[t({ en: "Frequency", sv: "Frekvens" }), frequency],
[t({ en: "Frequency", sv: "Frekvens" }), t(FREQUENCY_LABEL[frequency])],
[t({ en: "Delivery", sv: "Leverans" }), `≈ ${est.timelineDays} ${t({ en: "days", sv: "dgr" })}`],
[t({ en: "Confidence", sv: "Konfidens" }), `${est.estConfidence}%`],
].map(([k, v]) => (
+6 -5
View File
@@ -511,10 +511,11 @@ function QualityTab({ d }: { d: Dataset }) {
}
function SchemaTab() {
const t = useT();
return (
<div>
<SectionHead num="04" kicker="Schema" />
<p className="mt-8 font-mono text-[12px] text-muted-foreground">Fält, källa, vikt. Per fält.</p>
<p className="mt-8 font-mono text-[12px] text-muted-foreground">{t({ en: "Field, source, weight. Per field.", sv: "Fält, källa, vikt. Per fält." })}</p>
<div className="mt-8">
<DataArtifact variant="schema" />
@@ -531,14 +532,14 @@ function SchemaTab() {
<span className="font-mono text-[10px] tabular-nums text-muted-foreground">
{String(i + 1).padStart(2, "0")}
</span>
<span className="text-[15px] tracking-tight">{f.name}</span>
<span className="text-[15px] tracking-tight">{t(f.name)}</span>
{f.locked ? (
<span className="font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground">låst</span>
<span className="font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground">{t({ en: "locked", sv: "låst" })}</span>
) : null}
</div>
<p className="mt-1 pl-7 font-mono text-[11px] text-muted-foreground">{f.sub}</p>
<p className="mt-1 pl-7 font-mono text-[11px] text-muted-foreground">{t(f.sub)}</p>
</div>
<ProvenanceBadge level={f.level} source={f.source} freshness={f.freshness} />
<ProvenanceBadge level={f.level} source={t(f.source)} freshness={t(f.freshness)} />
</div>
))}
</div>
+5 -5
View File
@@ -39,7 +39,7 @@ export default function ServicesPage() {
return (
<section key={flow} className="mt-16">
<div className="flex items-baseline justify-between border-b border-border pb-4">
<h2 className="font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">{FLOW_LABEL[flow]}</h2>
<h2 className="font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">{t(FLOW_LABEL[flow])}</h2>
<span className="font-mono text-[11px] tabular-nums text-muted-foreground/60">{String(items.length).padStart(2, "0")}</span>
</div>
<div className="grid grid-cols-1 gap-px border border-border bg-border md:grid-cols-2 lg:grid-cols-3">
@@ -47,10 +47,10 @@ export default function ServicesPage() {
const cta = ctaFor(flow, s.id);
return (
<div key={s.id} className={`reveal delay-${(i % 6) + 1} group flex flex-col bg-background p-6 transition-colors hover:bg-secondary/40`}>
<h3 className="text-lg font-semibold tracking-[-0.01em]">{s.name}</h3>
<p className="mt-1 font-mono text-[11px] uppercase tracking-[0.06em] text-muted-foreground">{s.tagline}</p>
<p className="mt-4 flex-1 text-sm text-muted-foreground">{s.covers}</p>
<p className="mt-4 font-mono text-[10.5px] uppercase tracking-[0.08em] text-muted-foreground/70">→ {s.deliverable}</p>
<h3 className="text-lg font-semibold tracking-[-0.01em]">{t(s.name)}</h3>
<p className="mt-1 font-mono text-[11px] uppercase tracking-[0.06em] text-muted-foreground">{t(s.tagline)}</p>
<p className="mt-4 flex-1 text-sm text-muted-foreground">{t(s.covers)}</p>
<p className="mt-4 font-mono text-[10.5px] uppercase tracking-[0.08em] text-muted-foreground/70">→ {t(s.deliverable)}</p>
<Link href={cta.href} className="mt-5 inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.1em] underline-offset-4 hover:underline">
{t(cta.label)}
<span aria-hidden className="transition-transform duration-300 group-hover:translate-x-1">→</span>
+1 -1
View File
@@ -32,7 +32,7 @@ export async function verifySession(token: string | undefined | null): Promise<R
if (!token || !process.env.AUTH_SECRET) return null;
const [body, sig] = token.split(".");
if (!body || !sig) return null;
const ok = await crypto.subtle.verify("HMAC", await hmacKey(), fromB64url(sig), enc.encode(body));
const ok = await crypto.subtle.verify("HMAC", await hmacKey(), fromB64url(sig) as BufferSource, enc.encode(body));
if (!ok) return null;
try {
return JSON.parse(new TextDecoder().decode(fromB64url(body)));
+67 -43
View File
@@ -1,84 +1,99 @@
// Central mock data + the live-estimate pricing model shared by the order flow.
// No real backend yet — the configurator's price/timeline/confidence come from
// computeEstimate(); replace with a call to the FastAPI /estimates later.
//
// i18n: IDs/values (used as keys + in pricing logic) stay stable; only DISPLAY
// labels are localized as { en, sv } and resolved with useT() at the render site.
export type ProvLevel = 0 | 1 | 2 | 3; // 0 licens/ej · 1 en källa · 2 delvis · 3 flera källor
export type ProvLevel = 0 | 1 | 2 | 3; // 0 licence/none · 1 one source · 2 partial · 3 multi-source
export type Loc = { en: string; sv: string };
export interface Market {
code: string;
name: string;
name: Loc;
live: boolean;
coverage: number; // %
}
// 20 European markets (Nordics live first, rest "kommande").
// 20 European markets (Nordics live first, rest coming).
export const MARKETS: Market[] = [
{ code: "SE", name: "Sverige", live: true, coverage: 98 },
{ code: "NO", name: "Norge", live: true, coverage: 96 },
{ code: "DK", name: "Danmark", live: true, coverage: 94 },
{ code: "FI", name: "Finland", live: true, coverage: 89 },
{ code: "IS", name: "Island", live: false, coverage: 0 },
{ code: "NL", name: "Nederländerna", live: false, coverage: 0 },
{ code: "DE", name: "Tyskland", live: false, coverage: 0 },
{ code: "BE", name: "Belgien", live: false, coverage: 0 },
{ code: "IE", name: "Irland", live: false, coverage: 0 },
{ code: "FR", name: "Frankrike", live: false, coverage: 0 },
{ code: "GB", name: "Storbritannien", live: false, coverage: 0 },
{ code: "AT", name: "Österrike", live: false, coverage: 0 },
{ code: "CH", name: "Schweiz", live: false, coverage: 0 },
{ code: "IT", name: "Italien", live: false, coverage: 0 },
{ code: "ES", name: "Spanien", live: false, coverage: 0 },
{ code: "PT", name: "Portugal", live: false, coverage: 0 },
{ code: "PL", name: "Polen", live: false, coverage: 0 },
{ code: "CZ", name: "Tjeckien", live: false, coverage: 0 },
{ code: "HU", name: "Ungern", live: false, coverage: 0 },
{ code: "LU", name: "Luxemburg", live: false, coverage: 0 },
{ code: "SE", name: { en: "Sweden", sv: "Sverige" }, live: true, coverage: 98 },
{ code: "NO", name: { en: "Norway", sv: "Norge" }, live: true, coverage: 96 },
{ code: "DK", name: { en: "Denmark", sv: "Danmark" }, live: true, coverage: 94 },
{ code: "FI", name: { en: "Finland", sv: "Finland" }, live: true, coverage: 89 },
{ code: "IS", name: { en: "Iceland", sv: "Island" }, live: false, coverage: 0 },
{ code: "NL", name: { en: "Netherlands", sv: "Nederländerna" }, live: false, coverage: 0 },
{ code: "DE", name: { en: "Germany", sv: "Tyskland" }, live: false, coverage: 0 },
{ code: "BE", name: { en: "Belgium", sv: "Belgien" }, live: false, coverage: 0 },
{ code: "IE", name: { en: "Ireland", sv: "Irland" }, live: false, coverage: 0 },
{ code: "FR", name: { en: "France", sv: "Frankrike" }, live: false, coverage: 0 },
{ code: "GB", name: { en: "United Kingdom", sv: "Storbritannien" }, live: false, coverage: 0 },
{ code: "AT", name: { en: "Austria", sv: "Österrike" }, live: false, coverage: 0 },
{ code: "CH", name: { en: "Switzerland", sv: "Schweiz" }, live: false, coverage: 0 },
{ code: "IT", name: { en: "Italy", sv: "Italien" }, live: false, coverage: 0 },
{ code: "ES", name: { en: "Spain", sv: "Spanien" }, live: false, coverage: 0 },
{ code: "PT", name: { en: "Portugal", sv: "Portugal" }, live: false, coverage: 0 },
{ code: "PL", name: { en: "Poland", sv: "Polen" }, live: false, coverage: 0 },
{ code: "CZ", name: { en: "Czechia", sv: "Tjeckien" }, live: false, coverage: 0 },
{ code: "HU", name: { en: "Hungary", sv: "Ungern" }, live: false, coverage: 0 },
{ code: "LU", name: { en: "Luxembourg", sv: "Luxemburg" }, live: false, coverage: 0 },
];
// Stable IDs (also the stored value + pricing key). Display via entityLabel().
export type EntityType = "Bolag" | "Beslutsfattare" | "Fordon";
export const ENTITY_TYPES: { id: EntityType; mult: number }[] = [
{ id: "Bolag", mult: 1.0 },
{ id: "Beslutsfattare", mult: 1.4 },
{ id: "Fordon", mult: 1.2 },
export const ENTITY_TYPES: { id: EntityType; label: Loc; mult: number }[] = [
{ id: "Bolag", label: { en: "Companies", sv: "Bolag" }, mult: 1.0 },
{ id: "Beslutsfattare", label: { en: "Decision-makers", sv: "Beslutsfattare" }, mult: 1.4 },
{ id: "Fordon", label: { en: "Vehicles", sv: "Fordon" }, mult: 1.2 },
];
export function entityLabel(id: EntityType): Loc {
return ENTITY_TYPES.find((e) => e.id === id)?.label ?? { en: id, sv: id };
}
export interface Field {
id: string;
name: string;
sub: string;
name: Loc;
sub: Loc;
level: ProvLevel;
source: string;
freshness: string;
source: Loc;
freshness: Loc;
weight: number;
locked?: boolean;
}
// Each field carries its honest provenance: source · freshness · confidence level.
export const FIELDS: Field[] = [
{ id: "identity", name: "Identitet & säte", sub: "org.nr, namn, adress", level: 3, source: "GLEIF · Brreg", freshness: "daglig", weight: 1.0, locked: true },
{ id: "lei", name: "LEI / cross-border-nyckel", sub: "GLEIF golden copy", level: 3, source: "GLEIF", freshness: "daglig", weight: 0.8 },
{ id: "financials", name: "Finansiellt", sub: "omsättning, resultat, nyckeltal", level: 2, source: "register", freshness: "årlig", weight: 1.6 },
{ id: "ubo", name: "Verklig huvudman (UBO)", sub: "ägarkedja", level: 2, source: "register", freshness: "vid ändring", weight: 1.4 },
{ id: "contact", name: "Kontakt: e-post & telefon", sub: "licens krävs i DE/FR", level: 1, source: "publik webb", freshness: "veckovis", weight: 1.8 },
{ id: "intent", name: "Köpsignaler & intent", sub: "jobbannonser, nyheter, tech", level: 1, source: "webb", freshness: "veckovis", weight: 1.5 },
{ id: "identity", name: { en: "Identity & registered office", sv: "Identitet & säte" }, sub: { en: "reg. no., name, address", sv: "org.nr, namn, adress" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, freshness: { en: "daily", sv: "daglig" }, weight: 1.0, locked: true },
{ id: "lei", name: { en: "LEI / cross-border key", sv: "LEI / cross-border-nyckel" }, sub: { en: "GLEIF golden copy", sv: "GLEIF golden copy" }, level: 3, source: { en: "GLEIF", sv: "GLEIF" }, freshness: { en: "daily", sv: "daglig" }, weight: 0.8 },
{ id: "financials", name: { en: "Financials", sv: "Finansiellt" }, sub: { en: "revenue, profit, key ratios", sv: "omsättning, resultat, nyckeltal" }, level: 2, source: { en: "register", sv: "register" }, freshness: { en: "annual", sv: "årlig" }, weight: 1.6 },
{ id: "ubo", name: { en: "Beneficial owner (UBO)", sv: "Verklig huvudman (UBO)" }, sub: { en: "ownership chain", sv: "ägarkedja" }, level: 2, source: { en: "register", sv: "register" }, freshness: { en: "on change", sv: "vid ändring" }, weight: 1.4 },
{ id: "contact", name: { en: "Contact: email & phone", sv: "Kontakt: e-post & telefon" }, sub: { en: "licence required in DE/FR", sv: "licens krävs i DE/FR" }, level: 1, source: { en: "public web", sv: "publik webb" }, freshness: { en: "weekly", sv: "veckovis" }, weight: 1.8 },
{ id: "intent", name: { en: "Buying signals & intent", sv: "Köpsignaler & intent" }, sub: { en: "job ads, news, tech", sv: "jobbannonser, nyheter, tech" }, level: 1, source: { en: "web", sv: "webb" }, freshness: { en: "weekly", sv: "veckovis" }, weight: 1.5 },
];
export interface Enrichment {
id: string;
label: string;
label: Loc;
weight: number;
}
export const ENRICHMENTS: Enrichment[] = [
{ id: "dedup", label: "Dedup mot min fil", weight: 0.1 },
{ id: "crm", label: "Matcha & berika min CRM-export", weight: 0.2 },
{ id: "sanctions", label: "Sanktioner / PEP-flagg", weight: 0.15 },
{ id: "nace", label: "Branschklassning (NACE/SNI)", weight: 0.1 },
{ id: "tech", label: "Webbplats & teknikstack", weight: 0.15 },
{ id: "dedup", label: { en: "Dedup against my file", sv: "Dedup mot min fil" }, weight: 0.1 },
{ id: "crm", label: { en: "Match & enrich my CRM export", sv: "Matcha & berika min CRM-export" }, weight: 0.2 },
{ id: "sanctions", label: { en: "Sanctions / PEP flag", sv: "Sanktioner / PEP-flagg" }, weight: 0.15 },
{ id: "nace", label: { en: "Industry class (NACE/SNI)", sv: "Branschklassning (NACE/SNI)" }, weight: 0.1 },
{ id: "tech", label: { en: "Website & tech stack", sv: "Webbplats & teknikstack" }, weight: 0.15 },
];
// Stable values (stored + used by computeEstimate); display via FREQUENCY_LABEL.
export type Frequency = "Engångs" | "Veckovis" | "Dagligt flöde";
export const FREQUENCIES: Frequency[] = ["Engångs", "Veckovis", "Dagligt flöde"];
export const FREQUENCY_LABEL: Record<Frequency, Loc> = {
"Engångs": { en: "One-time", sv: "Engångs" },
"Veckovis": { en: "Weekly", sv: "Veckovis" },
"Dagligt flöde": { en: "Daily feed", sv: "Dagligt flöde" },
};
// Stable values (stored in config.formats); display via FORMAT_LABEL.
export const DELIVERY_FORMATS = [
"CSV",
"Tabellvy (NocoDB)",
@@ -88,6 +103,15 @@ export const DELIVERY_FORMATS = [
"API / webhook",
"Push → FiveCRM",
];
export const FORMAT_LABEL: Record<string, Loc> = {
"CSV": { en: "CSV", sv: "CSV" },
"Tabellvy (NocoDB)": { en: "Table view (NocoDB)", sv: "Tabellvy (NocoDB)" },
"Parquet": { en: "Parquet", sv: "Parquet" },
"JSON": { en: "JSON", sv: "JSON" },
"Dashboard (Metabase)": { en: "Dashboard (Metabase)", sv: "Dashboard (Metabase)" },
"API / webhook": { en: "API / webhook", sv: "API / webhook" },
"Push → FiveCRM": { en: "Push → FiveCRM", sv: "Push → FiveCRM" },
};
const FIELD_RATE = 240; // kr per field-weight per 1000 rows per market
+1 -1
View File
@@ -49,7 +49,7 @@ export async function createOrder(flow: string, config: OrderConfig): Promise<Or
if (sql) {
const [row] = await sql<{ order_id: string; created_at: string }[]>`
INSERT INTO orders (flow, status, config, total_ore, deposit_ore, balance_ore, est_days, est_confidence)
VALUES (${flow}, 'draft', ${sql.json(config as object)}, ${est.price * 100}, ${est.deposit * 100},
VALUES (${flow}, 'draft', ${sql.json(config as Parameters<typeof sql.json>[0])}, ${est.price * 100}, ${est.deposit * 100},
${est.remainder * 100}, ${est.timelineDays}, ${est.estConfidence / 100})
RETURNING order_id, created_at`;
return {
+24 -19
View File
@@ -1,37 +1,42 @@
// The 0pt service catalog. Every sellable service maps to one of the three flows
// (docs/01): catalog product, custom job, subscription. Categories are tags within
// those flows — not separate machinery.
//
// i18n: id/flow stay stable (keys); display fields are localized { en, sv } and
// resolved with useT() at the render site.
import type { Loc } from "@/lib/data";
export type Flow = "catalog" | "custom" | "subscription";
export interface Service {
id: string;
name: string;
tagline: string; // terse, confident
covers: string; // one line of scope
name: Loc;
tagline: Loc; // terse, confident
covers: Loc; // one line of scope
flow: Flow;
deliverable: string;
deliverable: Loc;
mult: number; // price multiplier vs a base dataset job
}
export const SERVICES: Service[] = [
{ id: "data", name: "Dataleverans", tagline: "Färdig eller skräddarsydd.", covers: "Bolag, beslutsfattare, fordon, finansiellt — per fält med härkomst.", flow: "custom", deliverable: "CSV · Parquet · JSON · API", mult: 1.0 },
{ id: "scraping", name: "Webbskrapning", tagline: "Lagligt insamlat.", covers: "Officiella API:er och tydligt publika sidor. Aldrig bakom inloggning.", flow: "custom", deliverable: "Strukturerat dataset + källa per fält", mult: 1.1 },
{ id: "mapping", name: "Mappning & matchning", tagline: "Era data mot världen.", covers: "Koppla era poster mot orgnr/LEI och externa nycklar. Cross-border.", flow: "custom", deliverable: "Matchad fil + matchningskonfidens", mult: 1.2 },
{ id: "normalization", name: "Normalisering", tagline: "Städat. Standardiserat.", covers: "Format, dubbletter, validering (e-post/telefon), enhetlig taxonomi.", flow: "custom", deliverable: "Ren datamodell + kvalitetsrapport", mult: 1.1 },
{ id: "consolidation", name: "Konsolidering → CRM/ERP", tagline: "En sanning. Rätt system.", covers: "Slå ihop admindata och pusha till HubSpot, Salesforce, Dynamics, Pipedrive, FiveCRM.", flow: "custom", deliverable: "Konsoliderad data + push/sync", mult: 1.4 },
{ id: "documents", name: "Dokument & OCR", tagline: "Pixlar till poster.", covers: "PDF, bilder, scannade myndighetsdokument → strukturerade fält.", flow: "custom", deliverable: "Strukturerat dataset från dokument", mult: 1.3 },
{ id: "enrichment", name: "Berikning", tagline: "Fyll luckorna.", covers: "Kontakter, firmografik, finansiellt, köpsignaler — med laglig grund.", flow: "subscription", deliverable: "Berikad fil eller löpande flöde", mult: 1.2 },
{ id: "icp", name: "ICP & leads", tagline: "Rätt målgrupp.", covers: "Målgruppslistor med beslutsfattare, härkomst per kontakt.", flow: "catalog", deliverable: "Lead-lista + export till CRM", mult: 1.2 },
{ id: "ml", name: "ML-jobb", tagline: "Modeller på er data.", covers: "Scoring, matchning, prognos, klustring — tränat på era + våra data.", flow: "custom", deliverable: "Modell + prediktioner + API", mult: 1.8 },
{ id: "bi", name: "BI & marknadsanalys", tagline: "Beslut, inte bara siffror.", covers: "Dashboards, marknadsöversikter, konkurrent- och täckningsanalys.", flow: "custom", deliverable: "Metabase-dashboard + rapport", mult: 1.5 },
{ id: "feeds", name: "Flöden & övervakning", tagline: "Alltid färskt.", covers: "Återkommande leverans, freshness-bevakning, webhook vid förändring.", flow: "subscription", deliverable: "Webhook/feed + larm", mult: 1.3 },
{ id: "data", name: { en: "Data delivery", sv: "Dataleverans" }, tagline: { en: "Ready-made or bespoke.", sv: "Färdig eller skräddarsydd." }, covers: { en: "Companies, decision-makers, vehicles, financials — per field, with provenance.", sv: "Bolag, beslutsfattare, fordon, finansiellt — per fält med härkomst." }, flow: "custom", deliverable: { en: "CSV · Parquet · JSON · API", sv: "CSV · Parquet · JSON · API" }, mult: 1.0 },
{ id: "scraping", name: { en: "Web scraping", sv: "Webbskrapning" }, tagline: { en: "Legally collected.", sv: "Lagligt insamlat." }, covers: { en: "Official APIs and clearly public pages. Never behind a login.", sv: "Officiella API:er och tydligt publika sidor. Aldrig bakom inloggning." }, flow: "custom", deliverable: { en: "Structured dataset + source per field", sv: "Strukturerat dataset + källa per fält" }, mult: 1.1 },
{ id: "mapping", name: { en: "Mapping & matching", sv: "Mappning & matchning" }, tagline: { en: "Your data against the world.", sv: "Era data mot världen." }, covers: { en: "Link your records to org. no./LEI and external keys. Cross-border.", sv: "Koppla era poster mot orgnr/LEI och externa nycklar. Cross-border." }, flow: "custom", deliverable: { en: "Matched file + match confidence", sv: "Matchad fil + matchningskonfidens" }, mult: 1.2 },
{ id: "normalization", name: { en: "Normalization", sv: "Normalisering" }, tagline: { en: "Cleaned. Standardized.", sv: "Städat. Standardiserat." }, covers: { en: "Format, duplicates, validation (email/phone), unified taxonomy.", sv: "Format, dubbletter, validering (e-post/telefon), enhetlig taxonomi." }, flow: "custom", deliverable: { en: "Clean data model + quality report", sv: "Ren datamodell + kvalitetsrapport" }, mult: 1.1 },
{ id: "consolidation", name: { en: "Consolidation → CRM/ERP", sv: "Konsolidering → CRM/ERP" }, tagline: { en: "One truth. The right system.", sv: "En sanning. Rätt system." }, covers: { en: "Merge admin data and push to HubSpot, Salesforce, Dynamics, Pipedrive, FiveCRM.", sv: "Slå ihop admindata och pusha till HubSpot, Salesforce, Dynamics, Pipedrive, FiveCRM." }, flow: "custom", deliverable: { en: "Consolidated data + push/sync", sv: "Konsoliderad data + push/sync" }, mult: 1.4 },
{ id: "documents", name: { en: "Documents & OCR", sv: "Dokument & OCR" }, tagline: { en: "Pixels to records.", sv: "Pixlar till poster." }, covers: { en: "PDFs, images, scanned official documents → structured fields.", sv: "PDF, bilder, scannade myndighetsdokument → strukturerade fält." }, flow: "custom", deliverable: { en: "Structured dataset from documents", sv: "Strukturerat dataset från dokument" }, mult: 1.3 },
{ id: "enrichment", name: { en: "Enrichment", sv: "Berikning" }, tagline: { en: "Fill the gaps.", sv: "Fyll luckorna." }, covers: { en: "Contacts, firmographics, financials, buying signals — with a lawful basis.", sv: "Kontakter, firmografik, finansiellt, köpsignaler — med laglig grund." }, flow: "subscription", deliverable: { en: "Enriched file or ongoing feed", sv: "Berikad fil eller löpande flöde" }, mult: 1.2 },
{ id: "icp", name: { en: "ICP & leads", sv: "ICP & leads" }, tagline: { en: "The right audience.", sv: "Rätt målgrupp." }, covers: { en: "Target lists with decision-makers, provenance per contact.", sv: "Målgruppslistor med beslutsfattare, härkomst per kontakt." }, flow: "catalog", deliverable: { en: "Lead list + export to CRM", sv: "Lead-lista + export till CRM" }, mult: 1.2 },
{ id: "ml", name: { en: "ML jobs", sv: "ML-jobb" }, tagline: { en: "Models on your data.", sv: "Modeller på er data." }, covers: { en: "Scoring, matching, forecasting, clustering — trained on your + our data.", sv: "Scoring, matchning, prognos, klustring — tränat på era + våra data." }, flow: "custom", deliverable: { en: "Model + predictions + API", sv: "Modell + prediktioner + API" }, mult: 1.8 },
{ id: "bi", name: { en: "BI & market analysis", sv: "BI & marknadsanalys" }, tagline: { en: "Decisions, not just numbers.", sv: "Beslut, inte bara siffror." }, covers: { en: "Dashboards, market overviews, competitor and coverage analysis.", sv: "Dashboards, marknadsöversikter, konkurrent- och täckningsanalys." }, flow: "custom", deliverable: { en: "Metabase dashboard + report", sv: "Metabase-dashboard + rapport" }, mult: 1.5 },
{ id: "feeds", name: { en: "Feeds & monitoring", sv: "Flöden & övervakning" }, tagline: { en: "Always fresh.", sv: "Alltid färskt." }, covers: { en: "Recurring delivery, freshness monitoring, webhook on change.", sv: "Återkommande leverans, freshness-bevakning, webhook vid förändring." }, flow: "subscription", deliverable: { en: "Webhook/feed + alerts", sv: "Webhook/feed + larm" }, mult: 1.3 },
];
export const FLOW_LABEL: Record<Flow, string> = {
catalog: "Katalog",
custom: "Skräddarsytt",
subscription: "Flöde",
export const FLOW_LABEL: Record<Flow, Loc> = {
catalog: { en: "Catalog", sv: "Katalog" },
custom: { en: "Bespoke", sv: "Skräddarsytt" },
subscription: { en: "Feed", sv: "Flöde" },
};
// CRM/ERP destinations for the consolidation/push services.