Skip to content

Internationalization

Lattice apps are translated with Laravel’s normal translation system. Because you describe your UI in PHP, almost everything a user reads — page titles and headings, field labels and helper text, table headers, action labels, validation messages — is produced on the server, so you translate it exactly like any Laravel app: lang/ files and __() / trans(). There is no Lattice-specific layer to learn for it.

A smaller set of strings renders in the browser — Lattice’s built-in chrome (the rich-editor toolbar, table pagination, filter controls, the bulk bar, and the accessibility labels on menus, toasts, tabs, and selects) and any custom React components you write. Lattice delivers the same translations to the client through an i18next backend, so you keep one set of files in lang/ and never duplicate strings in JavaScript.

Start in the backend; reach for the frontend bridge only for what actually renders in the browser.

Your definitions are PHP, so wrap the text you author in Laravel’s translation helpers, exactly as you would anywhere else:

use Lattice\Lattice\Core\Components\Heading;
use Lattice\Lattice\Forms\Components\TextInput;
Heading::make(__('products.title'));
TextInput::make('name', __('products.fields.name'))
->helperText(__('products.fields.name_hint'))
->rules(['required']); // validation messages come from Laravel's validation translations

These render on the server and ship already-translated in the Inertia payload — nothing here touches i18next. The active language is the app locale (App::setLocale()), the same as any Laravel request.

Frontend: the same translations, over i18next

Section titled “Frontend: the same translations, over i18next”

The strings that live in the browser read through i18next, with the English baked into each call site as a fallback — so the UI is fully readable with zero configuration. To replace that English with your translations, you point Lattice’s i18next instance at a backend that serves your lang/ files (below). The instance is isolated from your app’s own i18next setup (if any), so wiring it up never touches your application’s translations.

Every built-in string lives in one i18next namespace, lattice, scoped into a few domain groups — form.* (fields, the rich editor, file uploads), table.* (columns, pagination, filters, bulk selection, row actions), and common.* (shared chrome: tabs, toasts, tooltips, menus). Keys read like form.editor.bold or table.pagination.next. One namespace keeps Lattice’s keys from colliding with your app’s, and means a single route translates everything Lattice renders.

There is nothing to set up to render in English. Each call site supplies its own default (t("form.editor.bold", "Bold")), and the instance initializes itself the first time a component reads a string. Apps that never opt into a backend never bundle one.

To serve translations, install bambamboole/laravel-i18next, which exposes the routes the frontend reads from:

Terminal window
composer require bambamboole/laravel-i18next

Its routes register automatically. Enable them, and serve namespaces as nested i18next JSON:

config/i18next.php
return [
'routes' => ['enabled' => true],
'namespaces' => true,
'output' => 'nested',
// …see the laravel-i18next docs for the full config.
];

Lattice registers its own lang/ directory under the lattice namespace, so its bundled English and German translations are served at /locales/{lng}/lattice.json automatically — there is nothing to copy or publish to get them.

Lattice reads whether the routes are serving (i18next.routes.enabled), whether missing keys are reported back (i18next.save_missing.enabled), the supported locales from config('lattice.i18n.locales'), and which locales to eagerly preload from config('lattice.i18n.preload_locales'). It shares those values to the frontend as an Inertia once prop.

Locales listed in preload_locales have their translations fetched once at startup (in the background, so the first paint isn’t blocked). Switching to a preloaded locale then resolves from the store instead of an HTTP round-trip, which avoids a flash of the fallback language. Leave it empty to load each locale lazily on first switch.

Lattice shares an i18n block under lattice.i18n. Configure the frontend from the initial Inertia page props before the first render, and add the locale header to every Inertia visit:

resources/js/app.tsx
import { createInertiaApp } from "@inertiajs/react";
import { Provider, withVisitHeaders } from "@lattice-php/lattice";
import { configureI18nFromPageProps, LocaleReload } from "@lattice-php/lattice/i18n";
import { createRoot } from "react-dom/client";
createInertiaApp({
// …resolve, layout…
defaults: {
visitOptions: withVisitHeaders,
},
setup({ el, App, props }) {
if (!el) {
return;
}
const root = createRoot(el);
const render = () => {
root.render(
<Provider>
<App {...props} />
<LocaleReload />
</Provider>,
);
};
void configureI18nFromPageProps(props.initialPage.props, {
namespaces: ["lattice", "app"],
}).then(render, render);
},
});

When the backend reports i18n as enabled, configureI18nFromPageProps attaches i18next’s HTTP backend and loads translations from laravel-i18next’s routes, overriding the inline English. When it’s disabled — or the prop is absent — the inline English stands. withVisitHeaders sends Accept-Language on Inertia page visits; Lattice’s own form, action, table, and fragment requests send it internally. LocaleReload reloads the current page after setLocale() dispatches the lattice:locale-change event, so server-rendered labels are refreshed in the new locale.

Lattice ships English and German and registers them automatically. To override a string or add a new locale, publish the bundled files into your app and edit them:

Terminal window
php artisan vendor:publish --tag=lattice-translations

This copies Lattice’s groups to lang/vendor/lattice/{locale}/, where Laravel resolves them ahead of the package’s own. Each domain is its own file — form.php, table.php, common.php — with the nested keys Lattice uses:

lang/vendor/lattice/de/form.php
return [
'editor' => [
'bold' => 'Fett',
'heading-1' => 'Überschrift 1',
],
// …
];
// lang/vendor/lattice/de/table.php
return [
'pagination' => [
'next' => 'Weiter',
'showing' => 'Zeige :from-:to von :total',
],
// …
];

Interpolated values use Laravel’s :placeholder syntax; laravel-i18next serves them as i18next {{placeholder}} tokens.

The same instance is available to your custom fields and columns. Inside a component, use the useT hook with your own namespace:

import { useT } from "@lattice-php/lattice/i18n";
function SaveButton() {
const { t, locale, locales, setLocale } = useT("app"); // served from lang/{locale}/app.php
return <button>{t("save", "Save")}</button>;
}

Passing "lattice" reads Lattice’s namespace instead — the built-in chrome does exactly that. Outside of a component — building a label in a helper, say — use translate:

import { translate } from "@lattice-php/lattice/i18n";
translate("app", "save", "Save");

Both take the English fallback inline, so they are safe to call before any translations have loaded.

The frontend picks its initial language from localStorage.locale, the locale cookie, the <html lang="…"> attribute, then en. To switch at runtime, call setLocale():

import { setLocale } from "@lattice-php/lattice/i18n";
setLocale("de");

This writes localStorage.locale, writes the locale cookie, updates <html lang>, changes the i18next language, and dispatches lattice:locale-change. The backend locale middleware reads the cookie first, then session locale, then the request’s Accept-Language header.

For server-driven screens, make locale choices normal actions that return a localeChange effect:

use Illuminate\Http\Request;
use Lattice\Lattice\Actions\ActionDefinition;
use Lattice\Lattice\Actions\ActionResult;
use Lattice\Lattice\Actions\Components\Action;
use Lattice\Lattice\Attributes\AsAction;
#[AsAction('app.locale.set')]
class SetLocaleAction extends ActionDefinition
{
public function definition(Action $action): Action
{
return $action->label(__('language.switch'));
}
public function handle(Request $request): ActionResult
{
$locale = $this->context($request, 'locale');
$locales = config('lattice.i18n.locales', []);
return is_string($locale) && is_array($locales) && in_array($locale, $locales, true)
? ActionResult::success()->localeChange($locale)
: ActionResult::failure();
}
}

Then compose the choices wherever your layout accepts components:

use Lattice\Lattice\Actions\Components\Action;
use Lattice\Lattice\Actions\Components\ActionGroup;
use Lattice\Lattice\Core\Components\FloatingPanel;
use Lattice\Lattice\Core\Enums\ButtonVariant;
use Lattice\Lattice\Core\Enums\FloatingPlacement;
use Lattice\Lattice\Core\Enums\Orientation;
$locale = app()->getLocale();
FloatingPanel::make('locale-switcher-panel')
->label(__('language.label'))
->placement(FloatingPlacement::TopEnd)
->schema([
ActionGroup::make('locale-switcher')
->label(__('language.label'))
->inline(Orientation::Horizontal)
->actions([
Action::use(SetLocaleAction::class)
->key('locale-en')
->label(__('language.en'))
->variant($locale === 'en' ? ButtonVariant::Secondary : ButtonVariant::Ghost)
->context(['locale' => 'en']),
Action::use(SetLocaleAction::class)
->key('locale-de')
->label(__('language.de'))
->variant($locale === 'de' ? ButtonVariant::Secondary : ButtonVariant::Ghost)
->context(['locale' => 'de']),
]),
]);

For custom React surfaces, Lattice provides headless switcher helpers so your app controls the markup and styling:

import { LocaleSwitcher } from "@lattice-php/lattice/i18n";
function LanguageSwitcher() {
return (
<LocaleSwitcher namespace="app">
{({ options, setLocale }) => (
<div>
{options.map((option) => (
<button
key={option.value}
type="button"
aria-pressed={option.active}
onClick={() => setLocale(option.value)}
>
{option.label}
</button>
))}
</div>
)}
</LocaleSwitcher>
);
}

The labels resolve from language.{locale} in the given namespace, falling back to the locale code. Use useLocaleOptions() directly when a hook fits your component better than a render prop.

configureI18nFromPageProps(props, options?)

Section titled “configureI18nFromPageProps(props, options?)”

Reads the shared lattice.i18n once prop from Inertia page props and calls configureI18n. Pass the namespaces your React components need:

void configureI18nFromPageProps(props.initialPage.props, {
namespaces: ["lattice", "app"],
});

Use as createInertiaApp({ defaults: { visitOptions: withVisitHeaders } }). It preserves the visit options and adds the active Accept-Language header.

Listens for lattice:locale-change and visits the current URL with { preserveScroll: true, preserveState: true }. Preserving state means the visit only swaps in the re-localized props without remounting the page, so table sort/filter, form input, scroll, and focus survive the switch. Override either option if your app needs a different reload behavior.

Returns { locale, locales, options, setLocale }, where options is a list of { value, label, active } objects built from the backend-supported locales.

configureI18n calls this for you, but you can call it directly for full control over the backend. It registers i18next’s HTTP backend and is the opt-in import.

OptionDefaultPurpose
loadPath/locales/{{lng}}/{{ns}}.jsonRoute translations are fetched from.
addPath/locales/add/{{lng}}/{{ns}}Route missing keys are reported to.
saveMissingfalseReport keys with no translation back to the backend, which persists them.
customHeadersReturns extra request headers, e.g. a CSRF token for the saveMissing POST.

The defaults match laravel-i18next’s namespaced routes; override them only behind a custom route prefix.

The block Lattice shares as an Inertia once prop:

type I18nConfig = {
enabled: boolean; // are the translation routes serving?
saveMissing: boolean; // should missing keys be reported back?
locales: string[]; // supported locale codes from config('lattice.i18n.locales')
preloadLocales: string[]; // locales eagerly loaded at startup, from config('lattice.i18n.preload_locales')
timezone: string | null; // the app/user timezone, when resolved
};

configureI18n(config) enables the backend only when enabled is true, forwards saveMissing, stores locales for useT, useLocaleOptions, and LocaleSwitcher, and preloads preloadLocales in the background. The load and add paths are fixed on the frontend, so they never travel in this prop.