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.
Backend: translate in PHP
Section titled “Backend: translate in PHP”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 translationsThese 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.
The lattice namespace
Section titled “The lattice namespace”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.
Zero config
Section titled “Zero config”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.
Translating the built-in strings
Section titled “Translating the built-in strings”To serve translations, install bambamboole/laravel-i18next,
which exposes the routes the frontend reads from:
composer require bambamboole/laravel-i18nextIts routes register automatically. Enable them, and serve namespaces as nested i18next JSON:
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.
Wire the frontend
Section titled “Wire the frontend”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:
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.
Customize or add a locale
Section titled “Customize or add a 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:
php artisan vendor:publish --tag=lattice-translationsThis 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:
return [ 'editor' => [ 'bold' => 'Fett', 'heading-1' => 'Überschrift 1', ], // …];
// lang/vendor/lattice/de/table.phpreturn [ 'pagination' => [ 'next' => 'Weiter', 'showing' => 'Zeige :from-:to von :total', ], // …];Interpolated values use Laravel’s :placeholder syntax; laravel-i18next serves them as i18next
{{placeholder}} tokens.
Using translations in your own components
Section titled “Using translations in your own components”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.
Language detection
Section titled “Language detection”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.
Language switchers
Section titled “Language switchers”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.
Reference
Section titled “Reference”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"],});withVisitHeaders(href, options)
Section titled “withVisitHeaders(href, options)”Use as createInertiaApp({ defaults: { visitOptions: withVisitHeaders } }). It preserves the visit
options and adds the active Accept-Language header.
LocaleReload
Section titled “LocaleReload”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.
useLocaleOptions(options?)
Section titled “useLocaleOptions(options?)”Returns { locale, locales, options, setLocale }, where options is a list of { value, label, active } objects built from the backend-supported locales.
enableBackend(options?)
Section titled “enableBackend(options?)”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.
| Option | Default | Purpose |
|---|---|---|
loadPath | /locales/{{lng}}/{{ns}}.json | Route translations are fetched from. |
addPath | /locales/add/{{lng}}/{{ns}} | Route missing keys are reported to. |
saveMissing | false | Report keys with no translation back to the backend, which persists them. |
customHeaders | — | Returns 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 shared i18n prop
Section titled “The shared i18n prop”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.