Every product has dates and money, and every SSR app has had the bug where the server rendered "21/09/2026" and the browser rendered "9/21/2026". This page is the practical knowledge interviewers check with one small task: "format this timestamp for the user", or "why is 0.1 + 0.2 not 0.3 and what do you do about prices".
Date is a millisecond timestamp with a local-time API. Months are zero-based, it is mutable, Date.parse of non-ISO strings is implementation-defined, and every getHours/toLocaleString call depends on the runtime's time zone and locale, which differ between your server (usually UTC, en-US) and the user's browser. That difference is the classic hydration mismatch.2026-09-21T08:00:00.000Z) or epoch milliseconds; keep the user's IANA time zone (Intl.DateTimeFormat().resolvedOptions().timeZone) and locale as explicit inputs; format at the edge with Intl, passing locale and timeZone explicitly so server and client agree; never do calendar arithmetic with setDate across DST boundaries by hand.PlainDate, PlainTime, PlainDateTime, ZonedDateTime, Instant, Duration, calendar-aware arithmetic, explicit time zones, no implicit local time. The libraries in between: date-fns (functions over Date, tree-shakable), Day.js (small, plugin-based), Luxon (Intl-based); Moment is legacy.Intl.DateTimeFormat: dateStyle/timeStyle presets or component options, timeZone, hour12, formatRange, formatToParts for custom layouts. Intl.RelativeTimeFormat ("3 days ago", numeric: 'auto' for "yesterday"). Intl.DurationFormat (Baseline 2025) for "2 h 30 min".Intl.NumberFormat: style: 'currency' with currency, currencyDisplay, notation: 'compact' ("1.2M"), style: 'unit' (unit: 'kilometer-per-hour'), style: 'percent', signDisplay, minimumFractionDigits/maximumFractionDigits, roundingMode ('halfEven' for banker's rounding), formatRange, formatToParts. Intl.PluralRules for one/few/many/other categories (Czech has few and many), Intl.ListFormat ("a, b and c"), Intl.DisplayNames (language and region names), Intl.Collator for locale-aware sorting (numeric: true for "file2" before "file10"), Intl.Segmenter for grapheme and word boundaries (emoji length, truncation).0.1 + 0.2 === 0.30000000000000004, integers are exact only up to Number.MAX_SAFE_INTEGER (2^53 − 1), toFixed rounds on the binary value ("1.005".toFixed(2) is "1.00"). Money is stored in minor units as integers (cents) or as decimal strings handled by a decimal library (big.js, decimal.js, dinero.js); BigInt for large integer ids (no mixing with Number, not JSON-serialisable without a reviver, see JSON.rawJSON). Math.sumPrecise (ES2026) sums doubles exactly.Number('12px') is NaN, parseFloat('12px') is 12, parseInt needs a radix, Number.isNaN versus the coercing global isNaN; Number.isInteger, Number.isSafeInteger. User input in locales with a decimal comma needs Intl-aware parsing or a library, because Number('1,5') is NaN.Intl formatter construction is expensive (locale data lookup); cache formatters per locale and options, and format in a computed. This app's formatDate helper and Nuxt's <NuxtTime> do exactly that.Accept-Language so both sides see the same values; <NuxtTime> renders consistent <time> elements; @nuxtjs/i18n exposes n() and d() over Intl with formats defined once.Formatters that agree between server and client, cached per locale:
const numberFormatters = new Map<string, Intl.NumberFormat>()
const dateFormatters = new Map<string, Intl.DateTimeFormat>()
export function formatMoney(minorUnits: number, currency: string, locale: string) {
const key = `${locale}:${currency}`
const fmt = numberFormatters.get(key) ?? numberFormatters.set(key, new Intl.NumberFormat(locale, {
style: 'currency',
currency,
roundingMode: 'halfEven',
})).get(key)!
return fmt.format(minorUnits / 100) // 12345 -> "123,45 €" in cs-CZ, "€123.45" in en-IE
}
export function formatDate(iso: string, locale: string, timeZone: string, style: Intl.DateTimeFormatOptions['dateStyle'] = 'medium') {
const key = `${locale}:${timeZone}:${style}`
const fmt = dateFormatters.get(key) ?? dateFormatters.set(key, new Intl.DateTimeFormat(locale, {
dateStyle: style,
timeStyle: 'short',
timeZone,
})).get(key)!
return fmt.format(new Date(iso))
}
export function formatRelative(iso: string, locale: string, now = Date.now()) {
const diffMs = new Date(iso).getTime() - now
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
const minutes = Math.round(diffMs / 60_000)
if (Math.abs(minutes) < 60) return rtf.format(minutes, 'minute') // "in 5 minutes", "yesterday" with numeric auto for days
const hours = Math.round(minutes / 60)
if (Math.abs(hours) < 24) return rtf.format(hours, 'hour')
return rtf.format(Math.round(hours / 24), 'day')
}
Using them in a component with locale and time zone from the request rather than the runtime:
<script setup lang="ts">
const props = defineProps<{ order: { totalMinor: number, currency: string, placedAt: string } }>()
// Both values are known on the server and the client: a cookie for the zone, the i18n locale for the language.
const { locale } = useI18n()
const timeZone = useCookie('tz', { default: () => 'Europe/Prague' })
const total = computed(() => formatMoney(props.order.totalMinor, props.order.currency, locale.value))
const placed = computed(() => formatDate(props.order.placedAt, locale.value, timeZone.value))
</script>
<template>
<tr>
<td><NuxtTime :datetime="order.placedAt" :locale="locale" :time-zone="timeZone" date-style="medium" time-style="short" /></td>
<td>{{ total }}</td>
<td class="text-muted">{{ placed }}</td>
</tr>
</template>
Plurals, lists and sorting the way native speakers expect:
const plural = new Intl.PluralRules('cs')
const forms = { one: 'položka', few: 'položky', many: 'položky', other: 'položek' } as const
const label = (n: number) => `${n} ${forms[plural.select(n) as keyof typeof forms]}` // 1 položka, 3 položky, 5 položek, 2.5 položky
new Intl.ListFormat('en', { type: 'conjunction' }).format(['Vue', 'Nuxt', 'Nitro']) // "Vue, Nuxt, and Nitro"
const collator = new Intl.Collator('cs', { numeric: true, sensitivity: 'base' })
['soubor10', 'soubor2', 'Čáp', 'cíl'].sort(collator.compare) // cíl, Čáp, soubor2, soubor10
[...new Intl.Segmenter('en', { granularity: 'grapheme' }).segment('👨👩👧👦')].length // 1, where '👨👩👧👦'.length is 11
{{ new Date(order.placedAt).toLocaleString() }} uses the server's locale and zone during SSR and the browser's during hydration: different text, a hydration mismatch, and wrong information for the user. Pass locale and timeZone explicitly from data both sides have, or use <NuxtTime>.
0.1 + 0.2 and 19.99 * 3 are not what a receipt says. Keep amounts as integer minor units (or decimal strings) through the API and the database, do arithmetic on integers, and let Intl.NumberFormat place the decimal separator and symbol at display time.
Temporal's browser support the week of the interview (which engines ship it unflagged) and whether Intl.DurationFormat is Widely available.
new Date().toLocaleString() in a Nuxt page, load it with the browser set to a different locale than the server, and read the hydration warning; fix it with explicit Intl options and again with <NuxtTime>.formatMoney and a test table for cs-CZ, de-DE, en-US, ja-JP with the same minor-unit amount.Intl.Collator; compare the orders."I keep dates as ISO 8601 UTC strings or timestamps in data and in the API, and I treat the user's locale and time zone as explicit inputs that both the server and the client know, from the URL, a cookie or Accept-Language, rather than from the runtime. Formatting happens at the edge with Intl.DateTimeFormat, passing locale and timeZone explicitly and caching the formatter per combination because constructing one is expensive. In Nuxt, <NuxtTime> does this and produces a proper <time> element. Calendar arithmetic goes through Temporal with a polyfill, or date-fns, never through setDate across DST by hand."
"Numbers are IEEE 754 binary doubles, and 0.1 and 0.2 have no exact binary representation, so the sum carries a rounding error that shows up as 0.30000000000000004. For money I never store or compute in floats: amounts travel as integer minor units, cents, or as decimal strings handled by a decimal library, arithmetic happens on integers, and Intl.NumberFormat with the right currency and rounding mode formats the value for display. toFixed is not a fix, because it rounds the already-inexact binary value; Math.sumPrecise from ES2026 helps when I genuinely must sum doubles exactly."
Nothing yet. Notes filed here show up on this page and the section overview.
Modern JavaScript, ES2024 to ES2026
What each of the last three editions added, what is finished for ES2027, the stage 3 proposals worth knowing by name, and how Baseline, build targets and polyfills decide what you can ship unpolyfilled.
TypeScript in practice
How senior interviews test TypeScript — as a design tool rather than a syntax quiz — and how the pages on the type system, generics, Vue and Nuxt typing, configuration and TypeScript 7, runtime validation and typing smells fit together.