i18n di Vue: Struktur yang Tidak Bikin File Membengkak
Setiap proyek Vue yang saya tangani, cepat atau lambat, butuh dukungan multi-bahasa. Yang jarang dibahas orang bukan cara pasang vue-i18n-nya, tapi apa yang terjadi setelah aplikasi berjalan cukup lama: satu file id.json yang isinya ribuan baris, sebagian besar sudah tidak dipakai, dan tidak ada yang berani menghapusnya karena tidak tahu masih dirujuk di mana. Di sistem klinik yang biasa saya kerjakan, modul bisa puluhan jumlahnya, dan kalau semua string terjemahan ditumpuk di satu file, mencari satu key saja jadi pekerjaan tersendiri. Struktur file terjemahan itu keputusan arsitektur, bukan detail kosmetik, dan sebaiknya diputuskan di awal, bukan waktu file sudah kepalang besar.
Kenapa satu file besar bermasalah
Pola paling umum dan paling gampang menjebak adalah satu file per bahasa: en.json, id.json, isinya flat semua string aplikasi. Awalnya nyaman, tapi masalah muncul seiring tim dan jumlah fitur bertambah:
- Konflik merge di git jadi sering, karena banyak developer menyentuh file yang sama untuk fitur yang tidak berkaitan.
- Sulit tahu key mana yang masih dipakai dan mana yang sisa fitur lama yang sudah dihapus.
- Reviewer tidak punya cara cepat menilai apakah terjemahan baru konsisten dengan konteks fitur terkait.
Solusinya bukan pindah ke satu file per komponen (itu ekstrem sebaliknya, terlalu granular), tapi menata per fitur atau per domain, mengikuti struktur folder aplikasi itu sendiri.
Struktur per fitur
Pola yang saya pakai: satu folder locale berisi sub-file per domain fungsional, lalu digabung saat build lewat modul i18n. Ilustrasinya kira-kira begini:
locales/
id/
common.json
auth.json
patient.json
billing.json
en/
common.json
auth.json
patient.json
billing.json
common.json menampung string lintas fitur, seperti tombol "Simpan", "Batal", pesan error generik. Sisanya mengikuti domain: modul pendaftaran pasien punya file sendiri, modul billing punya file sendiri. Saat modul dihapus atau di-refactor besar, file terjemahannya jelas kelihatan dan gampang diputuskan nasibnya.
Di sisi vue-i18n, ini tinggal digabung waktu load, misalnya dengan glob import lalu merge object per locale sebelum dipasang ke instance i18n. Key-nya sebaiknya dinamespace, misalnya patient.form.save_button, supaya walau file-nya terpisah, konteksnya tetap jelas dibaca dari key saja tanpa buka filenya.
Menangkap string hardcoded sebelum lolos review
Struktur file yang rapi tidak banyak membantu kalau developer masih menulis <button>Simpan</button> langsung di template. Ini yang paling sering lolos review karena secara visual tidak salah, aplikasi tetap jalan normal, cuma tidak bisa diterjemahkan. Beberapa kebiasaan yang membantu:
- Pasang eslint plugin i18n (misalnya aturan yang melarang string literal panjang di template) supaya lint gagal otomatis kalau ada teks mentah, bukan mengandalkan mata reviewer.
- Jadikan checklist review eksplisit: setiap PR yang menambah teks baru di UI wajib menambah key terjemahan, bukan opsional.
- Untuk pesan dari backend (misalnya validasi Laravel), pastikan pesan itu juga lewat lapisan terjemahan, bukan ditampilkan mentah dari response API, karena ini sering jadi celah yang terlewat karena dianggap "kan bukan di Vue-nya".
Saya juga membiasakan menulis key terjemahan lebih dulu sebelum menulis teksnya, semacam t('patient.form.save_button') ditulis duluan lalu isi terjemahan menyusul. Urutan kecil ini membuat hardcoded string jauh lebih jarang muncul, karena kita sudah "terpaksa" mikir lewat sistem i18n sejak awal, bukan nulis teks dulu baru diingat-ingat perlu dibungkus t() atau tidak.
Takeaway
Intinya dua hal: pecah file terjemahan mengikuti struktur fitur/domain aplikasi supaya gampang dirawat dan dihapus saat modul berubah, dan jadikan pencegahan string hardcoded bagian dari proses (lint plus kebiasaan review), bukan sekadar niat baik individu. Investasi kecil di awal ini yang membedakan i18n yang tetap enak dikelola bertahun-tahun dengan yang jadi file raksasa penuh sampah tak terpakai.