# Reference-Data Scannability + Parity — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make the three reference-data lists scannable (locale-aware sort + filter), show `external_uri` in read rows, add field-group count badges, and close the small parity/validation gaps — no layout/edit-modality change, no backend change. **Architecture:** A shared `lib/sort.ts` (memoized `Intl.Collator` comparators) + a tiny `ExternalUriLink` are consumed by the vocab/authorities/fields list components. Each list gets a client-side filter `useState` + ``; rows are filtered then sorted before render. **Tech Stack:** React 19 + TS + pnpm, react-i18next, Vitest + RTL + MSW. Test runner: `pnpm test` (single pass). **Conventions:** pnpm; **no `any`/`eslint-disable`/`@ts-ignore`**; no codename; en/sv parity (3 new keys); app source double-quote+semicolon; token classes only; **don't mutate query-cache arrays — sort a copy** (`[...list].sort(...)`). **Spec:** `docs/superpowers/specs/2026-06-08-refdata-scannability-design.md` **Key facts (from the code):** - `labelText(labels, lang)` in `lib/labels.ts`. `lang = i18n.language.startsWith("sv") ? "sv" : "en"`. - term/authority view = `{ id, labels, external_uri?: string|null }` (authority also `kind`); vocabulary = `{ id, key }`; field-def = `{ key, labels, data_type, group?, required, … }`. - `term-row.tsx`/`authority-row.tsx`: read mode `{labelText(...)}` + Edit + DeleteConfirmDialog; edit mode has the `external_uri` `` (no `type`/placeholder). - `vocabulary-list.tsx`: create form (has empty-guard) + list; rename `
` calls `renameVocabulary.mutate({ id, key: draftKey.trim() })` with NO empty-guard. - `authorities-page.tsx`: tabs + list + create form; create calls `create.mutate({ kind, external_uri: null, labels })` — hardcoded null, no URI field. - `field-list.tsx`: groups built into a `Map`; sorted with `t("fields.other")` last (no A–Z); group header `
{group}
`; rows show label+key+type+required. - `common` i18n: `{ yes, no, close, loading }`. `labels`: `{ label, externalUri, otherLanguages }`. --- # Task 1: Shared `sort.ts` + `ExternalUriLink` + i18n + unit test **Files:** `web/src/lib/sort.ts` (new), `web/src/lib/sort.test.ts` (new), `web/src/components/external-uri-link.tsx` (new), `web/src/i18n/en.json`, `web/src/i18n/sv.json`. - [ ] **Step 1: i18n (both locales, parity).** - `common`: add `"filter"` (en "Filter…" / sv "Filtrera…") and `"noMatches"` (en "No matches" / sv "Inga träffar"). - `labels`: add `"uriPlaceholder": "https://…"` (same string in both). - [ ] **Step 2: `web/src/lib/sort.ts`:** ```ts import type { components } from "../api/schema"; import { labelText } from "./labels"; type LabelView = components["schemas"]["LabelView"]; const collators = new Map(); function collatorFor(lang: string): Intl.Collator { let c = collators.get(lang); if (!c) { c = new Intl.Collator(lang, { sensitivity: "base", numeric: true }); collators.set(lang, c); } return c; } export function compareStrings(lang: string, a: string, b: string): number { return collatorFor(lang).compare(a, b); } export function byLabel(lang: string) { return (a: { labels: LabelView[] }, b: { labels: LabelView[] }) => compareStrings(lang, labelText(a.labels, lang), labelText(b.labels, lang)); } export function byKey(lang: string) { return (a: { key: string }, b: { key: string }) => compareStrings(lang, a.key, b.key); } ``` - [ ] **Step 3: `web/src/lib/sort.test.ts`** (write failing first, then it passes with Step 2): ```ts import { expect, test } from "vitest"; import { byKey, byLabel, compareStrings } from "./sort"; const L = (label: string) => ({ labels: [{ lang: "en", label }] }); test("byLabel sorts case-insensitively and locale-aware", () => { const sorted = [L("Iron"), L("bronze"), L("Amber")].sort(byLabel("en")).map((x) => x.labels[0].label); expect(sorted).toEqual(["Amber", "bronze", "Iron"]); }); test("byKey sorts keys with numeric awareness", () => { const sorted = [{ key: "item10" }, { key: "item2" }, { key: "item1" }].sort(byKey("en")).map((x) => x.key); expect(sorted).toEqual(["item1", "item2", "item10"]); }); test("compareStrings is case-insensitive", () => { expect(compareStrings("en", "bronze", "BRONZE")).toBe(0); }); ``` Run: `cd web && pnpm vitest run src/lib/sort.test.ts` → PASS (3 tests). - [ ] **Step 4: `web/src/components/external-uri-link.tsx`** (app-source style): ```tsx export function ExternalUriLink({ uri }: { uri: string }) { return ( {uri} ); } ``` - [ ] **Step 5: Verify (vitest ONCE):** `cd web && pnpm vitest run src/lib/sort.test.ts && pnpm typecheck && pnpm lint`. PASS, clean. - [ ] **Step 6: Commit** ```bash git add web/src/lib/sort.ts web/src/lib/sort.test.ts web/src/components/external-uri-link.tsx web/src/i18n/en.json web/src/i18n/sv.json git commit -m "feat(web): collator sort helpers + ExternalUriLink + filter/uri i18n (#50)" ``` --- # Task 2: Vocabularies — filter + sort + URI + rename guard **Files:** `web/src/vocab/vocabulary-list.tsx`, `web/src/vocab/vocabulary-terms.tsx`, `web/src/vocab/term-row.tsx`, `web/src/vocab/vocabularies.test.tsx`. - [ ] **Step 1: `vocabulary-list.tsx`** — add `i18n` to `useTranslation` (`const { t, i18n } = …`) and `const lang = i18n.language.startsWith("sv") ? "sv" : "en";`; import `byKey` from `../lib/sort` and `Input` is already imported. Add `const [filter, setFilter] = useState("");`. - Add a filter `` between the create `` and the list block: ```tsx
setFilter(e.target.value)} />
``` - In the loaded `