zerospam/laravel-gettext
Gettext-based localization for Laravel: load PO/MO translation catalogs, translate strings with plural forms and contexts, switch locales at runtime, and use translations in Blade views. A practical alternative to Laravel’s default PHP/JSON language files.
Install the package and publish its config:
composer require zerospam/laravel-gettext
php artisan vendor:publish --provider="Xavrsl\LaravelGettext\LaravelGettextServiceProvider" --tag=config
Open config/gettext.php first. The settings that matter day-to-day are:
'locale' => 'en_US',
'fallback-locale' => 'en_US',
'locale-dir' => storage_path('gettext'),
'domain' => 'messages',
'source-paths' => [base_path('app'), base_path('resources/views')],
'encoding' => 'UTF-8',
Make sure the runtime has the PHP gettext extension enabled, and the build/dev environment has GNU gettext binaries (xgettext, msgmerge, msgfmt). The binaries are only needed for extraction/compilation; production only needs the PHP extension and the compiled .mo files.
A first translation loop:
Mark one string in a Blade view:
<h1>@gettext('Welcome to the dashboard')</h1>
In PHP use the package’s gettext helpers (_('...') / gettext('...')) rather than Laravel’s __() unless your version wires __() to gettext.
Generate/update the catalog:
php artisan gettext:update
Open storage/gettext/fr_FR/LC_MESSAGES/messages.po in PoEdit, translate, save (PoEdit compiles the .mo), or run:
php artisan gettext:compile
Set the locale via the package middleware or directly:
LaravelGettext::setLocale('fr_FR');
Where to look when something is wrong: the .po/.mo path under locale-dir, the domain in config, and whether setlocale() actually succeeded on the host.
Locale negotiation. Most applications use a URL prefix plus the package middleware. Register the middleware alias and group routes:
Route::prefix('{locale}')
->middleware('gettext')
->group(function () {
Route::get('/', [HomeController::class, 'index']);
});
The middleware reads the locale segment (or session) and calls LaravelGettext::setLocale(). Always validate the incoming locale before setting it:
if (LaravelGettext::isLocaleAvailable($locale)) {
LaravelGettext::setLocale($locale);
}
A language switcher is usually a small POST route that stores the chosen locale in the session and redirects back.
String marking. Use one convention consistently:
@gettext('Save changes')_('Save changes') or gettext('Save changes')ngettext()) rather than Laravel’s trans_choice(), unless your version explicitly bridges them.Add resources/views and any package/module source directories to source-paths; otherwise new strings are invisible to gettext:update.
Daily translation workflow. The loop is: add/change strings → php artisan gettext:update → translate in PoEdit → php artisan gettext:compile → verify in the browser. Treat .po files as source and commit them. Decide explicitly whether to commit .mo files: committing them makes deploys simpler and avoids requiring msgfmt on production, but creates noisy binary diffs.
Deployment. If you compile on deploy, install gettext in the image and run php artisan gettext:compile after code is in place. If you commit .mo, ensure the deploy pipeline does not skip them or overwrite them with stale artifacts. For Docker, install both gettext and locales, generate the locales you support, and set a UTF-8 LANG.
Coexistence with Laravel translations. Keep Laravel’s lang/ files for framework strings (validation, auth, pagination) and gettext for application content, or use gettext everywhere consistently. Mixing __() and @gettext() for the same key creates two translation stores and confusing “missing translation” reports.
Testing. Set the locale explicitly in the test setup:
LaravelGettext::setLocale('fr_FR');
$this->assertSame('Bonjour', _('Hello'));
Also assert that middleware falls back safely for unsupported locales. Keep a small smoke test that verifies a compiled .mo exists for every shipped locale.
Extension points. Use a custom middleware or service if locale needs to come from a tenant, cookie, or user profile. Publish and override the config to move locale-dir into the repo (for example resources/lang/gettext) if you want translators working directly in version control.
setlocale(LC_ALL, 'fr_FR.UTF-8') returns false if the locale is not installed, and gettext silently returns the original string. Check locale -a | grep fr_FR on the server and generate locales in the Docker image (locale-gen fr_FR.UTF-8).en_US, en_US.UTF-8, and en_US.utf8 are not interchangeable across systems. Normalize the config to the exact name reported by locale -a..mo. PoEdit compiles on save; the artisan workflow does not. Compare .po and .mo mtimes and run gettext:compile after every catalog edit.putenv, setlocale, textdomain) and test under the same runtime you deploy.xgettext only extracts literal strings. Dynamic calls like _($label) are skipped; add an explicit literal or a translator comment. Verify source-paths includes every directory that contains translatable strings.gettext:update writes under locale-dir. In container builds this may be read-only or owned by another user. Compile during build or ensure the directory is writable.config/gettext.php and the .po header. A BOM or Windows-1252 editor setting produces mojibake in the UI even though the catalog compiles.Plural-Forms header in each .po must match the language. Copying a .po template between languages and translating only the strings often leaves wrong plural rules.setLocale() without checking isLocaleAvailable(). Otherwise you can create traversal-like paths or trigger errors.config/gettext.php, run php artisan config:clear (and config:cache again in production) before debugging further.bindtextdomain() points at the expected directory, textdomain() returns the configured domain, setlocale() returns a non-false value, and msgunfmt messages.mo contains the expected entry. A one-line tinker call to gettext('...') isolates runtime issues from Blade issues..mo files so local runtime does not need msgfmt.composer.json, verify compatibility with your Laravel/PHP version, and keep a fallback plan (Laravel’s native lang/ or a maintained gettext wrapper) if you depend on long-term support.How can I help you explore Laravel packages today?