Multi-Language di Laravel + Inertia React dengan laravel-react-i18n
Laravel React Tutorial #laravel #inertia #react #i18n

Multi-Language di Laravel + Inertia React dengan laravel-react-i18n

A
Abd. Asis
8 min read
Bagikan:

Di aplikasi Laravel klasik dengan Blade, menerjemahkan teks UI sesederhana memanggil __('key') di view. Laravel tahu locale-nya, render string yang tepat, selesai. Tapi begitu frontend pindah ke React via Inertia.js, pendekatan itu tidak lagi bekerja — view dirender di browser, dan PHP sudah tidak ada di sana saat user melihat halaman.

Masalah ini yang membuat banyak developer akhirnya membangun solusi sendiri: meng-export file translation ke JSON, membuat custom hook, atau bahkan menduplikasi string di dua tempat berbeda. Padahal ada package yang sudah menangani semua itu — laravel-react-i18n. Package ini menjembatani file translation PHP Laravel ke komponen React, menggunakan logika yang sama persis dengan localization bawaan Laravel.

Artikel ini membahas implementasi lengkap multi-language di stack Laravel 12 + Inertia v2 + React 19: mulai dari instalasi, konfigurasi provider, penggunaan di komponen, hingga language switcher yang menyimpan preferensi bahasa user secara persisten.

Kenapa Tidak Cukup Hanya Export JSON Manual

Pendekatan paling sederhana yang sering terlintas adalah: export semua file translation Laravel ke JSON, lalu import di React. Secara teknis ini bisa jalan, tapi ada beberapa masalah yang muncul seiring aplikasi berkembang.

Pertama, setiap kali ada perubahan di file translation PHP, proses export harus dijalankan ulang secara manual. Kedua, fitur-fitur seperti pluralization dan parameter replacement (Amount cannot exceed :max) perlu diimplementasi sendiri di sisi JavaScript. Ketiga, sinkronisasi antara locale yang aktif di server dan di client harus dikelola manual — kalau tidak hati-hati, pesan validasi dari server bisa muncul dalam bahasa yang berbeda dari UI.

laravel-react-i18n menyelesaikan semua itu. Vite plugin bawaan package ini secara otomatis mengompilasi file PHP translation ke JSON saat build, dan provider-nya menyediakan hook useLaravelReactI18n() dengan API yang konsisten — t(), tChoice(), setLocale(), dan lainnya.

Instalasi dan Setup Awal

Ada dua package yang perlu dipasang: satu di sisi PHP untuk mengelola file bahasa, dan satu lagi di sisi JavaScript untuk membaca translation tersebut di React.

Menginstal Package PHP dan JavaScript

Jalankan perintah berikut di root project:

composer require laravel-lang/common
npm install laravel-react-i18n

Package laravel-lang/common menyediakan file translation untuk berbagai bahasa dalam format yang siap pakai Laravel. Setelah terinstal, tambahkan bahasa yang dibutuhkan:

php artisan lang:add en
php artisan lang:add id

Perintah ini akan membuat folder lang/en/ dan lang/id/ berisi file translation standar Laravel seperti validation.php, auth.php, dan pagination.php.

Selanjutnya, publish konfigurasi laravel-react-i18n:

php artisan vendor:publish --tag=laravel-react-i18n-config

Menambahkan Vite Plugin

Buka file vite.config.js dan tambahkan plugin i18n dari laravel-react-i18n:

// vite.config.js
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import react from '@vitejs/plugin-react'
import i18n from 'laravel-react-i18n/vite'

export default defineConfig({
    plugins: [
        laravel({
            input: 'resources/js/app.tsx',
            refresh: true,
        }),
        react(),
        i18n(),
    ],
})

Plugin i18n() bekerja di belakang layar: saat Vite berjalan, ia membaca semua file PHP di folder lang/ dan mengompilasinya menjadi JSON yang bisa di-import oleh React. Tidak perlu menjalankan script export terpisah.

Mengonfigurasi Provider di app.tsx

Provider LaravelReactI18nProvider harus membungkus seluruh aplikasi agar semua komponen bisa mengakses fungsi translation. Buka file resources/js/app.tsx dan modifikasi bagian createInertiaApp:

// resources/js/app.tsx
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { LaravelReactI18nProvider } from 'laravel-react-i18n'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'

createInertiaApp({
    resolve: (name) =>
        resolvePageComponent(
            `./pages/${name}.tsx`,
            import.meta.glob('./pages/**/*.tsx')
        ),
    setup({ el, App, props }) {
        const initialLocale = props.initialPage.props.locale as string || 'id'

        createRoot(el).render(
            <LaravelReactI18nProvider
                locale={initialLocale}
                fallbackLocale="id"
                files={import.meta.glob('/lang/*.json', { eager: true })}
            >
                <App {...props} />
            </LaravelReactI18nProvider>
        )
    },
})

Ada tiga prop penting di sini. locale menentukan bahasa awal — nilainya diambil dari shared data yang dikirim server (akan dikonfigurasi di langkah berikutnya). fallbackLocale adalah bahasa cadangan jika translation untuk locale aktif tidak ditemukan. Dan files menggunakan import.meta.glob() untuk me-load semua file JSON hasil kompilasi Vite plugin.

Membagikan Locale dari Server via Middleware

Agar React tahu bahasa apa yang sedang aktif, locale perlu dibagikan dari Laravel ke setiap response Inertia. Buka file HandleInertiaRequests middleware:

// app/Http/Middleware/HandleInertiaRequests.php
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        $locale = 'id';

        if (auth()->check()) {
            $locale = auth()->user()->getPreference('general')['locale'] ?? 'id';
        }

        App::setLocale($locale);

        return [
            ...parent::share($request),
            'locale' => $locale,
        ];
    }
}

App::setLocale($locale) memastikan bahwa __() di sisi server juga menggunakan bahasa yang benar. Ini penting karena pesan validasi, notifikasi email, dan response API lainnya ikut terjemahkan sesuai preferensi user.

Jika belum punya mekanisme penyimpanan preferensi user, untuk tahap awal bisa gunakan session:

$locale = session('locale', 'id');

Menyusun File Translation per Fitur

Alih-alih menaruh semua string di satu file besar, pecah berdasarkan fitur atau model. Pendekatan ini membuat file lebih mudah dikelola saat aplikasi berkembang.

lang/
  en/
    expense.php
    invoice.php
    general.php
  id/
    expense.php
    invoice.php
    general.php

Contoh isi file translation untuk fitur expense:

// lang/en/expense.php
<?php

return [
    'title' => 'Expense Data',
    'columns' => [
        'transaction_date' => 'Transaction Date',
        'description' => 'Description',
        'amount' => 'Amount',
    ],
    'form' => [
        'title' => 'Create New Expense',
        'pay_from' => 'Pay From',
        'toast_save_success' => 'Expense saved successfully!',
    ],
    'actions' => [
        'create' => 'New Expense',
        'edit' => 'Edit',
        'delete' => 'Delete',
    ],
];

Dan padanannya dalam bahasa Indonesia:

// lang/id/expense.php
<?php

return [
    'title' => 'Data Pengeluaran',
    'columns' => [
        'transaction_date' => 'Tanggal Transaksi',
        'description' => 'Keterangan',
        'amount' => 'Jumlah',
    ],
    'form' => [
        'title' => 'Buat Pengeluaran Baru',
        'pay_from' => 'Bayar Dari',
        'toast_save_success' => 'Pengeluaran berhasil disimpan!',
    ],
    'actions' => [
        'create' => 'Pengeluaran Baru',
        'edit' => 'Ubah',
        'delete' => 'Hapus',
    ],
];

Kunci yang penting: struktur array harus identik antara semua file bahasa. Jika en/expense.php punya key form.toast_save_success, maka id/expense.php juga harus punya key yang sama. Jika tidak, string akan fallback ke fallbackLocale — atau lebih buruk, menampilkan key mentah di UI.

Menggunakan Translation di Komponen React

Setelah semua konfigurasi selesai, panggil hook useLaravelReactI18n() di komponen mana pun untuk mengakses fungsi translation:

// resources/js/pages/Expense/Index.tsx
import { useLaravelReactI18n } from 'laravel-react-i18n'

export default function ExpenseIndex() {
    const { t } = useLaravelReactI18n()

    return (
        <div>
            <h1>{t('expense.title')}</h1>

            <table>
                <thead>
                    <tr>
                        <th>{t('expense.columns.transaction_date')}</th>
                        <th>{t('expense.columns.description')}</th>
                        <th>{t('expense.columns.amount')}</th>
                    </tr>
                </thead>
            </table>

            <button>{t('expense.actions.create')}</button>
        </div>
    )
}

Fungsi t() menerima dot-notation key yang sesuai dengan struktur file PHP. expense.columns.transaction_date merujuk ke key columns.transaction_date di file expense.php.

Menggunakan Parameter Dinamis di Translation

Untuk string yang membutuhkan nilai dinamis, definisikan placeholder dengan prefix : di file translation:

// lang/en/over_receipt.php
<?php

return [
    'refund_form' => [
        'amount_exceeds_balance' => 'Amount cannot exceed remaining credit (:max)',
    ],
];

Lalu kirim nilai parameter sebagai argumen kedua t():

import { useLaravelReactI18n } from 'laravel-react-i18n'
import { toast } from 'sonner'

function RefundForm({ availableBalance }: { availableBalance: number }) {
    const { t } = useLaravelReactI18n()

    const handleValidationError = () => {
        toast.error(
            t('over_receipt.refund_form.amount_exceeds_balance', {
                max: new Intl.NumberFormat('id-ID').format(availableBalance),
            })
        )
    }

    return (
        // form component
    )
}

Parameter replacement bekerja persis seperti __('key', ['max' => $value]) di PHP — konsistensi ini yang membuat laravel-react-i18n terasa natural bagi developer Laravel.

Reactivity: Jangan Lupa t di useMemo

Satu hal yang sering terlewat: jika menggunakan useMemo untuk meng-cache data yang berisi translated string, pastikan t masuk ke dependency array. Tanpa ini, komponen tidak akan re-render saat locale berubah.

const columns = useMemo(() => [
    { header: t('expense.columns.transaction_date'), accessorKey: 'date' },
    { header: t('expense.columns.description'), accessorKey: 'description' },
    { header: t('expense.columns.amount'), accessorKey: 'amount' },
], [t])

Tanpa t di dependency array, kolom tabel akan tetap menampilkan bahasa lama meskipun user sudah mengganti locale.

Membangun Language Switcher

Language switcher adalah komponen yang memungkinkan user berpindah bahasa secara real-time tanpa reload halaman. Hook useLaravelReactI18n menyediakan semua method yang dibutuhkan:

// resources/js/components/LanguageSwitcher.tsx
import { useLaravelReactI18n } from 'laravel-react-i18n'
import { router } from '@inertiajs/react'

const languages = [
    { code: 'id', label: 'Bahasa Indonesia' },
    { code: 'en', label: 'English' },
]

export function LanguageSwitcher() {
    const { currentLocale, setLocale, loading } = useLaravelReactI18n()

    const handleChange = (code: string) => {
        setLocale(code)
        document.documentElement.lang = code

        router.post('/user/preferences', { locale: code }, {
            preserveState: true,
            preserveScroll: true,
        })
    }

    return (
        <select
            value={currentLocale()}
            onChange={(e) => handleChange(e.target.value)}
            disabled={loading}
        >
            {languages.map((lang) => (
                <option key={lang.code} value={lang.code}>
                    {lang.label}
                </option>
            ))}
        </select>
    )
}

Ada tiga hal yang terjadi saat user memilih bahasa baru. setLocale(code) langsung mengganti semua string di UI tanpa menunggu response server — ini yang membuat pergantian terasa instan. document.documentElement.lang = code memperbarui atribut lang di tag <html>, penting untuk aksesibilitas dan fitur browser seperti spell-check. Dan router.post() mengirim preferensi ke backend untuk disimpan secara persisten.

Menyimpan Preferensi Bahasa di Backend

Agar pilihan bahasa user tidak hilang saat refresh atau login ulang, simpan preferensi tersebut di database. Buat route dan controller untuk menangani penyimpanan:

// routes/web.php
Route::post('/user/preferences', [UserPreferenceController::class, 'store'])
    ->middleware('auth');
// app/Http/Controllers/UserPreferenceController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;

class UserPreferenceController extends Controller
{
    public function store(Request $request)
    {
        $locale = $request->input('locale', 'id');

        if (!in_array($locale, ['id', 'en'])) {
            $locale = 'id';
        }

        auth()->user()->setPreference('general', [
            'locale' => $locale,
        ]);

        App::setLocale($locale);

        return redirect()->back();
    }
}

Perhatikan validasi whitelist di controller: hanya locale yang didukung (id, en) yang diterima. Ini mencegah user mengirim nilai locale sembarang melalui request yang dimanipulasi. Selalu validasi input locale di backend, jangan hanya mengandalkan UI.

Translation di Sisi Server

Meskipun sebagian besar teks UI ditangani oleh React, ada skenario di mana translation harus terjadi di sisi PHP: pesan validasi, notifikasi email, response API, dan flash message.

Karena App::setLocale() sudah dipanggil di middleware HandleInertiaRequests, helper __() otomatis menggunakan bahasa yang benar:

// Di controller atau form request
$validator->messages()->add(
    'amount',
    __('expense.validation.amount_required')
);

// Di notification
public function toMail($notifiable)
{
    return (new MailMessage)
        ->subject(__('notification.expense_approved_subject'))
        ->line(__('notification.expense_approved_body', [
            'amount' => number_format($this->expense->amount),
        ]));
}

Konsistensi bahasa antara frontend dan backend ini yang membuat pengalaman user terasa utuh — pesan error dari validasi server muncul dalam bahasa yang sama dengan tombol dan label di UI.

Gotcha yang Sering Ditemui

Beberapa masalah yang umum muncul saat implementasi multi-language di stack ini:

  • t tidak masuk dependency useMemo/useCallback — Komponen tidak re-render saat locale berubah. Selalu sertakan t di dependency array.
  • Lupa set document.documentElement.lang — Browser dan screen reader tidak tahu bahasa aktif berubah, yang berdampak pada aksesibilitas.
  • Struktur key tidak sinkron antar file bahasa — Key form.title ada di en/expense.php tapi tidak ada di id/expense.php. Hasilnya: string mentah muncul di UI saat locale berganti. Pastikan kedua file selalu punya struktur identik.
  • Tidak memvalidasi locale di backend — Tanpa whitelist, user bisa mengirim locale ../../etc/passwd atau nilai random lainnya. Selalu gunakan in_array() untuk memvalidasi.
  • Asumsi locale persist tanpa disimpansetLocale() hanya mengubah state di React. Tanpa mengirim ke backend dan menyimpan di database, preferensi hilang saat halaman di-refresh.

Kesimpulan

Dengan laravel-react-i18n, implementasi multi-language di stack Laravel + Inertia + React tidak lagi membutuhkan solusi custom yang rapuh. File translation PHP yang sudah ada tetap menjadi single source of truth, Vite plugin menangani kompilasi otomatis, dan hook useLaravelReactI18n() menyediakan API yang konsisten dengan konvensi Laravel. Untuk aplikasi yang perlu mendukung lebih dari dua bahasa atau membutuhkan fitur pluralization yang lebih kompleks, eksplorasi method tChoice() dan konfigurasi lazy loading bisa menjadi langkah selanjutnya. Jika tertarik mendalami best practices lainnya di stack ini, baca juga hal-hal yang harus diperhatikan saat menggunakan Laravel + Inertia.

Referensi

  1. 1laravel-react-i18n - GitHub Repository & Documentation
  2. 2Laravel 12.x - Localization (Dokumentasi Resmi)
  3. 3Laravel Lang Common - Package Documentation
  4. 4laravel-react-i18n - npm Package

Tentang Penulis

Abd. Asis

Abd. Asis

Software Developer dan Laravel Programmer dari Madura, Indonesia. Passionate tentang PHP, Laravel, dan teknologi web modern.

Artikel Terkait

Artikel lain yang mungkin menarik untuk kamu