Utilities
Pure helpers grouped by topic. Every export is a free function or constant — no classes, no module-scoped state — so each one is trivially tree-shakeable. Import what you need from the package root:
import { formatPrice, isContact, deriveUserMode } from 'propeller-v2-core-ui';
Formatting
formatPrice(amount: number, opts?: { currency?, locale?, symbol? }): string
formatDate(date: string | Date, opts?: { locale?, format? }): string
formatSurcharge(surcharge: number, opts?: { currency?, locale? }): string
calcDiscountPercent(original: number, current: number): number
formatPrice is locale-aware via Intl.NumberFormat; the symbol
override exists for shops that display a non-currency-code symbol (e.g.
'€' instead of 'EUR'). Throws on NaN/non-finite values — wrap in
the Result contract at the boundary if upstream data is untrusted.
Attribute extraction
attributeNameMatches(attr, name): boolean
getAttributeDisplayName(attr, language?): string
extractAttributeValues(product, attrName): string[]
collectAttributeValues(products, attrName): Set<string>
filterProductsBySelections(products, selections): Product[]
The Propeller GraphQL schema returns attributes as a flat array of
{ name, value, language? } records. These helpers handle the
denormalisation: case-insensitive name matching, multi-language fallback
via languageResolver, and selection-based
filtering for facet UIs.
Inventory
getStockStatus(quantity: number): { label: string, className: string }
Maps a stock quantity to a tri-state UI badge ("In stock" / "Low stock" /
"Out of stock"). The className is a stable token; the framework UI
packages map it to their own design-token palette.
Label fallback
getLabel(labels: Record<string, string> | undefined, key: string, fallback: string): string
Every component in the UI packages accepts an optional labels prop. This
helper is the canonical implementation — look up key, fall back to the
hardcoded English string. Used in ~50 components.
Language resolution
resolveLanguageEntry<T>(entries: LocalizedEntry<T>[], language: string): T | undefined
getLanguageString(entries, language, key): string | undefined
getLanguageUri(entries, language): string | undefined
Walks a LocalizedEntry[] array ({ language, value }) and resolves the
right value with a sane fallback chain: requested locale → en → first
entry. Used by attribute extraction, product helpers, and the CMS
renderer.
Product helpers
getProductImageUrl(product, opts?): string | null
getClusterImageUrl(cluster, opts?): string | null
getProductSku(product): string | null
getClusterSku(cluster): string | null
getLocalizedValue<T>(entries, language): T | undefined
Encapsulate the "default image / SKU" walks across the SDK's
Product / Cluster shape (which can have many images and multiple
SKUs depending on configuration).
Truncation
stripHtml(html: string): string
shouldTruncate(text: string, max: number): boolean
truncateAt(text: string, max: number, ellipsis?: string): string
stripHtml is dependency-free — it removes tags but does not sanitise.
Use it for length calculation and SEO-meta-tag derivation, not for
rendering untrusted HTML.
User identity
isContact(user: AnyUser): boolean // user is a B2B Contact
isCustomer(user: AnyUser): boolean // user is a B2C Customer
getUserId(user): string | null
getCompany(user): Company | null
getCompanyId(user): string | null
getAddresses(user): Address[]
getDefaultInvoiceAddress(user): Address | null
getDefaultDeliveryAddress(user): Address | null
The Propeller backend returns Contact or Customer as different SDK
classes; consumers want a flat predicate. isContact uses field presence
('contactId' in user) so it works on plain-object-serialised users too
(important for SSR hydration).
deriveUserMode
deriveUserMode(user: AnyUser | null, shopMode: ShopMode): UserMode
// ShopMode = 'b2b' | 'b2c' | 'hybrid'
// UserMode = 'anonymous' | 'b2b' | 'b2c'
Reduces (user, shopMode) to the single runtime mode every component
branches on. Anonymous users always resolve to 'anonymous' regardless
of shop mode; logged-in users resolve via isContact/isCustomer.
In a 'b2b' shop, a Customer who somehow logged in is still treated as
'b2b' — the route guard kicks them out before this is reached, but the
function is total either way.
const mode = deriveUserMode(currentUser, 'hybrid');
if (mode === 'b2b') showCompanySwitcher();
Visibility
isContentHidden(portalMode: PortalMode, user: AnyUser | null): boolean
Implements the portal-mode rules for when to hide entire route trees (prices, account, catalog) from anonymous visitors. Used by every catalog component and the page guards.
Video URL normalisation
isEmbeddable(url: string): boolean
normalizeVideoUrl(url: string): string | null
Accepts watch-style YouTube/Vimeo URLs and returns the embed equivalent.
Returns null for unrecognised hosts; callers should fall back to a
plain <a href>.
JSON-LD builders
buildProductJsonLd(product, ctx: JsonLdContext): object
buildClusterJsonLd(cluster, ctx: JsonLdContext): object
buildItemListJsonLd(items, ctx: JsonLdContext): object
safeJsonStringify(value): string
Generate schema.org JSON-LD for product, cluster, and item-list pages.
safeJsonStringify escapes </script> runs so the result is safe to
embed in an inline <script type="application/ld+json">.
Countries
COUNTRIES: Country[] // ISO 3166-1 alpha-2 + display name
COUNTRIES_MAP: Record<string, Country>
getCountryName(code: string, locale?: string): string | null
Used by address forms in the UI packages.