Managing Translations in Inertia.js

A few helpful tips on how to handle translation strings in your Inertia.js app.

If you've ever built a multilingual app with Inertia.js, you know the pain. Laravel has a fantastic built-in translation system on the backend, but the moment you need those strings in your React, Vue, or Svelte other components, things get awkward fast.

There's no official, out-of-the-box solution for sharing translations between Laravel and your Inertia frontend. You're left stitching things together yourself — loading JSON files, passing them through shared props, writing helper functions, and hoping nothing falls through the cracks.

Let's look at the common approaches, their tradeoffs, and a cleaner way to handle it all.

The Core Problem

Inertia.js is a bridge between your Laravel backend and a frontend framework like React, Vue, or Svelte. It gives you the SPA feel without building a separate API. But translations are where this bridge gets shaky.

Laravel stores translations in lang/*.json (or lang/*/*.php files). Your frontend components need access to those strings. The question is: how do you get them there?

Approach 1: Share Everything via Inertia Props

The most common approach is to load all translations in HandleInertiaRequests middleware and share them as a prop (that's what I was doing for a long time):

// app/Http/Middleware/HandleInertiaRequests.php

public function share(Request $request): array
{
    $locale = app()->getLocale();
    $file = lang_path("{$locale}.json");

    return array_merge(parent::share($request), [
        'locale' => $locale,
        'translations' => file_exists($file)
            ? json_decode(file_get_contents($file), true)
            : [],
    ]);
}

Then on the frontend, you write a small helper to look up keys:

export function __(key, replacements = {}) {
    const { translations } = usePage().props;
    let translation = translations[key] || key;

    Object.keys(replacements).forEach((r) => {
        translation = translation.replace(`:${r}`, replacements[r]);
    });

    return translation;
}

This works, but it has problems. You have to think about adding these strings to each of your Inertia apps. You're sending every translation string with every page response, even on pages that only use a handful of them. For apps with hundreds or thousands of keys across multiple languages, this bloats your responses significantly.

There's also no type safety. Typo in a key? You'll just see the raw key rendered on screen and probably won't notice until a user reports it.

Approach 2: Use a Framework-Specific i18n Library

For Vue, you could integrate vue-i18n. For React, something like react-i18next. These libraries are powerful — they handle pluralization, interpolation, formatting, and more.

But now you're managing two translation systems. Laravel has its own on the backend, and your frontend library has its own format and conventions. You need a build step or a generator package to convert Laravel's translation files into a format your frontend library understands. Every time you add a string, you might need to run a conversion command, restart a watcher, or remember to update both systems.

It's doable, but it adds friction to your workflow and complexity to your build pipeline.

Approach 3: A Unified Workflow with Lynguist

This is where Lynguist and its open-source packages come in. Full disclosure: I built it, precisely because I was tired of the disjointed workflow described above and overly complicated tools online.

The idea is simple: you write your translation function calls in your code (both in Blade/PHP and in your JavaScript/TypeScript components) and let the tooling handle the rest.

Setting It Up

Install the Laravel package and the frontend helper:

composer require vixen/laravel-lynguist
npm install @vixen-tech/lynguist

Publish the config:

php artisan vendor:publish --provider="Vixen\Lynguist\LynguistServiceProvider"

Scanning Your Codebase

The Laravel package scans your entire codebase — PHP, Blade, JavaScript, Vue, TypeScript, JSX, TSX — for translation function calls (configurable in config/lynguist.php and automatically generates your language JSON files:

php artisan lynguist:scan

This looks for calls like __('welcome-message')trans('greeting')@lang('page-title') and others across all your files. It outputs a clean JSON file per language in your lang/ directory:

{
    "greeting": null,
    "page-title": null,
    "welcome-message": null
}

Keys with null values are untranslated. Fill them in, and the next time you scan, your existing translations are preserved. New keys get added, everything stays sorted alphabetically.

Using Translations in Your Frontend

On the frontend, the @vixen-tech/lynguist package gives you the familiar __() (among others, e.g. trans, transChoice) function that works exactly like Laravel's:

import { __ } from '@vixen-tech/lynguist';

export default function Welcome() {
    return (
        <div>
            <h1>{__('welcome-message')}</h1>
            <p>{__('greeting', { name: 'Alex' })}</p>
        </div>
    );
}

Same function, same keys, everywhere. No separate translation system for the frontend.

TypeScript Support

When the scan runs, it also generates a TypeScript declaration file (configurable path, defaults to resources/js/types/translations.d.ts):

interface LynguistTranslations {
    'greeting': string;
    'page-title': string;
    'welcome-message': string;
}

This means your editor will autocomplete translation keys and flag typos at compile time. No more rendering raw keys because of a misspelling.

Cloud Sync (Optional)

For solo projects, the local workflow might be all you need. But if you're working with a team, or with translators who shouldn't need access to your codebase, lynguist.com provides a translation management UI on top of the same packages.

You connect it with an API token in your .env:

LYNGUIST_API_TOKEN=your-api-token

Then upload your translations:

php artisan lynguist:scan --upload // First scans and extracts all your translations, then uploads them
php artisan lynguist:upload // Uploads already existing translations

From there, translators can work in a side-by-side editor with AI-powered suggestions (via DeepL), glossary enforcement, and translation memory. When they're done, pull the translations back:

php artisan lynguist:download

The free tier includes 500 translation keys, 3 languages, and 10,000 AI translation credits — enough to evaluate whether it fits your workflow before committing.

Comparison

Manual sharing

i18n libraries

Lynguist

Setup complexity

Low

Medium-High

Low

Frontend helper function

Write your own

Library-provided

Included (@vixen-tech/lynguist)

Auto-discover translation keys

No

Depends on library

Yes (lynguist:scan)

Type safety

No

Depends on library

Yes (auto-generated .d.ts)

Preserves existing translations

Manual effort

Depends on setup

Automatic

Team collaboration

Git-based only

Depends on library

Web UI + API sync

AI-powered translations

No

Depends on library

Yes (via DeepL)

Framework support

DIY per framework

One per library

Framework-agnostic

Wrapping Up

There's no single "right" way to handle translations in an Inertia.js app. If your project has a dozen strings and one language, sharing translations through Inertia props with a small helper function is perfectly fine.

But as your app grows — more languages, more keys, more contributors — the manual approach starts to crack. That's the gap Lynguist was built to fill: a single, unified workflow from scanning to translating to deploying, whether you're a solo developer or a team.

The open-source packages (laravel-lynguist and @vixen-tech/lynguist) work independently of the cloud platform, so you can adopt the scanning and frontend helper without any SaaS dependency. And if you do want the collaboration features, the cloud layer is there when you need it.

Give it a try, and let me know what you think.

Managing Translations in Inertia.js • AlexTorscho.com