Appearance
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.