Translation File Management — i18n

Translation File Management Seiring aplikasi tumbuh, mengelola file terjemahan bisa jadi rumit. Struktur yang baik sejak awal mencegah chaos di kemudian hari…

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-ID berisi "nights_other": "{{count}} malam". Bu Sri menjalankan npx i18next-cli extract. Sesudahnya, isi file id-ID untuk jumlah malam jadi apa: tetap nights_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

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

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

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

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.

Yang akan kamu pelajari