Angka dan tanggal yang sama tampil berbeda di tiap wilayah: pembaca Indonesia membaca “1.234.567,89” dan “Kamis, 15 Januari 2026”, pembaca Amerika membaca “1,234,567.89” dan “Thursday, January 15, 2026”. Aplikasi multibahasa wajib menghormati gaya ini, dan di sinilah banyak pengembang PHP tersandung: fungsi format bawaan PHP seperti date() dan number_format() sama sekali tidak sadar locale, sementara jalur sistem lewat setlocale() ternyata rapuh. Pada chapter ini kita bedah kenyataan kedua jalur itu, lalu membangun formatter murni PHP — tabel nama hari/bulan dan pemisah angka — yang berperilaku identik di setiap server. Pembahasan mendalam format tanggal berbasis objek DateTime tersedia di chapter Locale-Aware Formatting pada Section Date Time
.
Apa Itu Locale #
Locale adalah pengenal budaya perwalian perangkat lunak terhadap preferensi format: id-ID berarti bahasa Indonesia dengan wilayah Indonesia, en-US bahasa Inggris Amerika, de-DE bahasa Jerman Jerman. Dua huruf pertama (bahasa) menentukan nama hari dan bulan, dua huruf setelah garis (wilayah) menentukan konvensi angka, mata uang, dan urutan tanggal. Locale bekerja pada tiga format utama:
| Locale | Angka desimal | Tanggal panjang |
|---|---|---|
id-ID | 1.234.567,89 | Kamis, 15 Januari 2026 |
en-US | 1,234,567.89 | Thursday, January 15, 2026 |
de-DE | 1.234.567,89 | Donnerstag, 15. Januar 2026 |
Perhatikan jebakannya: Indonesia dan Jerman sama-sama memakai titik sebagai pemisah ribuan dan koma sebagai desimal, tetapi format tanggalnya berbeda total. Locale memang satu paket — Anda tidak bisa mencampur nama bulan Indonesia dengan urutan tanggal Amerika dan menyebutnya satu locale.
Kenyataan setlocale #
Jalur “resmi” untuk locale di PHP adalah setlocale(), yang meminta C library sistem mengganti locale proses. Di atas kertas, setelah setlocale(LC_ALL, "id_ID.UTF-8") berhasil, fungsi yang sadar locale akan berformat Indonesia. Praktiknya jauh lebih rapuh — mari kita buktikan langsung:
<?php
var_dump(function_exists("strftime"));
var_dump(function_exists("money_format"));
$hasil = setlocale(LC_ALL, "id_ID.UTF-8");
var_dump($hasil);
// setlocale "berhasil" — tetapi apakah formatnya benar-benar berubah?
echo @strftime("%A, %d %B %Y", strtotime("2026-01-15 08:00:00")), "\n";
setlocale(LC_ALL, "C"); // pulihkan keadaan netral untuk proses ini
Output yang Dihasilkan:
bool(true)
bool(false)
string(11) "id_ID.UTF-8"
Thursday, 15 January 2026Hasil pada build PHP yang dipakai course ini memperlihatkan tiga kenyataan sekaligus. Pertama, strftime() masih ada tetapi sudah ditandai deprecated sejak PHP 8.1 — itulah sebabnya panggilannya di atas dibungkus @ agar pemberitahuan deprecated tidak menutupi hasilnya, dan kode baru tidak boleh memakainya lagi. Kedua, money_format() bahkan tidak dikompilasi pada build ini: ketersediaan fungsi locale-sensitive bergantung pada build PHP dan libc-nya. Ketiga, dan paling penting: setlocale() mengembalikan "id_ID.UTF-8" seolah berhasil, tetapi strftime() tetap mencetak English — C library pada lingkungan minimal ini tidak memiliki data locale id_ID sehingga permintaan diabaikan tanpa error.
Tabel Nama Hari dan Bulan Murni PHP #
Solusi yang berperilaku identik di semua server adalah membawa datanya sendiri: nama hari dan bulan sebagai array PHP biasa, dipetakan dari angka yang dihasilkan DateTimeImmutable. Method format("w") mengembalikan nomor hari dalam minggu (0 untuk Minggu hingga 6 untuk Sabtu) dan format("n") nomor bulan (1–12), sehingga keduanya langsung bisa dipakai sebagai indeks:
<?php
const NAMA_BULAN = [
"Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember",
];
const NAMA_HARI = [
"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu",
];
function tanggalPanjangId(DateTimeImmutable $tanggal): string
{
$hari = NAMA_HARI[(int) $tanggal->format("w")];
$bulan = NAMA_BULAN[(int) $tanggal->format("n") - 1];
return sprintf(
"%s, %d %s %d",
$hari,
(int) $tanggal->format("j"),
$bulan,
(int) $tanggal->format("Y"),
);
}
$waktu = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
echo tanggalPanjangId($waktu), "\n";
echo tanggalPanjangId($waktu->modify("+3 days")), "\n";Output yang Dihasilkan:
Kamis, 15 Januari 2026
Minggu, 18 Januari 2026Berbeda total dengan percobaan setlocale() di atas, dua baris ini mencetak “Kamis, 15 Januari 2026” dan “Minggu, 18 Januari 2026” — persis hasil yang dijanjikan locale id_ID, tanpa bergantung pada sistem operasi sedikit pun. Tabel didefinasikan sebagai konstanta (satu salinan per proses), indeks hari dikurangi offset yang tepat ("w" mulai dari 0 cocok langsung, "n" mulai dari 1 sehingga dikurangi 1), dan fungsi menerima DateTimeImmutable sehingga zona waktu dan pergeseran tanggal ditangani objek tanggal alih-alih string. Menambah bahasa baru cukup menyalin tabel — data, bukan logika.
Format Angka per Locale #
Angka punya dua pemisah yang bertukar antar locale: ribuan dan desimal. Fungsi number_format() menerima keduanya sebagai argumen, jadi formatter per locale tinggal memetakan locale ke pasangan pemisah lalu mewakilkan pekerjaannya:
<?php
function formatAngka(float $angka, string $locale, int $jumlahDesimal = 2): string
{
[$pemisahRibuan, $pemisahDesimal] = match ($locale) {
"id-ID" => [".", ","],
"en-US" => [",", "."],
default => [" ", "."],
};
return number_format($angka, $jumlahDesimal, $pemisahDesimal, $pemisahRibuan);
}
$harga = 1234567.891;
echo formatAngka($harga, "id-ID"), "\n";
echo formatAngka($harga, "en-US"), "\n";
echo formatAngka($harga, "id-ID", 0), "\n";Output yang Dihasilkan:
1.234.567,89
1,234,567.89
1.234.568Ekspresi match memetakan setiap locale ke sepasang pemisahnya, dengan cabang default untuk locale yang belum dikenal (pemisah spalaikaa fr-FR). Hasilnya: 1.234.567,89 untuk id-ID, 1,234,567.89 untuk en-US, dan pemanggilan tanpa desimal membulatkan ke 1.234.568. Dua baris terakhir memperlihatkan bahwa number_format() juga membulatkan — 1234567.891 menjadi dua desimal. Perlu dicatat bahwa input tetap float murni: pemisah hanyalah urusan penyajian, dan perhitungan tidak pernah dilakukan pada string berformat.
IntlDateFormatter dan NumberFormatter #
Ekstensi intl membungkus pustaka ICU dan menyediakan formatter locale yang lengkap — termasuk aturan kompleks seperti penulisan tanggal bahasa Jepang dan angka bahasa Arab. Kode berikut adalah padanan langsung dari dua formatter buatan kita di atas:
<?php
$angka = new NumberFormatter("id-ID", NumberFormatter::DECIMAL);
echo $angka->format(1234567.891), "\n";
$tanggal = new IntlDateFormatter(
"id-ID",
IntlDateFormatter::FULL,
IntlDateFormatter::NONE,
"Asia/Jakarta",
);
echo $tanggal->format(new DateTimeImmutable("2026-01-15 08:00:00")), "\n";Pada server dengan ext-intl terpasang, blok di atas mencetak 1.234.567,891 (presisi bawaan tiga desimal) dan tanggal lengkap berbahasa Indonesia setara dengan “Kamis, 15 Januari 2026” sesuai data CLDR yang dibawa ICU. Ekstensi ini adalah pilihan pertama bila tersedia di lingkungan produksi Anda — data locale-nya terpelihara oleh konsorsium Unicode, jauh lebih lengkap daripada tabel manual. Kendalanya satu: ketersediaannya. ext-intl tidak selalu terpasang pada hosting bersama maupun build PHP minimal (termasuk build yang dipakai course ini), sehingga formatter murni PHP pada dua bagian sebelumnya tetap perlu Anda kuasai sebagai fondasi yang berjalan di mana saja — pendekatan yang sama dipakai framework besar yang menyertakan data locale sendiri.
Ringkasan #
Locale adalah paket preferensi format per wilayah, dan PHP menawarkan dua jalur untuk menghormatinya: setlocale() yang hasilnya bergantung pada sistem operasi (bisa gagal diam-diam seperti yang kita buktikan), atau formatter murni PHP dengan data sendiri — tabel nama hari/bulan dan pemisah angka — yang identik di semua server. ext-intl memperkuat keduanya di lingkungan yang menyediakannya. Teks yang diformat rapi tinggal satu langkah menuju aplikasi multibahasa sejati: teks itu sendiri harus bisa berganti bahasa. Chapter Translation Files
berikutnya mengurus persis itu.