Waarom Datums en Getallen Lokalisatie Nodig Hebben
TABLE OF CONTENTS
Als je wereldwijd verzendt, kunnen dezelfde tekens verschillende dingen betekenen voor verschillende gebruikers: 12/01/2025, 1.234, 1,234, 12:00, 00:00. Die kleine verschillen zijn niet cosmetisch—ze beïnvloeden vertrouwen, begrip en zelfs wettelijke naleving. Deze gids legt uit waarom datums en getallen gelokaliseerd moeten worden, wat er daadwerkelijk verandert per locatie, en hoe je dit correct implementeert in je product.
Wat Verandert per Locatie
- Datums: volgorde (MDY vs DMY vs YMD), scheidingstekens, maand/dag namen; sommige markten gebruiken niet-Gregoriaanse kalenders.
- Tijden: 12-uurs vs 24-uurs klokken, AM/PM markeringen, tijdzones, zomertijdregels, weekdagstart (zondag vs maandag).
- Getallen: decimale scheidingstekens (
.vs,), groepeer stijlen (1,234vs1.234vs1 234), Indiase groepering (1,23,456), niet-afbreekbare spaties, min/plus stijlen (koppelteken vs echte min−). - Valuta: symbool vs ISO-code, symboolplaatsing (voorvoegsel/achtervoegsel), smalle vs brede spatiëring, decimale precisie (0, 2, of 3+), boekhoudkundige negatieven (bijv.
(1 234,56 €)). - Percentage en eenheden: spatiëring en tekenplaatsing (
50%vs50 %), lokale namen voor eenheden, metrisch vs imperiaal.
Zakelijke Impact
- Vertrouwen en conversie: Prijzen, kosten en datums die “verkeerd lijken” verminderen aankopen en verhogen verloop.
- Operationeel risico: Verkeerd gelezen datums kunnen boekingen of deadlines verschuiven; parseerfouten breken facturen, exporten en analyses.
- Naleving: Financiële documenten met verkeerde formaten kunnen facturering, belasting of rapportageregels schenden.
- Ondersteuningsbelasting: Gebruikers openen tickets om tijden, valuta’s en getalinvoer te verduidelijken die niet aan de verwachtingen voldoen.
Incident in de Praktijk
Een Europees reisboekingsplatform toonde vertrekdatums als 01/03/2024 zonder lokale context. Amerikaanse klanten interpreteerden dit als “3 januari”, terwijl Britse en Europese klanten het lazen als “1 maart”.
Impact:
- 12% van de internationale boekingen werden gemaakt voor de verkeerde data
- Klantenservice ontving meer dan 3.400 verduidelijkingsverzoeken in één week
- $2,3M aan terugbetalingen en herboekingskosten
- 8% van de getroffen klanten stapte over naar concurrenten
Root cause: Hard-coded DD/MM/YYYY formaat in e-mailbevestigingen terwijl de website MM/DD/YYYY gebruikte voor Amerikaanse gebruikers. De oplossing vereiste slechts 3 uur ingenieurstijd—de ambiguïteit kostte 800x meer dan preventie zou hebben gedaan.
Lesson: Datums zonder context zijn een tikkende tijdbom voor elk wereldwijd product.
Common Pitfalls
- Hard‑coded formats:
MM/DD/YYYYof1,234.56ingebakken in UI, e-mails, PDF’s of CSV-exporten. - Lokaal opgeslagen strings: Opslaan van “Dec 1, 2025” in plaats van ISO-tijd veroorzaakt parsefouten en tijdzoneverschuivingen.
- Naïeve parsing: Gebruik van server locale defaults; aannemen van scheidingstekens of 12/24-uurs klokken.
- String concatenation: Opbouwen van “amount + currency” zoals
"$" + 1234.56in plaats van locale-bewuste formattering. - Tijdzone ambiguïteit: Lokale tijden tonen zonder expliciete zones voor tijdgevoelige acties.
- Browser inconsistentie: Safari, Firefox en Chrome kunnen datums/nummers verschillend formatteren; test altijd op alle doelbrowsers.
- Server-side rendering gaps: Node.js zonder volledige ICU data (
node --with-intl=full-icu) produceert onvolledige of incorrecte formattering. - Performance traps: Nieuwe
Intl.*instanties creëren in loops of rendercycli in plaats van formatteren te cachen.
Best Practices
- Bewaar canonieke waarden: Datums/tijden als ISO‑8601 in UTC; geld als kleine eenheden (centen) of hoogprecisie decimalen.
- Render per gebruikerslocale: Gebruik locale-bewuste API’s (bijv.
Intl.DateTimeFormat,Intl.NumberFormat) voor weergave. - Valideer en parse per locale: Accepteer locale invoer waar gepast; toon placeholders/voorbeelden om gebruikers te begeleiden.
- Wees expliciet met tijd: Toon tijdzone-afkortingen of offsets; sta gebruikersselectie toe waar uitkomsten afhankelijk zijn van tijd.
- Valuta duidelijkheid: Geef de voorkeur aan ISO-codes waar er ambiguïteit bestaat; respecteer symboolplaatsing en spatiëring per locale.
- Centraliseer formattering: Eén hulplaag voor alle oppervlakken (UI, e-mails, PDF’s, exporten) om consistentie te waarborgen.
- Cache formatteren:
Intl.*constructor-aanroepen zijn duur; maak eenmaal en hergebruik instanties per locale.
Wanneer NIET te Lokaliseren
Niet alles moet gelokaliseerd worden. Hier zijn scenario’s waar consistente, machineleesbare formaten beter zijn:
- Machineleesbare exporten: CSV-bestanden die door datapijplijnen of analysetools worden gebruikt, moeten een vast formaat gebruiken (bijv. ISO-8601 datums, punt als decimaal scheidingsteken). Documenteer dit formaat duidelijk in exportheaders of README-bestanden.
- API-responses: REST/GraphQL API’s moeten datums als ISO-8601 strings en nummers in standaard JSON-formaat retourneren. Laat clients lokalisatie afhandelen op basis van hun gebruikersvoorkeuren.
- Interne logs en statistieken: Logs, monitoring dashboards en database queries profiteren van consistente formaten voor parsing, aggregatie en waarschuwing. Gebruik ISO-8601 en standaard numerieke formaten.
- Canonieke identificatoren: Transactie-ID’s, ordernummers of referenties die gebruikers mogelijk moeten communiceren over locales heen, moeten locale-specifieke formattering vermijden.
Vuistregel: Lokaliseer voor mensen die inhoud lezen; gebruik standaardformaten voor machines die data verwerken.
Implementatieplan
- Controleer oppervlakken: Inventariseer datums, tijden, nummers, valuta in UI, e-mails, PDF’s, CSV/Excel-exporten, analyses, logs.
- Definieer lokale instellingen: Lijst van ondersteunde lokale instellingen en standaard terugvalopties; specificeer 12/24-uursbeleid per lokale instelling.
- Bouw hulpprogramma’s: Omhul
Intl.*API’s (of een bibliotheek) met gedeelde helpers en tests; implementeer formatter caching. - Invoerstrategie: Werk formulieren bij om lokale formaten te accepteren en te valideren; sla canonieke waarden op naast ruwe invoer.
- Inhoudsregels: Documenteer stijl voor korte/lange datums, relatieve datums, valutaweergave en procentformattering.
- Uitrol: Converteer eerst de oppervlakken met het hoogste verkeer en het hoogste risico; verzend achter een feature-vlag indien nodig.
Teststrategie
Naast handmatige QA, implementeer geautomatiseerde tests om regressies te voorkomen:
- Snapshot tests: Bevries geformatteerde uitvoer voor elke ondersteunde lokale instelling. Wijzigingen activeren een beoordeling om ervoor te zorgen dat ze opzettelijk zijn.
- Round-trip tests: Verifieer dat
format → parse → formatstabiele resultaten oplevert. Vangt precisieverlies of dubbelzinnige formaten. - Randgevallen: Test negatieve nummers, nul, zeer grote nummers (miljarden/biljoenen), DST-overgangen, schrikkeljaren en grensdatums (1 jan, 31 dec).
- Cross-browser validatie: Geautomatiseerde tests op Chrome, Firefox, Safari en Edge om renderingsverschillen te detecteren.
- Locale fallback: Verifieer gracieuze degradatie wanneer niet-ondersteunde lokale instellingen worden opgevraagd.
- Prestatiebenchmarks: Meet formatter creatiekosten; zorg ervoor dat caching werkt in productie.
Prestatie Tips
Intl.* API’s zijn krachtig maar kunnen traag zijn als ze verkeerd worden gebruikt. Volg deze richtlijnen:
// ❌ SLECHT: Maakt bij elke oproep een nieuwe formatter aan
function formatPrice(amount, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
}
// ✅ GOED: Cache formatters per locale
const formatters = new Map();
function getFormatter(locale) {
if (!formatters.has(locale)) {
formatters.set(locale, new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}));
}
return formatters.get(locale);
}
function formatPrice(amount, locale) {
return getFormatter(locale).format(amount);
}
- Cache formatter instances:
Intl.*constructie is duur (~1–5ms); formatteren met een bestaande instantie is snel (~0.01ms). - Vermijd loops: Maak nooit formatters binnen
map(),forEach(), of render loops. - Lazy-load locale data: Voor web apps, overweeg code-splitting om alleen vereiste locales te laden, waardoor de bundelgrootte wordt verminderd.
- Server-side: Gebruik een enkele formatter per request/response cyclus; vermijd het maken per item in grote datasets.
QA Checklist
- Datums zijn ondubbelzinnig in
en-US,en-GB,de-DE,fr-FR,hi-IN,ja-JP,ar-EG. - Tijd gebruikt de juiste 12/24-uurs conventie; AM/PM markeringen verschijnen waar verwacht; tijdzones worden getoond voor tijdkritieke stromen.
- Nummers gebruiken de juiste decimale en groeperingsscheidingstekens; niet-breekbare spaties waar standaard (bijv.
fr-FR). - Valuta toont het juiste symbool/code, plaatsing en decimale precisie voor elke locale; negatieve waarden worden correct weergegeven.
- Invoer accepteert en parseert gebruikerslocale gegevens (of dwingt duidelijk een ondersteund formaat af); validatieberichten zijn locale-bewust.
- Exports (CSV/PDF) komen overeen met locale verwachtingen of documenteren duidelijk vaste formaten voor machinegebruik.
- Cross-browser: Datums, tijden en nummers worden consistent weergegeven op Chrome, Firefox, Safari en Edge.
- Performance: Geen formatter creatie in loops; gecachte instanties worden hergebruikt over renders.
Code Voorbeelden
JavaScript
// Datums
const date = new Date('2025-01-12T00:00:00Z');
console.log(new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeZone: 'UTC' }).format(date));
// → 1/12/25
```javascript
console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeZone: 'UTC' }).format(date));
// → 12/01/2025 (dag/maand/jaar)
console.log(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'medium', timeZone: 'UTC' }).format(date));
// → 2025/01/12
// Tijden met tijdzone weergave
const time = new Date('2025-01-12T18:05:00Z');
console.log(new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/New_York',
timeZoneName: 'short'
}).format(time));
// → 1/12/25, 1:05 PM EST
console.log(new Intl.DateTimeFormat('de-DE', {
timeStyle: 'short',
timeZone: 'Europe/Berlin',
hour12: false
}).format(time));
// → 19:05
// Nummers
const n = 1234.56;
console.log(new Intl.NumberFormat('en-US').format(n)); // → 1,234.56
console.log(new Intl.NumberFormat('de-DE').format(n)); // → 1.234,56
console.log(new Intl.NumberFormat('fr-FR').format(n)); // → 1 234,56 (NBSP als groep scheidingsteken)
// Indiase nummering
console.log(new Intl.NumberFormat('hi-IN').format(1234567.89)); // → 12,34,567.89
// Valuta
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n));
// → $1,234.56
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n));
// → 1.234,56 €
console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1234));
// → ¥1,234 (geen decimalen)
// Percentage en eenheden
console.log(new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 0 }).format(0.5));
// → 50%
console.log(new Intl.NumberFormat('fr-FR', { style: 'percent', maximumFractionDigits: 0 }).format(0.5));
// → 50 %
// Formatter caching voorbeeld
class LocaleFormatter {
constructor() {
this.cache = new Map();
}
getDateFormatter(locale, options) {
const key = `date:${locale}:${JSON.stringify(options)}`;
if (!this.cache.has(key)) {
this.cache.set(key, new Intl.DateTimeFormat(locale, options));
}
return this.cache.get(key);
}
}
formatDate(date, locale, options = { dateStyle: ‘medium’ }) { return this.getDateFormatter(locale, options).format(date); } }
const formatter = new LocaleFormatter(); console.log(formatter.formatDate(new Date(), ‘en-US’)); // → Jan 12, 2025 console.log(formatter.formatDate(new Date(), ‘fr-FR’)); // → 12 janv. 2025
### Python
```python
from babel.dates import format_date, format_time, format_datetime
from babel.numbers import format_number, format_currency, format_percent
from datetime import datetime
import pytz
# Datums
date = datetime(2025, 1, 12)
print(format_date(date, format='short', locale='en_US')) # → 1/12/25
print(format_date(date, format='short', locale='en_GB')) # → 12/01/2025
print(format_date(date, format='medium', locale='ja_JP')) # → 2025/01/12
# Tijden met tijdzone
tz_ny = pytz.timezone('America/New_York')
tz_berlin = pytz.timezone('Europe/Berlin')
time = datetime(2025, 1, 12, 18, 5, tzinfo=pytz.UTC)
print(format_datetime(time.astimezone(tz_ny), 'short', tzinfo=tz_ny, locale='en_US'))
# → 1/12/25, 1:05 PM
print(format_time(time.astimezone(tz_berlin), format='short', tzinfo=tz_berlin, locale='de_DE'))
# → 19:05
# Nummers
n = 1234.56
print(format_number(n, locale='en_US')) # → 1,234.56
print(format_number(n, locale='de_DE')) # → 1.234,56
print(format_number(n, locale='fr_FR')) # → 1 234,56
# Indiase nummering
print(format_number(1234567.89, locale='hi_IN')) # → 12,34,567.89
# Valuta
print(format_currency(n, 'USD', locale='en_US')) # → $1,234.56
print(format_currency(n, 'EUR', locale='de_DE')) # → 1.234,56 €
print(format_currency(1234, 'JPY', locale='ja_JP')) # → ¥1,234
# Percentage
print(format_percent(0.5, locale='en_US')) # → 50%
print(format_percent(0.5, locale='fr_FR')) # → 50 %
Java
import java.text.NumberFormat;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.Currency;
// Datums
ZonedDateTime date = ZonedDateTime.of(2025, 1, 12, 0, 0, 0, 0, ZoneId.of("UTC"));
DateTimeFormatter usFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.US);
System.out.println(usFormatter.format(date)); // → 1/12/25
DateTimeFormatter gbFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.UK);
System.out.println(gbFormatter.format(date)); // → 12/01/2025
DateTimeFormatter jpFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.JAPAN);
System.out.println(jpFormatter.format(date)); // → 2025/01/12
// Tijden
ZonedDateTime time = ZonedDateTime.of(2025, 1, 12, 18, 5, 0, 0, ZoneId.of("UTC"));
ZonedDateTime timeNY = time.withZoneSameInstant(ZoneId.of("America/New_York"));
DateTimeFormatter usTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.US);
System.out.println(usTimeFormatter.format(timeNY)); // → 1:05 PM
// Getallen
double n = 1234.56;
NumberFormat usFormat = NumberFormat.getInstance(Locale.US);
System.out.println(usFormat.format(n)); // → 1,234.56
NumberFormat deFormat = NumberFormat.getInstance(Locale.GERMANY);
System.out.println(deFormat.format(n)); // → 1.234,56
NumberFormat frFormat = NumberFormat.getInstance(Locale.FRANCE);
System.out.println(frFormat.format(n)); // → 1 234,56
// Valuta
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
usCurrency.setCurrency(Currency.getInstance("USD"));
System.out.println(usCurrency.format(n)); // → $1,234.56
NumberFormat deCurrency = NumberFormat.getCurrencyInstance(Locale.GERMANY);
deCurrency.setCurrency(Currency.getInstance("EUR"));
System.out.println(deCurrency.format(n)); // → 1.234,56 €
NumberFormat jpCurrency = NumberFormat.getCurrencyInstance(Locale.JAPAN);
jpCurrency.setCurrency(Currency.getInstance("JPY"));
System.out.println(jpCurrency.format(1234)); // → ¥1,234
Go
package main
import (
"fmt"
"time"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
)
func main() {
// Nummers
n := 1234.56
pUS := message.NewPrinter(language.AmericanEnglish)
fmt.Println(pUS.Sprintf("%.2f", n)) // → 1,234.56
pDE := message.NewPrinter(language.German)
fmt.Println(pDE.Sprintf("%.2f", n)) // → 1.234,56
pFR := message.NewPrinter(language.French)
fmt.Println(pFR.Sprintf("%.2f", n)) // → 1 234,56
// Valuta (met behulp van number package)
fmt.Println(pUS.Sprint(number.Decimal(n, number.Scale(2)))) // → 1,234.56
// Datums - Go's time package gebruikt layouts in plaats van locale formatting
// Voor volledige i18n datumformattering, gebruik github.com/goodsign/monday of iets dergelijks
date := time.Date(2025, 1, 12, 0, 0, 0, 0, time.UTC)
fmt.Println(date.Format("01/02/2006")) // VS: 01/12/2025
fmt.Println(date.Format("02/01/2006")) // EU: 12/01/2025
fmt.Println(date.Format("2006/01/02")) // ISO: 2025/01/12
}
Opmerking: De standaardbibliotheek van Go heeft beperkte ondersteuning voor locale. Voor gebruik in productie, overweeg:
golang.org/x/textvoor nummerformatteringgithub.com/goodsign/mondayvoor gelokaliseerde datum/tijd formattinggithub.com/bojanz/currencyvoor valuta-afhandeling
Nuttige Bronnen
- Standaarden: Unicode CLDR (standaard voor lokale gegevens), IETF BCP 47 (lokale identificatoren)
- Documentatie: MDN Intl Reference
- Bibliotheken:
- Luxon (moderne datum/tijd bibliotheek met i18n)
- date-fns met lokale modules
- Globalize.js (uitgebreide i18n gebaseerd op CLDR)
- Format.js (React-georiënteerde i18n met ICU berichtensyntaxis)
- Testen: Intl polyfill voor oudere browsers
Sluiten
Het correct weergeven van data en getallen is een inspanning met lage moeite en hoge impact: meer vertrouwen, minder fouten en een soepelere wereldwijde ervaring. Centraliseer formattering, sla canonieke waarden op, gebruik overal API’s die zich bewust zijn van de lokale instellingen waar uw gebruikers tijd, geld of getallen lezen of typen—en weet wanneer u lokalisatie moet overslaan voor machineleesbare gegevens. Met de juiste caching en testen bouwt u een robuust systeem dat zich over lokale instellingen schaalt zonder prestaties op te offeren.


