From a928ae4e98838254ec77482e6816dca0f79ae77a Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Thu, 25 Jun 2026 16:31:07 +0200 Subject: [PATCH] fix(i18n): localize /katalog filters, /katalog/[id] full rewrite, /portal/subscriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three missed files from the i18n pass: - /katalog: FILTERS had hardcoded Swedish labels (Alla/Bolag/Beslutsfattare/Fordon/Finansiellt) - /katalog/[id]: entire page was Swedish — CATALOG data, TABS (with variable-shadowing bug where map((t) shadowed useT()), SectionHead, all 5 tab bodies, order rail, PROVENANCE_LEGEND rendered as raw Loc object. Converted to stable TabKey enum + all Loc. - /portal/subscriptions: useT imported but never called; title/labels/options all Swedish. tsc 0 + build green. Co-Authored-By: Claude Sonnet 4.6 --- app/katalog/[id]/page.tsx | 347 ++++++++++++++---------------- app/katalog/page.tsx | 14 +- app/portal/subscriptions/page.tsx | 48 ++++- 3 files changed, 207 insertions(+), 202 deletions(-) diff --git a/app/katalog/[id]/page.tsx b/app/katalog/[id]/page.tsx index 075171e..5660dfd 100644 --- a/app/katalog/[id]/page.tsx +++ b/app/katalog/[id]/page.tsx @@ -10,24 +10,24 @@ import { ProvenanceBadge, CellProvenance, PROVENANCE_LEGEND } from "@/components import { LoadingSkeleton, EmptyState, ErrorState } from "@/components/states"; import { useReveal } from "@/hooks/use-reveal"; import { FIELDS, SAMPLE_ROWS, MARKETS, formatSEK, type ProvLevel } from "@/lib/data"; +import type { Loc } from "@/lib/data"; // ───────────────────────────────────────────────────────────────────────────── // Inline catalog. No detail API yet — keyed by slugified id with a safe fallback. -// Mirrors the slugs that the catalog cards (featured-products-section) imply. // ───────────────────────────────────────────────────────────────────────────── interface Dataset { id: string; markets: string; marketCodes: string[]; - title: string; - blurb: string; - count: string; + title: Loc; + blurb: Loc; + count: Loc; rows: number; - freshness: string; + freshness: Loc; level: ProvLevel; source: string; - cat: string; + cat: Loc; price: number; artifact: "table" | "map" | "schema" | "chart"; } @@ -37,14 +37,14 @@ const CATALOG: Dataset[] = [ id: "nordisk-bolagsdata", markets: "SE · NO · DK · FI", marketCodes: ["SE", "NO", "DK", "FI"], - title: "Nordisk bolagsdata", - blurb: "Officiella register. Per fält.", - count: "4,9M poster", + title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, + blurb: { en: "Official registers. Per field.", sv: "Officiella register. Per fält." }, + count: { en: "4.9M records", sv: "4,9M poster" }, rows: 4900000, - freshness: "daglig", + freshness: { en: "daily", sv: "daglig" }, level: 3, source: "GLEIF · Brreg", - cat: "Bolag", + cat: { en: "Companies", sv: "Bolag" }, price: 18400, artifact: "table", }, @@ -52,14 +52,14 @@ const CATALOG: Dataset[] = [ id: "beslutsfattare-tech", markets: "SE · NO · DK · FI", marketCodes: ["SE", "NO", "DK", "FI"], - title: "Beslutsfattare i tech", - blurb: "Roll och väg. Källa per fält.", - count: "182k poster", + title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, + blurb: { en: "Role and path. Source per field.", sv: "Roll och väg. Källa per fält." }, + count: { en: "182k records", sv: "182k poster" }, rows: 182000, - freshness: "veckovis", + freshness: { en: "weekly", sv: "veckovis" }, level: 1, - source: "publik webb", - cat: "Beslutsfattare", + source: "public web", + cat: { en: "Decision-makers", sv: "Beslutsfattare" }, price: 9600, artifact: "schema", }, @@ -67,14 +67,14 @@ const CATALOG: Dataset[] = [ id: "fordon-flotta", markets: "SE", marketCodes: ["SE"], - title: "Fordon & flotta", - blurb: "Tekniska data. VehIQ-synergin.", - count: "6,1M poster", + title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, + blurb: { en: "Technical data. VehIQ synergy.", sv: "Tekniska data. VehIQ-synergin." }, + count: { en: "6.1M records", sv: "6,1M poster" }, rows: 6100000, - freshness: "daglig", + freshness: { en: "daily", sv: "daglig" }, level: 2, source: "register", - cat: "Fordon", + cat: { en: "Vehicles", sv: "Fordon" }, price: 12200, artifact: "map", }, @@ -82,21 +82,29 @@ const CATALOG: Dataset[] = [ id: "finansiella-serier", markets: "SE · NO · DK · FI", marketCodes: ["SE", "NO", "DK", "FI"], - title: "Finansiella serier", - blurb: "Omsättning, resultat, nyckeltal.", - count: "12 år", + title: { en: "Financial series", sv: "Finansiella serier" }, + blurb: { en: "Revenue, result, KPIs.", sv: "Omsättning, resultat, nyckeltal." }, + count: { en: "12 years", sv: "12 år" }, rows: 240000, - freshness: "årlig", + freshness: { en: "annual", sv: "årlig" }, level: 2, source: "register", - cat: "Finansiellt", + cat: { en: "Financials", sv: "Finansiellt" }, price: 14800, artifact: "chart", }, ]; -const TABS = ["Översikt", "Exempeldata", "Kvalitet", "Schema", "Täckning", "Pris"] as const; -type Tab = (typeof TABS)[number]; +// Stable tab keys — never change, display labels are localized separately +type TabKey = "overview" | "samples" | "quality" | "schema" | "coverage" | "price"; +const TABS: { key: TabKey; label: Loc }[] = [ + { key: "overview", label: { en: "Overview", sv: "Översikt" } }, + { key: "samples", label: { en: "Sample data", sv: "Exempeldata" } }, + { key: "quality", label: { en: "Quality", sv: "Kvalitet" } }, + { key: "schema", label: { en: "Schema", sv: "Schema" } }, + { key: "coverage", label: { en: "Coverage", sv: "Täckning" } }, + { key: "price", label: { en: "Price", sv: "Pris" } }, +]; function findDataset(raw: string | undefined): Dataset | undefined { if (!raw) return undefined; @@ -111,27 +119,25 @@ export default function DatasetDetailPage() { const [status, setStatus] = useState<"loading" | "ready" | "empty" | "error">("loading"); const [dataset, setDataset] = useState(null); - const [tab, setTab] = useState("Översikt"); + const [tab, setTab] = useState("overview"); const attempt = useRef(0); - // Simulated detail fetch. Replace with GET /catalog/:id when the API lands. const load = useCallback(() => { setStatus("loading"); attempt.current += 1; const run = attempt.current; - const t = setTimeout(() => { + const timer = setTimeout(() => { if (run !== attempt.current) return; const hit = findDataset(id); if (hit) { setDataset(hit); setStatus("ready"); } else { - // Known id-space, no match → empty (not an error). setDataset(null); setStatus("empty"); } }, 540); - return () => clearTimeout(t); + return () => clearTimeout(timer); }, [id]); useEffect(() => { @@ -157,12 +163,9 @@ export default function DatasetDetailPage() { {status === "error" ? (
- +
- + {t({ en: "← Back to the catalog", sv: "← Till katalogen" })}
@@ -172,19 +175,13 @@ export default function DatasetDetailPage() { {status === "empty" ? (
- + {t({ en: "← Back to the catalog", sv: "← Till katalogen" })}
@@ -199,15 +196,7 @@ export default function DatasetDetailPage() { // ───────────────────────────────────────────────────────────────────────────── -function DatasetView({ - d, - tab, - setTab, -}: { - d: Dataset; - tab: Tab; - setTab: (t: Tab) => void; -}) { +function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k: TabKey) => void }) { const t = useT(); const deposit = Math.round(d.price / 2); const remainder = d.price - deposit; @@ -220,50 +209,42 @@ function DatasetView({ {t({ en: "Catalog", sv: "Katalog" })} / - {d.title} + {t(d.title)} - {/* masthead — oversized faint numeral as texture */} + {/* masthead */}
- + 02

- Katalog — {d.cat} + {t({ en: "Catalog", sv: "Katalog" })} — {t(d.cat)}

- {d.title}. + {t(d.title)}.

-

{d.blurb}

+

{t(d.blurb)}

- - - {d.markets} - + + {d.markets} · - {d.count} + {t(d.count)}
- {/* two columns — content + sticky order rail */} + {/* two columns */}
- {/* tabs — shared baseline, single active marker */} -
- {/* sticky right — order panel */} + {/* sticky order rail */} @@ -343,43 +319,48 @@ function DatasetView({ ); } -// ── Tab bodies ─────────────────────────────────────────────────────────────── +// ── Shared section header ──────────────────────────────────────────────────── -function SectionHead({ num, kicker }: { num: string; kicker: string }) { +function SectionHead({ num, kicker }: { num: string; kicker: Loc }) { + const t = useT(); return (
-

{kicker}

- - {num} - +

{t(kicker)}

+ {num}
); } +// ── Tab bodies ─────────────────────────────────────────────────────────────── + function OverviewTab({ d }: { d: Dataset }) { - const stats: [string, string][] = [ - [d.count, "omfattning"], - [`${d.marketCodes.length}`, "marknader"], - [d.freshness, "uppdatering"], + const t = useT(); + const stats: [Loc, Loc][] = [ + [d.count, { en: "scope", sv: "omfattning" }], + [{ en: String(d.marketCodes.length), + sv: String(d.marketCodes.length) }, { en: "markets", sv: "marknader" }], + [d.freshness, { en: "update", sv: "uppdatering" }], ]; return (
- +

- Strukturerat per fält. Varje värde bär källa, färskhet och konfidens — så att du ser var det - kommer ifrån innan du litar på det. + {t({ + en: "Structured per field. Every value carries source, freshness, and confidence — so you see where it comes from before you rely on it.", + sv: "Strukturerat per fält. Varje värde bär källa, färskhet och konfidens — så att du ser var det kommer ifrån innan du litar på det.", + })}

{stats.map(([n, l], i) => ( -
+

- {n} + {t(n)}

-

{l}

+

{t(l)}

))}
@@ -392,29 +373,33 @@ function OverviewTab({ d }: { d: Dataset }) { } function SampleTab({ d }: { d: Dataset }) { + const t = useT(); + const headers: Loc[] = [ + { en: "Org.no", sv: "Org.nr" }, + { en: "Company", sv: "Bolag" }, + { en: "HQ", sv: "Säte" }, + { en: "Employees", sv: "Anställda" }, + { en: "Revenue (tSEK)", sv: "Oms (tkr)" }, + { en: "Email", sv: "E-post" }, + ]; return (
- +

- Fem rader. Provenans per cell. + {t({ en: "Five rows. Provenance per cell.", sv: "Fem rader. Provenans per cell." })}

- {/* primary view — generative table artifact */}
- {/* concrete sample rows from lib/data */}
- {["Org.nr", "Bolag", "Säte", "Anställda", "Oms (tkr)", "E-post"].map((h) => ( - ))} @@ -422,9 +407,7 @@ function SampleTab({ d }: { d: Dataset }) { {SAMPLE_ROWS.map((r) => ( - + @@ -447,32 +430,35 @@ function SampleTab({ d }: { d: Dataset }) {
- {h} + {headers.map((h) => ( + + {t(h)}
- {r.orgnr} - {r.orgnr} {r.bolag} {r.sate} @@ -437,7 +420,7 @@ function SampleTab({ d }: { d: Dataset }) { {r.epost ? ( {r.epost} ) : ( - ej tillgänglig + {t({ en: "not available", sv: "ej tillgänglig" })} )}

- Tom = licens krävs i marknaden. Inget gissat. Källa: {d.source}. + {t({ en: "Empty = licence required in that market. Nothing guessed. Source:", sv: "Tom = licens krävs i marknaden. Inget gissat. Källa:" })} {d.source}.

); } function QualityTab({ d }: { d: Dataset }) { - const metrics: [string, number, ProvLevel][] = [ - ["Identifierad entitet", 99, 3], - ["LEI-länkad", 92, 3], - ["Dubbletter borttagna", 100, 3], - ["E-post verifierad (MX)", 71, 2], - ["Källor överens", 88, d.level], + const t = useT(); + const metrics: [Loc, number, ProvLevel][] = [ + [{ en: "Identified entity", sv: "Identifierad entitet" }, 99, 3], + [{ en: "LEI-linked", sv: "LEI-länkad" }, 92, 3], + [{ en: "Duplicates removed", sv: "Dubbletter borttagna" }, 100, 3], + [{ en: "Email verified (MX)", sv: "E-post verifierad (MX)" }, 71, 2], + [{ en: "Sources agree", sv: "Källor överens" }, 88, d.level], ]; return (
- +

- Mätt, inte påstått. Varje rad passerar expectations, dedup och en konfidenströskel innan den - levereras. + {t({ + en: "Measured, not claimed. Every row passes expectations, dedup and a confidence threshold before delivery.", + sv: "Mätt, inte påstått. Varje rad passerar expectations, dedup och en konfidenströskel innan den levereras.", + })}

{metrics.map(([label, pct, level]) => ( -
- {label} +
+ {t(label)}
- {/* provenance legend */}
-

Härkomst

+

{t({ en: "Provenance", sv: "Härkomst" })}

{PROVENANCE_LEGEND.map((p) => (
@@ -501,7 +486,7 @@ function QualityTab({ d }: { d: Dataset }) { border: p.level === 0 ? "1px solid var(--prov-0)" : "none", }} /> - {p.text} + {t(p.text)}
))}
@@ -514,7 +499,7 @@ function SchemaTab() { const t = useT(); return (
- +

{t({ en: "Field, source, weight. Per field.", sv: "Fält, källa, vikt. Per fält." })}

@@ -523,15 +508,10 @@ function SchemaTab() {
{FIELDS.map((f, i) => ( -
+
- - {String(i + 1).padStart(2, "0")} - + {String(i + 1).padStart(2, "0")} {t(f.name)} {f.locked ? ( {t({ en: "locked", sv: "låst" })} @@ -548,12 +528,16 @@ function SchemaTab() { } function CoverageTab({ d }: { d: Dataset }) { + const t = useT(); const inSet = (code: string) => d.marketCodes.includes(code); return (
- +

- Var datasetet lever idag. Markerade marknader ingår — resten är kommande. + {t({ + en: "Where the dataset lives today. Marked markets are included — the rest is coming.", + sv: "Var datasetet lever idag. Markerade marknader ingår — resten är kommande.", + })}

@@ -564,23 +548,19 @@ function CoverageTab({ d }: { d: Dataset }) { {MARKETS.slice(0, 12).map((m) => { const included = inSet(m.code) && m.live; return ( -
+

{m.code}

{included ? ( - + ) : null}

- {included ? `${m.coverage}%` : m.live ? "ej i set" : "kommande"} + {included + ? `${m.coverage}%` + : m.live + ? t({ en: "not in set", sv: "ej i set" }) + : t({ en: "upcoming", sv: "kommande" })}

); @@ -591,25 +571,26 @@ function CoverageTab({ d }: { d: Dataset }) { } function PriceTab({ price, deposit, remainder }: { price: number; deposit: number; remainder: number }) { - const lines: [string, string, boolean][] = [ - ["Listpris", formatSEK(price), false], - ["Förskott 50 % — startar produktion", formatSEK(deposit), true], - ["Vid godkänt — släpps ur escrow", formatSEK(remainder), false], + const t = useT(); + const lines: [Loc, string, boolean][] = [ + [{ en: "List price", sv: "Listpris" }, formatSEK(price), false], + [{ en: "Deposit 50% — starts production", sv: "Förskott 50 % — startar produktion" }, formatSEK(deposit), true], + [{ en: "On approval — released from escrow",sv: "Vid godkänt — släpps ur escrow" }, formatSEK(remainder), false], ]; return (
- +

- Halva nu, halva vid godkänt. Resterande hålls i escrow tills du validerat resultatet. + {t({ + en: "Half now, half on approval. The remaining half is held in escrow until you have validated the result.", + sv: "Halva nu, halva vid godkänt. Resterande hålls i escrow tills du validerat resultatet.", + })}

{lines.map(([label, val, hot]) => ( -
- {label} +
+ {t(label)} - Komponera & beställ - - → - + {t({ en: "Compose & order", sv: "Komponera & beställ" })} + →
diff --git a/app/katalog/page.tsx b/app/katalog/page.tsx index 6d265f2..c3140e6 100644 --- a/app/katalog/page.tsx +++ b/app/katalog/page.tsx @@ -22,12 +22,12 @@ interface CatalogItem { confidence: number; } -const FILTERS: { key: string; label: string }[] = [ - { key: "all", label: "Alla" }, - { key: "company", label: "Bolag" }, - { key: "contact", label: "Beslutsfattare" }, - { key: "vehicle", label: "Fordon" }, - { key: "financials", label: "Finansiellt" }, +const FILTERS: { key: string; label: { en: string; sv: string } }[] = [ + { key: "all", label: { en: "All", sv: "Alla" } }, + { key: "company", label: { en: "Companies", sv: "Bolag" } }, + { key: "contact", label: { en: "Decision-makers", sv: "Beslutsfattare" } }, + { key: "vehicle", label: { en: "Vehicles", sv: "Fordon" } }, + { key: "financials", label: { en: "Financials", sv: "Finansiellt" } }, ]; function level(c: number): ProvLevel { @@ -97,7 +97,7 @@ export default function CatalogPage() { color: filter === f.key ? "var(--background)" : "var(--foreground)", }} > - {f.label} + {t(f.label)} ))}
diff --git a/app/portal/subscriptions/page.tsx b/app/portal/subscriptions/page.tsx index 33cc356..5cbda72 100644 --- a/app/portal/subscriptions/page.tsx +++ b/app/portal/subscriptions/page.tsx @@ -14,6 +14,7 @@ interface Sub { } export default function SubscriptionsPage() { + const t = useT(); const [subs, setSubs] = useState(null); const [error, setError] = useState(false); const [orderId, setOrderId] = useState(""); @@ -37,28 +38,53 @@ export default function SubscriptionsPage() { } return ( - +
-

Gör om en order till ett flöde

+

+ {t({ en: "Convert an order into a flow", sv: "Gör om en order till ett flöde" })} +

- setOrderId(e.target.value)} placeholder="order_id" className="border border-border bg-input px-3 py-2 font-mono text-xs outline-none focus:border-foreground" /> - setOrderId(e.target.value)} + placeholder="order_id" + className="border border-border bg-input px-3 py-2 font-mono text-xs outline-none focus:border-foreground" + /> + -
- {error ? : !subs ? : subs.length === 0 ? ( - + {error ? ( + + ) : !subs ? ( + + ) : subs.length === 0 ? ( + ) : (
- OrderKadensStatusWebhook + {t({ en: "Order", sv: "Order" })} + {t({ en: "Cadence", sv: "Kadens" })} + {t({ en: "Status", sv: "Status" })} + {t({ en: "Webhook", sv: "Webhook" })}
{subs.map((s) => (