Mengapa Tanggal dan Angka Perlu Dilokalkan
TABLE OF CONTENTS
Jika Anda melayani pengiriman secara global, satu string yang sama bisa berarti hal berbeda bagi pengguna di berbagai negara: 12/01/2025, 1.234, 1,234, 12:00, 00:00. Perbedaan kecil ini bukan sekadar soal tampilan—mereka memengaruhi kepercayaan, pemahaman, bahkan kepatuhan hukum. Panduan ini menjelaskan mengapa tanggal dan angka harus dilokalkan, apa saja yang berubah tergantung lokal, dan bagaimana cara mengimplementasikannya dengan benar di seluruh produk Anda.
Apa yang Berubah Berdasarkan Lokal
- Tanggal: urutan (MDY vs DMY vs YMD), pemisah, nama bulan/hari; beberapa pasar menggunakan kalender non-Gregorian.
- Waktu: format 12 jam vs 24 jam, penanda AM/PM, zona waktu, aturan daylight saving, hari pertama dalam seminggu (Minggu vs Senin).
- Angka: pemisah desimal (
.vs,), gaya pemisahan ribuan (1,234vs1.234vs1 234), pemisahan gaya India (1,23,456), spasi non-breaking, gaya minus/plus (tanda hubung vs minus sejati−). - Mata uang: simbol vs kode ISO, posisi simbol (di depan/belakang), spasi sempit vs lebar, presisi desimal (0, 2, atau 3+), format negatif akuntansi (misal,
(1 234,56 €)). - Persen dan satuan: penempatan spasi dan tanda (
50%vs50 %), nama satuan dalam lokal, sistem metrik vs imperial.
Dampak Bisnis
- Kepercayaan dan konversi: Harga, biaya, dan tanggal yang “terlihat salah” menurunkan pembelian dan meningkatkan churn.
- Risiko operasional: Tanggal yang salah baca bisa menggeser pemesanan atau tenggat waktu; kesalahan parsing merusak faktur, ekspor, dan analitik.
- Kepatuhan: Dokumen keuangan dengan format salah bisa melanggar aturan faktur, pajak, atau pelaporan.
- Beban dukungan: Pengguna mengajukan tiket untuk mengklarifikasi waktu, mata uang, dan input angka yang tidak sesuai harapan.
Insiden Dunia Nyata
Sebuah platform pemesanan perjalanan asal Eropa menampilkan tanggal keberangkatan sebagai 01/03/2024 tanpa konteks lokal. Pelanggan Amerika Serikat mengartikan ini sebagai “3 Januari” sementara pelanggan Inggris dan Eropa membacanya sebagai “1 Maret”.
Dampak:
- 12% pemesanan internasional dilakukan pada tanggal yang salah
- Layanan pelanggan menerima lebih dari 3.400 tiket klarifikasi dalam satu minggu
- $2,3 juta dalam bentuk pengembalian dana dan biaya pemesanan ulang
- 8% pelanggan yang terdampak beralih ke pesaing
Penyebab utama: Format tanggal DD/MM/YYYY yang di-hardcode pada konfirmasi email, sementara situs web menggunakan MM/DD/YYYY untuk pengguna Amerika Serikat. Perbaikannya hanya membutuhkan waktu 3 jam dari tim engineering—ambiguitas ini menimbulkan biaya 800 kali lebih besar daripada pencegahannya.
Pelajaran: Tanggal tanpa konteks adalah bom waktu bagi produk global mana pun.
Kesalahan Umum
- Format yang di-hardcode:
MM/DD/YYYYatau1,234.56tertanam di UI, email, PDF, atau ekspor CSV. - Menyimpan string lokal: Menyimpan “Dec 1, 2025” alih-alih waktu ISO menyebabkan bug parsing dan pergeseran zona waktu.
- Parsing naif: Menggunakan default lokal server; mengasumsikan pemisah atau jam 12/24 jam.
- Penggabungan string: Membuat “jumlah + mata uang” seperti
"$" + 1234.56alih-alih pemformatan yang sesuai lokal. - Ambiguitas zona waktu: Menampilkan waktu lokal tanpa zona eksplisit untuk tindakan yang sensitif terhadap waktu.
- Ketidakkonsistenan browser: Safari, Firefox, dan Chrome dapat memformat tanggal/angka secara berbeda; selalu uji di semua browser target.
- Celah rendering sisi server: Node.js tanpa data ICU penuh (
node --with-intl=full-icu) menghasilkan format yang tidak lengkap atau salah. - Perangkap performa: Membuat instance baru
Intl.*dalam loop atau siklus render alih-alih melakukan caching pada formatter.
Praktik Terbaik
- Simpan nilai kanonik: Tanggal/waktu sebagai ISO‑8601 dalam UTC; uang sebagai satuan kecil (sen) atau desimal presisi tinggi.
- Tampilkan sesuai lokal pengguna: Gunakan API yang sadar lokal (misalnya,
Intl.DateTimeFormat,Intl.NumberFormat) untuk tampilan. - Validasi dan parsing sesuai lokal: Terima input lokal jika sesuai; tampilkan placeholder/contoh untuk membimbing pengguna.
- Jelaskan waktu secara eksplisit: Tampilkan singkatan zona waktu atau offset; izinkan pengguna memilih jika hasil bergantung pada waktu.
- Kejelasan mata uang: Gunakan kode ISO jika ada ambiguitas; hormati penempatan simbol dan spasi sesuai lokal.
- Sentralisasi pemformatan: Satu lapisan utilitas untuk semua permukaan (UI, email, PDF, ekspor) agar konsisten.
- Cache formatter: Pemanggilan konstruktor
Intl.*mahal; buat sekali dan gunakan ulang instance per lokal.
Kapan TIDAK Perlu Melokal
Tidak semua hal harus dilokal. Berikut beberapa skenario di mana format konsisten yang dapat dibaca mesin lebih baik:
- Ekspor yang dapat dibaca mesin: File CSV yang digunakan oleh pipeline data atau alat analitik harus memakai format tetap (misalnya, tanggal ISO-8601, titik sebagai pemisah desimal). Dokumentasikan format ini dengan jelas di header ekspor atau file README.
- Respons API: REST/GraphQL API harus mengembalikan tanggal sebagai string ISO-8601 dan angka dalam format JSON standar. Biarkan klien menangani lokal sesuai preferensi pengguna mereka.
- Log internal dan metrik: Log, dashboard monitoring, dan query database mendapat manfaat dari format konsisten untuk parsing, agregasi, dan alert. Gunakan ISO-8601 dan format angka standar.
- Identifier kanonik: ID transaksi, nomor pesanan, atau referensi yang mungkin perlu dikomunikasikan lintas lokal sebaiknya menghindari format spesifik lokal.
Aturan praktis: Lokal untuk manusia yang membaca konten; gunakan format standar untuk mesin yang memproses data.
Rencana Implementasi
- Audit permukaan: Inventarisasi tanggal, waktu, angka, mata uang di UI, email, PDF, ekspor CSV/Excel, analitik, dan log.
- Definisikan lokal: Daftar lokal yang didukung dan fallback default; tentukan kebijakan jam 12/24 per lokal.
- Bangun utilitas: Bungkus API
Intl.*(atau pustaka) dengan helper bersama dan pengujian; terapkan caching formatter. - Strategi input: Perbarui formulir agar menerima dan memvalidasi format lokal; simpan nilai kanonik bersama input mentah.
- Aturan konten: Dokumentasikan gaya untuk tanggal pendek/panjang, tanggal relatif, tampilan mata uang, dan format persen.
- Peluncuran: Konversi permukaan dengan trafik dan risiko tertinggi terlebih dahulu; rilis di balik feature flag jika diperlukan.
Strategi Pengujian
Selain QA manual, terapkan pengujian otomatis untuk mencegah regresi:
- Pengujian snapshot: Bekukan output terformat untuk setiap lokal yang didukung. Perubahan memicu review untuk memastikan perubahan memang disengaja.
- Pengujian round-trip: Pastikan
format → parse → formatmenghasilkan hasil yang stabil. Menangkap kehilangan presisi atau format ambigu. - Kasus edge: Uji angka negatif, nol, angka sangat besar (miliar/triliun), transisi DST, tahun kabisat, dan tanggal batas (1 Jan, 31 Des).
- Validasi lintas browser: Pengujian otomatis di Chrome, Firefox, Safari, dan Edge untuk menangkap perbedaan rendering.
- Fallback lokal: Pastikan degradasi yang mulus saat lokal yang tidak didukung diminta.
- Benchmark performa: Ukur biaya pembuatan formatter; pastikan caching berjalan di produksi.
Tips Performa
API Intl.* sangat kuat tetapi bisa lambat jika digunakan secara tidak tepat. Ikuti panduan berikut:
// ❌ BURUK: Membuat formatter baru setiap kali dipanggil
function formatPrice(amount, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
}
// ✅ BAIK: 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 instance formatter: Pembuatan objek
Intl.*memakan waktu (~1–5ms); formatting dengan instance yang sudah ada sangat cepat (~0,01ms). - Hindari di dalam loop: Jangan pernah membuat formatter di dalam
map(),forEach(), atau loop render. - Lazy-load data lokal: Untuk aplikasi web, pertimbangkan code-splitting agar hanya memuat locale yang dibutuhkan, sehingga ukuran bundle lebih kecil.
- Server-side: Gunakan satu formatter per siklus request/response; hindari membuat formatter untuk setiap item pada dataset besar.
Daftar Periksa QA
- Tanggal tidak ambigu dalam format
en-US,en-GB,de-DE,fr-FR,hi-IN,ja-JP,ar-EG. - Waktu menggunakan konvensi 12/24 jam yang benar; penanda AM/PM muncul sesuai harapan; zona waktu ditampilkan untuk alur yang sensitif waktu.
- Angka menggunakan pemisah desimal dan ribuan yang benar; spasi non-breaking digunakan sesuai standar (misal,
fr-FR). - Mata uang menampilkan simbol/kode, posisi, dan presisi desimal yang benar untuk setiap locale; angka negatif tampil dengan benar.
- Input menerima dan mem-parsing data lokal pengguna (atau dengan jelas membatasi format yang didukung); pesan validasi peka terhadap locale.
- Ekspor (CSV/PDF) sesuai ekspektasi locale atau dengan jelas mendokumentasikan format tetap untuk konsumsi mesin.
- Lintas browser: Tanggal, waktu, dan angka tampil konsisten di Chrome, Firefox, Safari, dan Edge.
- Performa: Tidak ada pembuatan formatter di dalam loop; instance yang sudah ada digunakan ulang di seluruh render.
Contoh Kode
JavaScript
// Tanggal
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
console.log(new Intl.DateTimeFormat(‘en-GB’, { dateStyle: ‘short’, timeZone: ‘UTC’ }).format(date)); // → 12/01/2025 (hari/bulan/tahun)
console.log(new Intl.DateTimeFormat(‘ja-JP’, { dateStyle: ‘medium’, timeZone: ‘UTC’ }).format(date)); // → 2025/01/12
// Waktu dengan tampilan zona waktu 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
// Angka 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 sebagai pemisah grup)
// Penomoran India console.log(new Intl.NumberFormat(‘hi-IN’).format(1234567.89)); // → 12,34,567.89
// Mata uang 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 (tanpa desimal)
// Persen dan satuan 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 %
// Contoh caching formatter 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
# Tanggal
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
# Waktu dengan zona waktu
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
# Angka
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
# Penomoran India
print(format_number(1234567.89, locale='hi_IN')) # → 12,34,567.89
# Mata uang
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
# Persen
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;
// Tanggal
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
// Waktu
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
// Angka
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
// Mata Uang
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
```go
package main
import (
"fmt"
"time"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
)
func main() {
// Angka
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
// Mata uang (menggunakan paket number)
fmt.Println(pUS.Sprint(number.Decimal(n, number.Scale(2)))) // → 1,234.56
// Tanggal - paket time Go menggunakan layout, bukan format lokal
// Untuk format tanggal i18n penuh, gunakan github.com/goodsign/monday atau sejenisnya
date := time.Date(2025, 1, 12, 0, 0, 0, 0, time.UTC)
fmt.Println(date.Format("01/02/2006")) // AS: 01/12/2025
fmt.Println(date.Format("02/01/2006")) // Eropa: 12/01/2025
fmt.Println(date.Format("2006/01/02")) // ISO: 2025/01/12
}
Catatan: Pustaka standar Go memiliki dukungan lokal yang terbatas. Untuk penggunaan produksi, pertimbangkan:
golang.org/x/textuntuk format angkagithub.com/goodsign/mondayuntuk format tanggal/waktu lokalgithub.com/bojanz/currencyuntuk penanganan mata uang
Sumber Daya Berguna
- Standar: Unicode CLDR (standar data lokal), IETF BCP 47 (identifikasi lokal)
- Dokumentasi: Referensi MDN Intl
- Library:
- Luxon (library tanggal/waktu modern dengan dukungan i18n)
- date-fns dengan modul lokal
- Globalize.js (i18n komprehensif berbasis CLDR)
- Format.js (i18n berfokus pada React dengan sintaks pesan ICU)
- Pengujian: Intl polyfill untuk browser lama
Penutup
Menangani tanggal dan angka dengan benar adalah langkah mudah yang berdampak besar: kepercayaan lebih tinggi, lebih sedikit kesalahan, dan pengalaman global yang lebih mulus. Sentralisasi format, simpan nilai kanonik, gunakan API yang sadar lokal di setiap tempat pengguna membaca atau mengetik waktu, uang, atau angka—dan pahami kapan harus melewati lokalisasi untuk data yang dapat dibaca mesin. Dengan caching dan pengujian yang tepat, Anda akan membangun sistem tangguh yang dapat diskalakan ke berbagai lokal tanpa mengorbankan performa.


