@col/format
Versie: 1.0.0 Toegevoegd: 2026-08-06
Herkomst
Vier apps schreven elk hun eigen Nederlandse formattering. growerportal2/src/lib/format.ts
is de meest complete: formatCurrency, formatCurrencyDetailed, formatNumber, formatPrice,
formatDate, formatTime, formatFileSize — 283 aanroepen over 37 bestanden. floriday api/src/features/supply-search/format.ts is kleiner maar het meest doordacht: gepind op
nl-NL en UTC, met een bewust behandeld negatief getal en een bewust behandelde onherkende
valutacode. supplier-onboarding-vercel/src/lib/i18n.ts formatteert datum/tijd per taal via
een Language → locale-mapping (nl, en, es, it). voorraadbeheer heeft geen gedeeld
bestand: formatCurrency- en toLocaleDateString-aanroepen staan letterlijk gekopieerd op
zes of meer pagina's, met een eigen localeMap per aanroepplek.
Dit item combineert de vier: de functieset van growerportal2, de edge-case-discipline van floriday, en de eis van supplier-onboarding/voorraadbeheer dat de locale een parameter is, geen aanname.
Waarvoor
Vijf pure functies voor het formatteren van een datum, tijd, getal, bedrag of bestandsgrootte
naar een leesbare string — geen state, geen React, geen Intl-kennis nodig bij de aanroeper.
Pak dit voor elke plek waar een Date, number of bytes-aantal aan een gebruiker getoond
wordt. Gebruik het niet voor parsen (invoervelden, formuliervalidatie) — dit item gaat één
kant op, waarde naar weergavestring.
Wat je app moet leveren
Niets — geen props, geen backend-contract. Vijf functies, direct importeren en aanroepen:
import { formatDate, formatTime, formatNumber, formatCurrency, formatFileSize } from '@/lib/format'
formatDate(new Date()) // "06-08-2026"
formatDate(request.createdAt, { locale: 'en-GB' }) // "06/08/2026"
formatDate(auctionDate, { timeZone: 'UTC' }) // pin op UTC voor een tijdzone-loze datum
formatTime(new Date()) // "14:05"
formatNumber(1234567.891) // "1.234.567,891"
formatNumber(count, { maximumFractionDigits: 0 }) // "1.235"
formatCurrency(1234.5) // "€ 1.234,50" (NBSP na €)
formatCurrency(total, { decimals: false }) // "€ 1.235"
formatCurrency(stukprijs, { decimals: 3 }) // "€ 1,500" — exact aantal decimalen
formatCurrency(amount, { locale: 'en-US', currency: 'USD' })
formatFileSize(file.size) // "2.4 MB"
function formatDate(value: Date | string, options?: Intl.DateTimeFormatOptions & { locale?: string }): string
function formatTime(value: Date | string, options?: Intl.DateTimeFormatOptions & { locale?: string }): string
function formatNumber(value: number, options?: Intl.NumberFormatOptions & { locale?: string }): string
function formatCurrency(value: number, options?: { locale?: string; currency?: string; decimals?: boolean | number }): string
function formatFileSize(bytes: number): string
| Functie | Standaard | Opmerking |
|---|---|---|
formatDate |
nl-NL, dd-mm-jjjj, geen tijdzone |
options (Intl.DateTimeFormatOptions) overschrijft per veld; timeZone gaat ongewijzigd door |
formatTime |
nl-NL, uu:mm, geen tijdzone |
zelfde tijdzone-gedrag als formatDate |
formatNumber |
nl-NL, geen eigen fractie-standaard |
options gaat ongewijzigd naar Intl.NumberFormat |
formatCurrency |
nl-NL, EUR, 2 decimalen |
decimals is boolean | number: false → 0 decimalen, true/weglaten → 2, een getal (bv. 3) zet het exacte aantal; onherkende currency valt terug op "<bedrag> <code>" in plaats van te gooien |
formatFileSize |
— | binair (1024), niet decimaal (1000); geen locale-parameter, dit zijn geen Intl-eenheden |
formatDate/formatTime accepteren zowel een Date als een ISO-string, zodat een waarde
rechtstreeks uit een API-response (JSON kent geen Date-type) zonder tussenstap geformatteerd
kan worden.
Bestanden
| Bestand | Landt in | Soort |
|---|---|---|
format.ts |
lib/format.ts |
beheerd |
Geen registryDependencies, geen cssVars. Eén bestand, geen dependencies.
Gebruikt door
| App | Sinds versie | Opmerkingen |
|---|---|---|
| — | Nog niet vanuit dit item geïnstalleerd. |
growerportal2 (283 aanroepen, 37 bestanden), floriday-middleware, supplier-onboarding-vercel
en voorraadbeheer hebben elk hun eigen versie — dat is precies waar dit item uit samengesteld is
(zie Herkomst). Wie een migratie oppakt: doe dat als eigen taak, niet als bijvangst (zie
docs/specs/2026-08-06-v10-basisitems.md, Risico's) — growerportal2 alleen al is een grote
zoek-en-vervang. Voeg hier een regel toe zodra een app is overgezet.
Bewuste keuzes
- Locale als parameter op elke functie, geen module-brede standaard. growerportal2 en
floriday zijn allebei Nederlandstalige apps en pinnen
nl-NLhardcoded; supplier-onboarding- vercel heeft vier talen (NL/EN/ES/IT) envoorraadbeheerheeft een eigenlocaleMapper taal. Een module-brede standaard zou de twee laatste apps dwingen om na elke--overwriteeen eigen wrapper eromheen te blijven onderhouden.nl-NLblijft wel de default-waarde — drie van de vier bronapps zijn Nederlandstalig — maar altijd overschrijfbaar per aanroep. - Geen standaard tijdzone.
floriday-middlewarepintformatDatebewust op UTC, voor determinisme in een feature die met tijdzone-loze synchronisatiedata werkt (auctionDateis een database-datum zonder tijdzone). De andere drie apps volgen de tijdzone van de browser of het Node-proces. UTC als standaard zetten zou die drie apps tijden een uur naast de werkelijkheid laten tonen zonder dat iemand het merkt — een stille regressie die pas opvalt bij een klacht over een verkeerde tijd. In plaats daarvan gaatoptions.timeZoneongewijzigd door naarIntl.DateTimeFormat/Intl.NumberFormat(impliciet via de spread), zodat floriday zelf{ timeZone: 'UTC' }meegeeft en de rest niets hoeft te doen. Getest intests/format.test.ts: één test bewijst dat een explicietetimeZonehet resultaat verandert (Tokio vs. UTC voor hetzelfde tijdstip), een tweede zetprocess.env.TZop een tijdzone ver van UTC (Pacific/Kiritimati, UTC+14) en bewijst datformatDatezóndertimeZone-optie exact hetzelfde teruggeeft als een rechtstreekseIntl-aanroep zonder tijdzone — een regressie naar een hardcoded UTC-standaard zou die test laten falen. formatFileSizeis binair (1024), niet decimaal (1000). growerportal2's origineel gebruikt al 1024 met de labels "B"/"KB"/"MB"/"GB" — dezelfde conventie als een Windows- of macOS-bestandenoverzicht, ook al is dat strikt genomen niet de SI-betekenis van "kilo". Dat is bewust aangehouden: een bestandsgrootte in deze apps komt altijd van een OS- of browser-bestandskiezer, en die tonen zelf ook 1024-gebaseerde groottes. Andere labels gebruiken (KiB/MiB, de correcte binaire notatie) zouden een leesbaarheidswinst voor niemand opleveren en een breuk zijn met wat gebruikers al gewend zijn.formatCurrency'sdecimalsisboolean | number, geen vrijminimumFractionDigits. growerportal2 heeft drie aparte functies voor bedragen:formatCurrency(0 decimalen, 18 aanroepen),formatCurrencyDetailed(2 decimalen, 56 aanroepen — de meerderheid) enformatPrice(3 decimalen, voor een stuksprijs, 18 aanroepen).true/weglaten enfalsedekken de eerste twee —true(2 decimalen) is de standaard, omdat die in de brondata het vaakst voorkomt en overeenkomt met hoevoorraadbeheeren floriday hunIntl-aanroep ongewijzigd laten (Intl's eigen standaard voor EUR is 2 decimalen). Voor de derde —formatPrice's 3 decimalen — accepteertdecimalsook een getal, dat rechtstreeks alsminimumFractionDigitsenmaximumFractionDigitsdient. Het alternatief (formatNumbermet een handmatig voorgeplakt valutateken) zou precies weggooien waarformatCurrencyvoor bestaat: de automatische symboolplaatsing en de locale-correcte spatiëring (de NBSP bijnl-NL), en de overdraagbaarheid naar een andere valuta of locale zonder de aanroeper daarover te laten nadenken.- Onherkende valutacode gooit geen fout.
Intl.NumberFormatgooit eenRangeErrorop een ongeldige ISO-4217-code. floriday'sformatPriceving dat al af (de valutacode in hun feed komt soms rechtstreeks uit externe data). Dat gedrag is overgenomen:formatCurrencyvalt terug op"<bedrag> <code>"mettoFixed, in plaats van de rij (of pagina) die deze functie aanroept mee te trekken in een crash. - Negatieve getallen worden niet gedressed als positief. floriday's
formatInteger- documentatie noemt expliciet dat de brondata (numberOfPieces) legitiem negatief kan zijn — 349 rijen, tot -98.200, vermoedelijk correcties in de Floriday-feed.formatNumberenformatCurrencydoen niets bijzonders met een negatief getal;Intltoont het teken gewoon. EenMath.abszou dat stilzwijgend verkeerd voorstellen. formatDate/formatTimeaccepterenDate | string, niet alleenDate. growerportal2 en supplier-onboarding-vercel doen dat al (new Date(date)binnen de functie); dat scheelt de aanroeper een handmatige conversie bij elke API-response, waar een datum als ISO-string uit JSON komt.
Bekende beperkingen
- De standaard datumvorm wijkt af van wat sommige apps nu tonen.
formatDate's standaard isdd-mm-jjjjmet voorloopnullen (06-08-2026) — growerportal2's vorm.supplier-onboarding-vercelroept nutoLocaleDateString(locale)aan zonder opties, wat innl-NLgeen voorloopnullen geeft (5-1-2026); floriday gebruikt{ month: 'short' }(6 aug 2026). Beide zijn bij migratie bereikbaar viaoptions, maar wieformatDatezonder opties aanroept in een van die twee apps ziet zijn datums stilzwijgend van vorm veranderen ten opzichte van wat er nu staat. - Geen valuta-symbool-positie-controle.
Intlbepaalt zelf of het symbool voor of na het bedrag komt, en met welk scheidingsteken (inclusief de NBSP dienl-NLgebruikt tussen€en het bedrag). Dat is bewustIntl's beslissing, niet dit item — een test die een string vergelijkt met een handmatig getypte spatie in plaats van een NBSP faalt op een manier die in de diff niet van een gewone spatie te onderscheiden is.tests/format.test.tsgebruikt daarom eenNBSP-constante in plaats van een letterlijke spatie in de verwachte string. Intl-uitvoer is niet gegarandeerd stabiel tussen Node-versies. De exacte tekens (spatie- variant, wel of geen punt achter een verkorte maandnaam) komen uit de ICU-data die met de gebruikte Node-versie is meegeleverd. Dit item is getest tegen de ICU-data van de Node-versie in deze repo (node -vbij het schrijven: v24) — een consumerende app op een andere Node-versie kan een net iets andere string krijgen voor dezelfde aanroep. Voor de vijf functies hier (datum/tijd/getal/bedrag/bestandsgrootte) is dat verschil altijd cosmetisch (spatie-teken, wel/geen punt), nooit functioneel.- Geen relatieve tijd ("3 uur geleden", "gisteren"). Geen van de vier bronapps had dat nodig;
Intl.RelativeTimeFormatis een aparte API met een eigen vormvrijheid en hoort, als de behoefte ontstaat, in een eigen functie of eigen item.