Code lists — shared, translatable vocabularies
Country, currency and unit of measure are code lists: shared, translatable, code-keyed vocabularies that many business objects reference.
They are deliberately not enum fields. An enum models one business object’s own
ad-hoc option set — an invoice’s payment status, a movement’s direction. A code list is
external vocabulary that outlives any single object and needs the same codes and the same
translations everywhere it appears.
Using one
Section titled “Using one”A code list is a Catalog, defined once in @onerp/catalogs, and reached through
the generic field builder:
import { countryCatalog } from "@onerp/catalogs";
country: field.codeList(countryCatalog, { label: "Country" }),The field stores the code in the column, validates it against the catalog, and renders
as a searchable dropdown showing the translated label. Its type is the catalog’s code
union — CountryCode | null above, and CountryCode only where the field says
required: true, exactly like every other kind.
Labels come from the catalog’s labels map, which is locale-complete: every code has a
label in every locale the catalog declares — TypeScript enforces it.
codeListLabel(catalog, code, locale) from @onerp/meta reads one and throws on a code
the catalog does not carry; useCodeListLabel(catalog, code) from @onerp/react/meta
reads it in the active locale. A label is never re-declared per field, so two business
objects referencing country cannot drift apart on what they call Belgium.
Adding a catalog
Section titled “Adding a catalog”@onerp/catalogs ships three: countryCatalog (ISO 3166-1 alpha-2, machine-generated —
regenerate it, never hand-edit it), currencyCatalog (ISO 4217), and unitCatalog
(UN/ECE Rec 20 — every unit a quantity can be denominated in). A fourth — incoterms, say —
is three moves in that package and no framework change, since field.codeList is generic
over any Catalog. The order is what makes the code union survive: a bare Catalog
infers string codes, and every field using it goes with them.
import type { Catalog } from "@onerp/meta";
const codes = ["EXW", "FCA", "DAP"] as const;
export type Incoterm = (typeof codes)[number];
export const incotermCatalog: Catalog<Incoterm> = { id: "incoterm", codes, labels: { en: { EXW: "Ex Works", FCA: "Free Carrier", DAP: "Delivered at Place" }, },};A catalog that carries data
Section titled “A catalog that carries data”unitCatalog is the one catalog whose codes mean something arithmetic, so
@onerp/catalogs publishes unitMeasures beside it: each code’s dimension and its
factor within that dimension, as strings. The catalog is the vocabulary, the record is
what a rule computes with, and the two extend together — Record<UnitCode, …> makes
TypeScript refuse a code added to one and not the other.
import { convertibleUnits, sharesDimension, unitMeasures } from "@onerp/catalogs";sharesDimension(a, b) is the whole conversion rule — quantities never cross a
dimension — and convertibleUnits(base) is the picker’s list. Both are pure and
browser-safe, which is why the unit picker in apps/web needs no round trip to ask
what a product’s line may be denominated in. The conversion itself lives with the
domain, in @onerp/master/unit.