Browser-Local Time
Render an absolute server instant with a useful fallback, then let the browser display it in the user's own timezone and language. No localization endpoint or extra request is involved.
The helper works on any FluentHtml element, so choose the tag that matches the meaning of your markup.
1. Fluent API
Pass the value to LocalizeTime. The value is the source of truth; existing inner text is not parsed as a date.
DateTimeOffset createdAt = order.CreatedAt;
return FluentHtml.Span(span => span
.Class("order-created")
.LocalizeTime(
createdAt,
"MMM d, yyyy 'at' h:mm tt"));2. Absolute Instants Only
Use DateTimeOffset, UTC DateTime, or local DateTime. DateTimeKind.Unspecified is rejected because it does not identify a real instant that can be converted safely.
// Preferred
span.LocalizeTime(DateTimeOffset.UtcNow, "G");
// Also valid
span.LocalizeTime(DateTime.UtcNow, "yyyy-MM-dd HH:mm zzz");
// Rejected: the timezone meaning is unknown
span.LocalizeTime(
DateTime.SpecifyKind(value, DateTimeKind.Unspecified),
"G");3. Formats
The runtime mirrors the useful C# DateTime.ToString shape with a documented subset. Standard formats are best for locale-aware UI; custom formats are best when the product needs a stable layout. As in C#, a one-character string is interpreted as a standard format: use % before a single custom token, such as %M.
Standard:
d short date
D long date
t short time
T medium time
g short date + short time
G short date + medium time (default)
Custom token shapes:
d / dd day number
ddd / dddd abbreviated / full weekday
M / MM month number
MMM / MMMM abbreviated / full month
y...yyyy year
h / hh 12-hour clock
H / HH 24-hour clock
m / mm minute
s / ss second
t / tt first letter / full day period
f / ff / fff fractional second through milliseconds
z / zz / zzz UTC offset
Literals:
'at' or "at" quoted text
\\y escaped character in a C# string
%M one custom token by itself
Examples:
"MMM d, yyyy 'at' h:mm tt"
"yyyy-MM-dd HH:mm:ss.fff zzz"4. What the Server Emits
The response contains an invariant ISO timestamp with an explicit offset, the requested format, and encoded server-rendered fallback text. If JavaScript is unavailable or localization fails, that fallback remains visible.
<span
heimdall-time="2026-08-26T18:30:05.123Z"
heimdall-time-format="MMM d, yyyy 'at' h:mm tt">
Aug 26, 2026 at 6:30 PM
</span>5. Language and Timezone
The timezone comes from the browser. Language resolves from the nearest lang boundary, then the document language, then the browser language. Use the native Lang helper to create an intentional language boundary.
FluentHtml.Div(panel => panel
.Lang("fr-FR")
.Span(time => time.LocalizeTime(createdAt, "MMM d HH:mm")));
// The same instant can appear as:
// en-US: Aug 26 14:30
// fr-FR: août 26 14:306. Every HTML Delivery Path
Localization runs on the initial page and on HTML introduced by content actions, out-of-band invocations, Bifrost SSE, and observed DOM changes. Action, invocation, and SSE fragments are localized before insertion to avoid a visible UTC-to-local flash.
initial response -> localize during runtime boot
action result -> localize fragment, then swap
OOB invocation -> localize fragment, then insert
Bifrost SSE -> localize fragment, then apply
dynamic DOM -> localize when observed7. Lifecycle Hooks
The before event is cancellable and mutable. Supply detail.text to take ownership of the final text, or prevent the default to keep the fallback. Output is always assigned with textContent, never interpreted as HTML.
document.addEventListener("heimdall:time-before", event => {
if (event.target.matches(".relative-time")) {
event.detail.text = formatRelative(event.detail.value);
}
});
document.addEventListener("heimdall:time-after", event => {
console.debug("Localized", event.detail.text);
});
document.addEventListener("heimdall:time-error", event => {
console.error(event.detail.error);
});8. Identity, Duplicates, and Reprocessing
A WeakMap tracks each DOM element by object identity. Two elements with the same instant are independent. A changed value, format, timezone, or language creates a new processing signature; unchanged elements are skipped.
Test the combinations that matter to your product:
- standard and custom formats
- quoted literals and explicit offsets
- daylight-saving transitions
- nearest lang boundaries
- initial, action, OOB, and SSE delivery
- malformed values and unsupported tokens9. When Not to Use It
LocalizeTime means browser-local time only. If the product asks for a specific named timezone, convert and format it on the server with TimeZoneInfo and render ordinary text.
var zoned = TimeZoneInfo.ConvertTime(instant, requestedZone);
span.Text(zoned.ToString(format, culture));