Skip to content

Format source ​

@basmilius/react-ui/format writes every number, date, time and duration a person reads. It is the only place in the library that builds an Intl formatter, and your app can use it the same way.

ts
import { formatNumber, formatDayClock, setFormatSource } from '@basmilius/react-ui/format';
tsx
import { formatDayClock, formatLocale, formatNumber, formatPercent, useFormatLocale } from '@basmilius/react-ui/format';
import { PreferencesBar } from '../shared/preferences-bar.tsx';
import { Values } from '../shared/values.tsx';

const AT = new Date(2026, 8, 19, 8, 5);

export default function FormatSourceDemo() {
    // Draws again whenever the language or the region changes.
    useFormatLocale();

    return (
        <div className="flex w-full max-w-lg flex-col gap-4">
            <PreferencesBar />
            <Values
                rows={[
                    ['formatLocale()', formatLocale()],
                    ['formatNumber(1234567)', formatNumber(1234567)],
                    ['formatPercent(4.25)', formatPercent(4.25)],
                    ['formatDayClock(at)', formatDayClock(AT)]
                ]}
            />
        </div>
    );
}

Words and notation ​

Two settings decide what a value looks like, and they are separate on purpose. The language writes the words: the month, the weekday, "3 min ago". The region writes the notation: the order of day and month, the separators, and whether the clock counts to 12 or 24. An English interface on a Dutch computer is a real combination, and it reads 19 Sep, 08:05.

Change the language and the region above and watch both halves move independently.

The source ​

The formatters read both settings through a FormatSource your app hands over once, before the first render. UIProvider takes it as formatSource; without the provider, call setFormatSource yourself.

ts
interface FormatSource {
    language(): string;
    region(): string;
    systemLocale?(): string | undefined;
    subscribe(onChange: () => void): () => void;
}

language answers the language the interface is written in, such as en or nl. region answers a region tag such as nl-NL, or FORMAT_LANGUAGE to use the region the language comes with, or FORMAT_SYSTEM to follow the operating system; see Regions. systemLocale is for a desktop shell that can read the operating system's own region; a browser leaves it out. subscribe calls back whenever either setting may have changed.

Without a source the formatters write English in the region of English. setFormatSource answers the source it replaced, so a test can put that one back.

Drawing again on a change ​

A formatter is a plain function, so a component that calls one does not know the region changed. Call useFormatLocale() in it. It subscribes to the source and answers the current locale, which you may use or ignore. A component that calls it draws again when a person changes the language or the region.

tsx
function LastSaved({ at }: { at: number }) {
    useFormatLocale();
    return <span>{formatMoment(at)}</span>;
}

formatLocale() answers the locale the notation is written in, resolved from the source, such as en-US. It is a plain function over the source, so code outside React can read it too.

Sorting labels ​

labelCollator() answers an Intl.Collator for the language, which is how two labels a person reads are put in order. Labels are words, so the language sorts them. Dutch and English can put the same two words in a different order.

ts
names.sort(labelCollator().compare);

It compares without regard to case or accents and reads numbers inside a label as numbers, so Tab 2 comes before Tab 10.

Fallbacks ​

FALLBACK_LOCALE is en-US, the one locale to fall back on, so a format never depends on which machine ran a test. systemLocale() answers the operating system's locale: the shell's when it hands one over and Intl accepts it, then the browser's first language, then FALLBACK_LOCALE.

Cost ​

A formatter is built once per options object and locale, and kept. A list of a thousand rows builds nothing new. A region change throws the cached formatters away, since the locale is part of what was cached.

FormatSource is an exported type. Testing has a fake source for tests.