Bridge Training

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.

ExportDefinition
LocalizedField<T>Record<string, T>
LocalizedLabelsLocalizedField<string>, the plain-string specialisation used for theme labels
LocalizedFieldEntry<T>{ lang: string; value: T }

Reading

FunctionSignatureBehaviour
getLocalizedField<T>(field: LocalizedField<T> | undefined | null, language: string, defaultValue: T, fallbackLanguage = "en") => TExact language, then fallbackLanguage, then the first value in insertion order, then defaultValue
getLocalizedFieldWithLangsame 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.

shared.ts
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

FunctionSignatureBehaviour
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.

shared.ts
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"); // => true

Supported languages

The languages supported for board content localisation. The i18n locale list of the apps is a different, wider concept.

ExportDefinition
SUPPORTED_LANGUAGE{ FR: "fr", EN: "en" } as const
SUPPORTED_LANGUAGE_VALUESObject.values(SUPPORTED_LANGUAGE), that is ["fr", "en"]
SupportedLanguage"fr" | "en"
LanguageFilterSupportedLanguage | (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.

shared.ts
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 autocomplete

Calendar days

ExportDefinition
CalendarDaystring, a day as YYYY-MM-DD in UTC. Stable key for occurrence grouping, calendar rendering and date-only inputs
toCalendarDay(value: string): CalendarDayThe 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): stringThe 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
shared.ts
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.