Skip to content

Translate your features

The kit's own strings are done. This is how the ones you write join them.

The rule

Every string a person reads goes through a translation file, and the key is written in English. That holds whether you have a second language today or not: the cost is one indirection now, and the alternative is finding every hardcoded sentence in an application you have already shipped.

In PHP

Your application's strings live in lang/{locale}/app.php, a plain group with no namespace:

php
// lang/en/app.php
return [
    'projects' => [
        'title' => 'Projects',
        'archived' => ':name has been archived.',
    ],
];
php
return back()->with('status', __('app.projects.archived', ['name' => $project->name]));

In React

The same keys, resolved through a hook:

tsx
import { useTranslations } from '@/hooks/use-translations';

export default function Projects({ project }) {
    const { t } = useTranslations();

    return (
        <>
            <h1>{t('app.projects.title')}</h1>
            <p>{t('app.projects.archived', { name: project.name })}</p>
        </>
    );
}

:name, :Name and :NAME substitute exactly as they do in PHP: as given, capitalised, and upper-cased. A key with no translation renders as itself, which is the useful failure: a screen showing app.projects.title is obviously missing a string, where a blank heading looks like a styling bug and gets filed as one.

useTranslations() also hands back locale, which is what to pass to Intl.NumberFormat and toLocaleDateString so a date on a Spanish page is not formatted for the reader's browser instead of the page they are reading.

Getting a group to the browser

App\I18n\LaraspringTranslations names the groups the frontend receives:

php
// app/I18n/LaraspringTranslations.php
$registry->exportMany([
    'laraspring-auth::auth',
    'laraspring-organizations::organizations',
    'laraspring-billing::billing',
    'laraspring-i18n::i18n',
    'app',
]);

A namespaced group arrives under its short name, so laraspring-auth::auth.login.title in PHP is auth.login.title in a component. The part after the namespace is identical on both sides, which is what lets one translator finish an email and a screen from the same file.

Only export what a screen draws

Everything named here is public, readable by anyone who opens the page in any language, and shipped on every request. Server-side exception wording is neither needed nor wanted in the browser: that is why laraspring-storage and laraspring-mail are absent from the list above, and why a group of internal messages should stay absent too.

Adding a settings screen

The declarative nav takes a translation key, never a title:

ts
// resources/js/layouts/settings/nav.ts
{ titleKey: 'app.settings.nav.projects', routeName: 'projects.settings' },

The route name is the only thing that is true in every build, which is why an entry whose route is not registered disappears instead of leading to a 404. The title changes with the language, so it is looked up when the list is drawn.

In a package of your own

A package ships its own translations under its own namespace and gains no dependency for it:

php
// YourServiceProvider::boot()
$this->loadTranslationsFrom(__DIR__.'/../lang', 'your-package');

$this->publishes([
    __DIR__.'/../lang' => $this->app->langPath('vendor/your-package'),
], 'your-package-lang');

loadTranslationsFrom is framework rather than laraspring/i18n, so nothing about this makes your package depend on it: an installation without laraspring/i18n renders your strings in app.locale and never learns there were other languages.

Then let the edition decide whether the frontend needs them, by adding 'your-package::messages' to LaraspringTranslations. That indirection is the same one laraspring/mail and laraspring/storage use for their registries, and it is what keeps laraspring/i18n a leaf: a package that named it would put it in the composer.json of everything with a screen, and removing it would break them rather than merely un-translate them.

Email

Nothing extra to do. Build a plain MailMessage with __() calls in it, send it to a user, and Laravel renders it in that user's language because User implements HasLocalePreference.

The one case that needs a decision is an email to somebody who has no account, where there is no preference to read. Name the language explicitly:

php
Notification::route('mail', $email)->notify(
    $notification->locale(App::make(LocaleResolver::class)->current())
);

laraspring/organizations does exactly this for invitations, and the reasoning is in the overview.

Laraspring is a commercial starter kit. Buying it gets you the source.