Today Canvas Kit
SDK packages

@todayai-labs/tck-host-utils

Pure utility functions for product Hosts and development tooling

@todayai-labs/tck-host-utils contains deterministic helpers that a product Host and TCK Preview can share without depending on the React Host runtime. It does not own browser lifecycle, timers, locale detection, or time-zone detection. Callers pass those inputs explicitly.

Card locale negotiation

import { resolveCardLocale } from '@todayai-labs/tck-host-utils'

const locale = resolveCardLocale({
  cardSupportedLocales: manifest.locale?.supported,
  userPreferredLocales: ['zh-TW', 'en-US'],
  cardDefaultLocale: manifest.locale?.default,
  hostFallbackLocale: 'en',
})

The return value is a canonical language tag. For a declared card, it always belongs to the canonicalized supported list. Preferences are tried from first to last using FormatJS best-fit matching. A preference with an explicit script, or a script inferred from its region, excludes candidates with an incompatible specified script. Explicit script wins over region inference: zh-Hans-TW can match zh-Hans. Language-only candidates such as zh remain eligible without asserting a script. Matching does not rewrite the card's declaration.

  • ['en-GB', 'fr'] against ['en-US', 'fr'] selects en-US: preference order wins over a later exact match.
  • ['zh-TW', 'en-US'] against ['zh-Hans', 'en'] selects en: incompatible scripts are not treated as interchangeable.
  • No match, or an empty preference list, returns cardDefaultLocale.
  • Only a card with both card fields omitted uses hostFallbackLocale. This is Host policy for unknown content, not evidence that the card actually supports that language.

Provide both card fields together. Supported languages must be non-empty and unique after canonicalization, and the card default must be a canonical member. Invalid card declarations throw RangeError instead of falling through to the Host fallback. Malformed tags in the active branch also throw; the unused Host fallback is not consulted for declared cards, and user preferences are not consulted for undeclared cards. No browser language or process default is read. The runtime requires Intl.getCanonicalLocales and Intl.Locale with maximize() support.

This is a pure selection helper; consumers decide how to apply its result. It does not translate card content or provide a runtime useLocale hook.

Updated-at slot formatting

import { formatUpdatedAtSlotValue, nextUpdatedAtRefreshAt } from '@todayai-labs/tck-host-utils'

const timestamp = '2026-09-03T14:14:30Z'
const now = Date.now()
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone

const updatedAt = formatUpdatedAtSlotValue(timestamp, {
  now,
  locale: navigator.languages,
  timeZone,
})

const nextRefreshAt = nextUpdatedAtRefreshAt(timestamp, { now, timeZone })

The formatter applies these priorities:

ConditionDefault English label
Less than one minuteUpdated now
Less than one hourUpdated 18 min ago
Same natural dayUpdated at 14:32
Previous natural dayUpdated yesterday at 23:10
EarlierUpdated Sep 1 at 09:05

Natural-day comparisons use the supplied IANA time zone, not an elapsed 24/48-hour test. Invalid timestamps return null. A product Host should keep one shared clock, recompute at nextRefreshAt, and also recompute after foreground/focus, locale changes, or time-zone changes. The Preview package contains its own shared-clock adapter; the utility package deliberately does not.

On this page