Aplikasi multibahasa dimulai dari satu disiplin sederhana: teks antarmuka tidak boleh ditulis langsung di dalam kode. Kalimat echo "Selamat datang" yang tertanam di template berarti setiap penambahan bahasa menuntut penggandaan berkas kode, dan penerjemah harus menyentuh file program hanya untuk mengubah satu kata. Pola yang benar memisahkan keduanya: kode memanggil pengenal (misalnya home.salam), dan berkas terjemahan memetakan pengenal itu ke kalimat final dalam tiap bahasa. Pada chapter ini kita bangun sistem berkas terjemahan berbasis array PHP — dari katalog sederhana, struktur direktori lang/, fallback antarbahasa, parameter dalam pesan, hingga kunci bersarang — fondasi yang nanti dilengkapi aturan jamak pada chapter Pluralization
.
Terjemahan sebagai Peta Kunci ke Pesan #
Bentuk paling dasar berkas terjemahan adalah array asosiatif: kunci sebagai pengenal stabil, nilai sebagai kalimat berbahasa manusia. Fungsi pencarian kecil menyatukan keduanya:
<?php
$katalogId = [
"home.salam" => "Selamat datang",
"keranjang.total" => "Total belanja Anda",
];
function trans(string $kunci, array $katalog): string
{
return $katalog[$kunci] ?? "[{$kunci}]";
}
echo trans("home.salam", $katalogId), "\n";
echo trans("profil.bio", $katalogId), "\n";Output yang Dihasilkan:
Selamat datang
[profil.bio]Kunci "home.salam" diterjemahkan menjadi kalimatnya, sedangkan kunci "profil.bio" yang tidak ada di katalog menghasilkan penanda [profil.bio] — bukan error, bukan string kosong. Penanda ini penting: saat pengembang lupa menambahkan terjemahan, layar tetap menunjukkan kunci yang hilang alih-alih halaman yang kosong, sehingga kelalaian langsung terlihat saat testing. Seluruh kecerdasan sistem sesederhana apa pun terletak pada fungsi pencarian ini; bagian-bagian berikut hanya memperkaya bentuk katalog dan fungsinya.
Menyusun Direktori lang #
Aplikasi nyata punya ratusan pesan, dan menumpuk semuanya dalam satu array per bahasa cepat atau lambat menjadi sulit dirawat. Konvensi yang lazim adalah memecah per bahasa dan per domain — lang/id/app.php untuk pesan umum, lang/id/validasi.php untuk pesan validasi, dan seterusnya:
lang/
├── id/
│ ├── app.php
│ └── validasi.php
└── en/
├── app.php
└── validasi.phpKarena tiap berkas hanyalah PHP array yang di-return, pemuatannya selesai dalam satu fungsi:
<?php
$base = sys_get_temp_dir() . "/latihan-lang";
foreach (["id", "en"] as $kode) {
if (!is_dir("{$base}/{$kode}")) {
mkdir("{$base}/{$kode}", 0777, true);
}
}
file_put_contents("{$base}/id/app.php", <<<'PHP'
<?php
return [
"home.salam" => "Selamat datang",
"keranjang.total" => "Total belanja Anda",
];
PHP);
file_put_contents("{$base}/en/app.php", <<<'PHP'
<?php
return [
"home.salam" => "Welcome",
"keranjang.total" => "Your total",
"keranjang.kosong" => "Your cart is empty",
];
PHP);
function muatKatalog(string $baseLang, string $locale, string $domain = "app"): array
{
$berkas = "{$baseLang}/{$locale}/{$domain}.php";
return is_file($berkas) ? require $berkas : [];
}
$katalogId = muatKatalog($base, "id");
$katalogEn = muatKatalog($base, "en");
echo $katalogId["home.salam"], " / ", $katalogEn["home.salam"], "\n";
echo "jumlah kunci — id: ", count($katalogId), ", en: ", count($katalogEn), "\n";Output yang Dihasilkan:
Selamat datang / Welcome
jumlah kunci — id: 2, en: 3Contoh di atas membangun struktur lang/id dan lang/en di direktori sementara agar bisa dijalankan langsung — pada proyek nyata kedua berkas itu sudah ada di repositori dan fungsinya tinggal dipanggil. Fungsi muatKatalog() merangkai jalur dari tiga bagian (basis, kode bahasa, domain) lalu memuat berkas hanya bila ada; require mengembalikan array-nya tanpa perlu variabel perantara. Hasilnya dua katalog hidup berdampingan, dan perhatikan bahwa jumlah kuncinya memang boleh beda — bahasa Inggris punya keranjang.kosong yang belum ada padanannya di Indonesia, keadaan yang bagian berikutnya menanganinya.
Konvensi Penamaan Kunci #
Kunci adalah kontrak antara kode dan penerjemah, jadi namanya harus stabil dan bermakna. Dua aturan praktis: susun bertingkat mengikuti domain (validasi.email.invalid), dan jangan pernah memakai teks sumber sebagai kunci:
| Contoh kunci | Penilaian | Alasan |
|---|---|---|
keranjang.total | tepat | domain.subjek, deskriptif, stabil |
validasi.email.invalid | tepat | bertingkat rapi, mudah dicari |
teks3 | buruk | tidak menjelaskan isi, mudah tertukar |
Selamat datang | buruk | teks sumber sebagai kunci — tiap revisi copywriting memutus kontrak |
home.btn1 | buruk | nomor sementara, tidak bermakna saat halaman berubah |
Alasan larangan “teks sumber sebagai kunci” layak ditegaskan: kalimat marketing sering direvisi (“Selamat datang” → “Halo, selamat bergabung!”), dan bila teks itu menjadi kunci, satu revisi kecil memaksa Anda mengubah kode di semua tempat yang memanggilnya. Kunci deskriptif tidak pernah berubah walau kalimatnya berganti berkali-kali.
Fallback saat Terjemahan Hilang #
Katalog antarbahasa hampir tidak pernah sinkron sempurna — terjemahan baru datang belakangan dari penerjemah. Aplikasi yang matang tidak menampilkan penanda kosong untuk kasus ini, melainkan jatuh ke bahasa cadangan yang umumnya bahasa Inggris. Rantainya: cek katalog utama, lalu katalog fallback, terakhir penanda kunci:
<?php
function transDenganFallback(string $kunci, array $utama, array $cadangan): string
{
return $utama[$kunci] ?? $cadangan[$kunci] ?? "[{$kunci}]";
}
$id = [
"home.salam" => "Selamat datang",
"keranjang.total" => "Total belanja Anda",
];
$en = [
"home.salam" => "Welcome",
"keranjang.total" => "Your total",
"keranjang.kosong" => "Your cart is empty",
];
echo transDenganFallback("keranjang.total", $id, $en), "\n";
echo transDenganFallback("keranjang.kosong", $id, $en), "\n";
echo transDenganFallback("tautan.bantuan", $id, $en), "\n";Output yang Dihasilkan:
Total belanja Anda
Your cart is empty
[tautan.bantuan]Tiga baris hasil memetakan tiga keadaan: keranjang.total ada di Indonesia sehingga langsung terpakai; keranjang.kosong tidak ada di Indonesia tetapi tertangkap katalog Inggris sehingga pengguna melihat bahasa campuran — jauh lebih baik daripada penanda kosong; dan tautan.bantuan tidak ada di keduanya sehingga penanda [tautan.bantuan] muncul sebagai alarm. Operator null coalescing ?? merangkai rantai itu dalam satu ekspresi — setiap ?? berarti “bila belum ada, coba sumber berikutnya”.
Parameter dalam Pesan #
Pesan nyata hampir selalu berisi nilai dinamis: nama pengguna, nama produk, jumlah hari. Menempel nilai dengan concatenation ("Halo, " . $nama . "!") merusak pemisahan kode-terjemahan, karena urutan kata menjadi kaku mengikuti kode. Solusinya adalah placeholder bernama yang ditulis langsung di pesan dan digantikan saat runtime — fungsi strtr() adalah alat paling tepat untuk penggantian seperti ini:
<?php
function transParam(string $pesan, array $parameter): string
{
$pasangan = [];
foreach ($parameter as $kunci => $nilai) {
$pasangan["{" . $kunci . "}"] = (string) $nilai;
}
return strtr($pesan, $pasangan);
}
echo transParam(
"Halo {nama}, pesanan {produk} sudah dikirim.",
["nama" => "Budi", "produk" => "Kopi Toraja 250g"]
), "\n";
echo transParam("Welcome back, {nama}!", ["nama" => "Siti"]), "\n";Output yang Dihasilkan:
Halo Budi, pesanan Kopi Toraja 250g sudah dikirim.
Welcome back, Siti!Placeholder {nama} dan {produk} diganti dari pasangan kunci-nilai yang diberikan pemanggil, dan nilai yang tidak diberikan dibiarkan apa adanya — pesan tidak pernah rusak karena parameter hilang. Kelebihan strtr() dibanding str_replace() berantai adalah penggantian satu arah dalam satu pass: hasil penggantian tidak ikut discan ulang, sehingga nilai yang kebetulan mengandung teks {...} tidak membuka celah penggantian berantai. Bonus terbesarnya untuk penerjemah: kalimat dengan placeholder bisa disusun ulang bebas per bahasa — bahasa yang menaruh nama di akhir kalimat tinggal memindahkan posisi {nama} tanpa menyentuh kode.
Kunci Bersarang #
Katalog datar dengan kunci bertitik (keranjang.total) praktis, tetapi beberapa tim lebih suka struktur array bersarang yang lebih mudah dipindai dan diedit per sub-domain. Keduanya bisa dipersatukan: pencarian mengurai kunci bertitik lalu menelusuri tingkat demi tingkat:
<?php
$katalogSarang = [
"validasi" => [
"wajib" => "Kolom {kolom} wajib diisi.",
"email" => "Format email tidak valid.",
],
"keranjang" => [
"ditambahkan" => "{produk} masuk ke keranjang.",
],
];
function transSarang(array $katalog, string $kunciBerputus): string
{
$nilai = $katalog;
foreach (explode(".", $kunciBerputus) as $bagian) {
if (!is_array($nilai) || !array_key_exists($bagian, $nilai)) {
return "[{$kunciBerputus}]";
}
$nilai = $nilai[$bagian];
}
return is_string($nilai) ? $nilai : "[{$kunciBerputus}]";
}
echo transSarang($katalogSarang, "validasi.email"), "\n";
echo transSarang($katalogSarang, "keranjang.ditambahkan"), "\n";
echo transSarang($katalogSarang, "validasi.minimal"), "\n";Output yang Dihasilkan:
Format email tidak valid.
{produk} masuk ke keranjang.
[validasi.minimal]explode(".", ...) memecah "validasi.email" menjadi ["validasi", "email"], dan loop menuruni katalog tingkat demi tingkat dengan dua penjaga: is_array() menghentikan penelusuran bila bertemu nilai non-array di tengah jalur, dan array_key_exists() memastikan tingkat berikutnya memang ada. Kunci yang lolos sampai akhir dan ternyata string dikembalikan; selain itu (termasuk validasi.minimal yang tidak ada) kembali sebagai penanda. Pesan hasilnya pun masih memakai placeholder {kolom} dan {produk} — pada implementasi nyata, fungsi ini dipadukan dengan transParam() sehingga pemanggil cukup satu trans("validasi.wajib", ["kolom" => "Email"]).
Menuju Pluralization #
Satu bentuk pesan masih bergerak di luar jangkauan peta kunci-pesan biasa: jumlah. Bahasa Indonesia mengganti kata benda di depan angka (“1 pesanan”, “3 pesanan”), bahasa Inggris mengubah kalimatnya (“1 item” versus “2 items”), dan beberapa bahasa mengubah bentuk kata sesuai nilai angka dengan aturan yang jauh lebih kompleks. Menerjemahkannya dengan percabangan if di setiap halaman adalah jalan pintas yang berakhir kacau — aturan ini milik katalog, bukan kode. Chapter Pluralization
berikutnya melengkapi sistem kita dengan kontrak transChoice() beserta aturan jamak per bahasa.
Ringkasan #
Sistem terjemahan kita kini utuh sebagai arsitektur: pesan hidup di katalog array per bahasa dan per domain di bawah lang/, kunci deskriptif menjaga kontrak kode-penerjemah, rantai fallback menutup kekosongan antarbahasa, parameter bernama membuat kalimat lentur disusun ulang, dan kunci bersarang memberi struktur yang rapi dirawat. Fondasi ini sengaja dibangun murni dari PHP array agar berjalan di semua lingkungan — sama seperti formatter locale pada chapter sebelumnya. Dengan teks yang siap berganti bahasa dan format yang mengikuti locale, aplikasi Anda tinggal menghadapi satu tantangan terakhir: menyatukan semuanya dalam desain multilingual yang koheren.