Shared
The value objects and helpers exposed by the package root besides errors and the service context: localized fields, supported languages, calendar days, UTC month windows, label sorting and search normalisation.
Import from @workspace/training. These are the cross-cutting pieces of domain/shared that every module and every app reuses. The errors are on Errors and ServiceContext on Service Context.
Localized fields
A localized field is a record from a language code to a value: { fr: "Enchères", en: "Bidding" }. Board reviews, auction and play comments and theme labels are stored this way.
| Export | Definition |
|---|---|
LocalizedField<T> | Record<string, T> |
LocalizedLabels | LocalizedField<string>, the plain-string specialisation used for theme labels |
LocalizedFieldEntry<T> | { lang: string; value: T } |
Reading
| Function | Signature | Behaviour |
|---|---|---|
getLocalizedField | <T>(field: LocalizedField<T> | undefined | null, language: string, defaultValue: T, fallbackLanguage = "en") => T | Exact language, then fallbackLanguage, then the first value in insertion order, then defaultValue |
getLocalizedFieldWithLang | same parameters => { value: T; resolvedLang: string | null } | Same chain and also tells which language answered; resolvedLang is null when the default was used |
getLocalizedFieldEntries | <T>(field: LocalizedField<T> | undefined | null) => LocalizedFieldEntry<T>[] | The entries as { lang, value }, [] for a nullish field |
The fallback chain uses ?? on the exact and fallback lookups, so an entry explicitly set to undefined is skipped like a missing one.
const labels: LocalizedLabels = { fr: "Enchères", en: "Bidding" };
getLocalizedField(labels, "fr", "?"); // => "Enchères"
getLocalizedField(labels, "de", "?"); // => "Bidding" (fallback language "en")
getLocalizedField({ fr: "Enchères" }, "de", "?"); // => "Enchères" (first available)
getLocalizedField({}, "fr", "?"); // => "?"
getLocalizedField(null, "fr", "?"); // => "?"
getLocalizedField(labels, "de", "?", "fr"); // => "Enchères" (custom fallback)
getLocalizedFieldWithLang(labels, "de", "?");
// => { value: "Bidding", resolvedLang: "en" }
getLocalizedFieldWithLang(null, "fr", "?");
// => { value: "?", resolvedLang: null }Building and checking
| Function | Signature | Behaviour |
|---|---|---|
createLocalizedField | <T>(language: string, value: T) => LocalizedField<T> | A one-entry field |
mergeLocalizedFields | <T>(base: LocalizedField<T>, patch: LocalizedField<T>) => LocalizedField<T> | Shallow spread; patch values override base values |
isLocalizedField | <T>(value: unknown, valuePredicate?: (v: unknown) => v is T) => value is LocalizedField<T> | A non-null, non-array object whose every value satisfies the predicate. The default predicate checks for strings, so the bare call recognises LocalizedLabels |
isLocalizedField({}) is true: an empty record has no value that fails the predicate.
createLocalizedField("fr", "Enchères"); // => { fr: "Enchères" }
mergeLocalizedFields(labels, { en: "Auction", ca: "Subhasta" });
// => { fr: "Enchères", en: "Auction", ca: "Subhasta" } (patch wins)
getLocalizedFieldEntries(labels);
// => [{ lang: "fr", value: "Enchères" }, { lang: "en", value: "Bidding" }]
getLocalizedFieldEntries(undefined); // => []
isLocalizedField(labels); // => true (every value is a string)
isLocalizedField({ fr: 1 }); // => false
isLocalizedField([]); // => false
isLocalizedField({}); // => true (vacuously)
isLocalizedField({ fr: 1 }, (v): v is number => typeof v === "number"); // => trueSupported languages
The languages supported for board content localisation. The i18n locale list of the apps is a different, wider concept.
| Export | Definition |
|---|---|
SUPPORTED_LANGUAGE | { FR: "fr", EN: "en" } as const |
SUPPORTED_LANGUAGE_VALUES | Object.values(SUPPORTED_LANGUAGE), that is ["fr", "en"] |
SupportedLanguage | "fr" | "en" |
LanguageFilter | SupportedLanguage | (string & {}): autocompletes the known languages while accepting any string, for filters |
UNDETERMINED_LOCALE | "und", the BCP-47 sentinel for content with no recorded language |
UNDETERMINED_LOCALE keys the rich text parsed from a legacy editor-board review (a single HTML string without locale metadata) until the clone-by-code flow asks the user for a real language. The PBN import uses the same sentinel, and apps/admin passes it as the theme locale of its bulk claim service.
SUPPORTED_LANGUAGE.FR; // => "fr"
SUPPORTED_LANGUAGE_VALUES; // => ["fr", "en"]
UNDETERMINED_LOCALE; // => "und"
const known: SupportedLanguage = "en";
const filter: LanguageFilter = "ca"; // any string accepted, known ones autocompleteCalendar days
| Export | Definition |
|---|---|
CalendarDay | string, a day as YYYY-MM-DD in UTC. Stable key for occurrence grouping, calendar rendering and date-only inputs |
toCalendarDay(value: string): CalendarDay | The UTC day of the instant an ISO timestamp or a date-only string denotes. The string is parsed, so an offset timestamp lands on the day of the instant, not the day written; a string that does not parse throws ValidationError |
startOfNextCalendarDayIso(day: CalendarDay): string | The ISO timestamp of midnight UTC the day after day. Use it as the exclusive upper bound of a half-open range on a timestamptz column, so every instant of day is included whatever the sub-second precision |
const day: CalendarDay = toCalendarDay("2026-03-01T14:30:00.000Z"); // => "2026-03-01"
toCalendarDay("2026-03-01"); // => "2026-03-01"
toCalendarDay("2026-03-01T22:30:00-05:00"); // => "2026-03-02", the UTC day of the instant
startOfNextCalendarDayIso(day); // => "2026-03-02T00:00:00.000Z"
startOfNextCalendarDayIso("2026-12-31"); // => "2027-01-01T00:00:00.000Z"
// => { start: 2026-10-01T00:00:00.000Z, end: 2026-11-01T00:00:00.000Z }Generic helpers live in shared
Helpers with no product meaning are imported from @workspace/shared, not from this package: locale-aware sorting and accent-insensitive search (sortByLabel, normalizeForSearch in @workspace/shared/sort-by-label) and the UTC month window (utcMonthWindow in @workspace/shared/date).
Not exported by the root
domain/shared/rotater.ts declares domainRotater, a createRotater instance from @workspace/bridge/rotation built over every rotation entry of @workspace/i18n-messages, with no default language because stored content is multilingual. It is internal: the editor-boards rotation projection and the boards write gate use it, and no public barrel exports it.