Begitu aplikasi punya empat bahasa, file terjemahan butuh aturan: di mana file disimpan, formatnya apa, key-nya dinamai bagaimana, dan siapa yang mengecek file mana yang ketinggalan. Bayangin Bu Sri menambah satu menu baru di daftar menu bahasa Indonesia. Tiap map bahasa lain butuh halaman baru itu, dan halaman untuk menu yang sudah gak dijual harus dicabut. Kalau dicek manual, pasti ada yang kelewat. Alat extract membaca kode, mendaftar key yang dipakai, lalu membandingkannya dengan tiap file.
🧪 Tebakan cepat: File sumber
id-IDberisi"nights_other": "{{count}} malam". Bu Sri menjalankannpx i18next-cli extract. Sesudahnya, isi fileid-IDuntuk jumlah malam jadi apa: tetapnights_other, berubah jadi"nights": "{{count}} malam", atau berubah jadi"nights": "nights"? Jawabannya ada di bagian "Extract otomatis: i18next-cli dan formatjs".
Susunan file: per locale, per namespace
locales/
id-ID/booking.json ← sumber, ditulis Bu Sri
id-ID/admin.json ← dashboard Bu Sri
en-US/booking.json
ja-JP/booking.json
ar-EG/booking.json
- Satu folder per locale, satu file per namespace (fitur). Kamus kartu pesan kami cuma 350 sampai 500 byte per bahasa.
- Namespace bisa di-lazy-load: halaman tamu cukup memuat
booking.json;admin.jsonbaru diambil saat dashboard dibuka. Dengan i18next,useTranslation('admin')memicu pemuatannya lewati18next-http-backend(loadPath: '/locales/{{lng}}/{{ns}}.json') ataui18next-resources-to-backend1.2.3 (resourcesToBackend((lng, ns) => import(`./locales/${lng}/${ns}.json`)), jadi tiap file jadi chunk sendiri di bundle). - File per fitur juga mengurangi konflik: tim admin dan tim booking gak mengedit file yang sama.
- Aplikasi kecil boleh satu file per locale (
messages/ja-JP.json, gaya next-intl) dengan namespace sebagai key paling luar.
Format: JSON, ICU, XLIFF, PO
Satu teks yang sama, tombol book, dalam empat format:
// JSON i18next v4 (locales/ja-JP/booking.json)
{ "book": "予約する(お支払いは現地で)" }
// JSON ICU (react-intl, next-intl, i18next-icu)
{ "booking.book": "予約する(お支払いは現地で)" }
<!-- XLIFF 2.0 -->
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="id-ID" trgLang="ja-JP">
<file id="booking">
<unit id="book">
<notes><note category="description">Tombol utama kartu pesan. Tamu bayar tunai saat tiba. Maks. 32 karakter.</note></notes>
<segment><source>Pesan kamar, bayar di tempat</source><target>予約する(お支払いは現地で)</target></segment>
</unit>
</file>
</xliff>
# PO (gettext)
#. Tombol utama kartu pesan. Tamu bayar tunai saat tiba. Maks. 32 karakter.
msgctxt "booking"
msgid "Pesan kamar, bayar di tempat"
msgstr "予約する(お支払いは現地で)"
| Format | Dipakai | Kapan dipilih |
|---|---|---|
| JSON i18next v4 | i18next, react-i18next | Aplikasi yang membaca file itu langsung; jamak lewat akhiran _one/_other |
| JSON ICU | react-intl, next-intl, FormatJS | Kalimat dengan plural dan select di dalam satu pesan |
| XLIFF | Alat penerjemah profesional (CAT tool, TMS), Angular | Bertukar file dengan penerjemah: sumber, terjemahan, catatan, dan status dalam satu file |
| PO (gettext) | PHP, WordPress, Python/Django, Poedit | Proyek yang sudah memakai gettext; jamaknya pakai rumus Plural-Forms sendiri |
Nama key: semantik atau teks sumber
- Key semantik (
book,booking.total) menamai peran teksnya. Teksnya boleh diganti tanpa menyentuh kode atau terjemahan lain. - Teks sumber sebagai key (gaya gettext:
t('Pesan kamar, bayar di tempat'), di i18next dengankeySeparator: falsedannsSeparator: false) enak dibaca di kode. Tapi membetulkan satu salah ketik mengganti key-nya, dan semua terjemahan untuk key lama hilang. Satu kata juga gak bisa punya dua arti: "Pesan" bisa berarti message atau book. - Nama yang baik: fitur plus peran (
booking.book,inbox.title), bukanbtn1atautext2. Key lama yang diganti nama harus dihapus dari semua file; kalau gak, dia jadi key yang gak terpakai.
Mencari key yang hilang dan gak terpakai
Kode kartu pesan memakai 10 key: room, perNight, checkIn, group, nights dan total (dengan count), book, cancel, breakfast, thanks. Key jamak butuh satu bentuk per kategori Intl.PluralRules bahasa itu:
const used = ['room', 'perNight', 'checkIn', 'group', 'nights', 'total', 'book', 'cancel', 'breakfast', 'thanks'];
const plural = new Set(['nights', 'total']);
const ORDER = ['zero', 'one', 'two', 'few', 'many', 'other'];
function expected(locale) {
const cats = new Intl.PluralRules(locale).resolvedOptions().pluralCategories.sort((a, b) => ORDER.indexOf(a) - ORDER.indexOf(b));
return used.flatMap((k) => (plural.has(k) ? cats.map((c) => `${k}_${c}`) : [k]));
}
function check(locale, file) {
const want = expected(locale);
return { missing: want.filter((k) => !(k in file)), unused: Object.keys(file).filter((k) => !want.includes(k)) };
}
check('ja-JP', jaJP); // { missing: ["book", "cancel", "breakfast"], unused: ["payAtHomestay"] }
check('ar-EG', arEG); // { missing: ["nights_zero", "nights_few", "nights_many", "total_zero", "total_one", "total_two", "total_few", "total_many", "breakfast", "thanks"], unused: [] }
ja-JP masih punya payAtHomestay, nama lama book sebelum Bu Sri menggantinya; karena itu book hilang dan payAtHomestay gak terpakai. ar-EG kehilangan 10 key, delapan di antaranya bentuk jamak yang gak dibutuhkan bahasa Inggris.
Extract otomatis: i18next-cli dan formatjs
i18next-parser sudah deprecated; npm menulis "use i18next-cli instead". i18next-cli 1.75.2 membaca kode dan file sekaligus:
// i18next.config.js
export default {
locales: ['id-ID', 'en-US', 'ja-JP', 'ar-EG'], // yang pertama = bahasa utama
extract: { input: 'src/**/*.{ts,tsx}', output: 'locales/{{language}}/{{namespace}}.json' },
};
$ npx i18next-cli status ja-JP
Key Status for "ja-JP":
Overall: [■■■■■■■■■■■■■■□□□□□□] 70% (7/10)
3 absent
✓ room
...
✗ book (absent)
✗ cancel (absent)
✗ breakfast (absent)
✓ thanks
npx i18next-cli statusmerangkum tiap bahasa;status ja-JPmerinci per key;status --unusedmendaftar key yang gak terpakai tanpa mengubah file.npx i18next-cli extractmenulis ulang file: key yang hilang diisi""di bahasa lain, key yang gak terpakai dihapus (removeUnusedKeys: truebawaannya).
Hasil extract pada proyek kami, diukur: bahasa Indonesia cuma punya kategori other, jadi i18next-cli 1.75.2 memakai key tanpa akhiran untuk bahasa utama. nights_other dianggap gak terpakai dan dihapus, lalu nights ditambahkan dengan nama key sebagai isinya: "nights": "nights", begitu juga "total": "total". Terjemahan "{{count}} malam" hilang, dan kartu Indonesia menulis "nights". Itu jawaban tebakannya. Di ar-EG, bentuk jamak yang hilang juga diisi "nights" dan "total", dan status menghitungnya sudah diterjemahkan (90%). Pencegahannya: di bahasa sumber yang cuma punya other, tulis key jamak tanpa akhiran ("nights": "{{count}} malam"; i18next mencoba nights_other lalu nights), jalankan extract di branch, dan baca git diff sebelum commit.
Untuk react-intl, formatjs (@formatjs/cli 6.16.34) mengumpulkan pesan dari FormattedMessage dan defineMessages: npx formatjs extract 'src/**/*.tsx' --out-file lang/id-ID.json menulis { "booking.book": { "defaultMessage": "Pesan kamar, bayar di tempat", "description": "..." } }.
Konteks untuk penerjemah
Penerjemah cuma melihat teks, bukan halamannya. "Pesan" tanpa konteks bisa diterjemahkan jadi "メッセージ". Beri tiap key: deskripsi (field description di FormatJS, <note> di XLIFF, komentar #. di PO), batas panjang ("maks. 32 karakter"), arti tiap slot ({{count}} adalah jumlah malam, {{name}} nama pemesan), dan screenshot bagian halamannya. Platform terjemahan (TMS) bisa menampilkan semua itu di samping teksnya.
⚠️ Jebakan yang sering ketemu
extracttanpa membaca diff. Di proyek berbahasa sumber Indonesia,"nights_other": "{{count}} malam"bisa berubah jadi"nights": "nights".- String kosong dari
extract. i18next menganggap""sebagai terjemahan (returnEmptyString: truebawaannya), jadi tombolja-JPtampil kosong. PasangreturnEmptyString: falsesupaya""jatuh ke bahasa cadangan. - Status 100% bukan bukti. Isian
"nights"diar-EGdihitung sudah diterjemahkan. Cari juga nilai yang sama dengan nama key-nya. - Key dinamis.
t(`status.${code}`)gak terbaca oleh extract, lalu dihapus sebagai key gak terpakai. Tulis key lengkap, atau daftarkan polanya dipreservePatterns.
TL;DR: Simpan kamus per locale dan per namespace (locales/ja-JP/booking.json), supaya tiap fitur bisa di-lazy-load (cuma terjemahan yang dibutuhkan yang dimuat) dan dikelola terpisah. JSON v4 untuk i18next, JSON ICU untuk react-intl dan next-intl, XLIFF untuk bertukar dengan penerjemah dan alat CAT, PO untuk proyek gettext. Pakai key semantik (booking.book), bukan teks sumber. Key yang hilang = key yang dipakai kode (dengan semua kategori jamak bahasa itu) tapi gak ada di file; key gak terpakai = ada di file tapi gak dipakai kode: ja-JP kehilangan book, cancel, breakfast dan masih menyimpan payAtHomestay. i18next-parser sudah deprecated; pakai npx i18next-cli status dan extract (i18next-cli 1.75.2), atau formatjs extract untuk react-intl. Selalu baca diff-nya: dengan bahasa sumber Indonesia, extract mengganti nights_other dengan "nights": "nights". Pasang returnEmptyString: false, dan beri penerjemah deskripsi, batas panjang, arti slot, dan screenshot.