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.
What it is. A thin Laravel service provider + facade over PHP's native ext-gettext. It binds .mo catalogs per domain, sets the process locale, and exposes gettext lookups (gettext, ngettext, dgettext) plus Artisan helpers for scanning/extracting strings. The translation authoring format is GNU .po, edited in PoEdit and compiled to binary .mo. It is not a translation management system — it is a runtime adapter plus a file-format convention.
Architecture fit. Fit is strong only if the organization has already decided that .po/gettext is the source of truth. The decisive architectural difference from Laravel's native translator is msgid-based lookup: the source string (usually English) is the key. That buys translator-friendly ergonomics — plural rules per locale, msgctxt disambiguation, fuzzy matching, translation memory, and compatibility with every CAT tool and TMS on the market — at the cost of coupling code to copy. Editing an English string orphans every existing translation of it. For a product with a professional localization vendor or a large non-English user base, that trade is usually worth it; for a small app with developer-translated strings, it is not.
Integration feasibility. Mechanically easy: composer require, publish config, add locale directories, done in an afternoon. The work is not in the package — it is in the surrounding infrastructure: ext-gettext present in every image, system locales generated (locale -a must show fr_FR.UTF-8, not just fr_FR), a msgfmt compile step in CI, and a mapping layer between Laravel's pt-BR codes and gettext's pt_BR.UTF-8 codes. That last item is a guaranteed source of production bugs and needs an explicit config table, not string munging.
Technical risk.
setlocale/putenv/textdomain are per-process, not per-request. Safe under classic PHP-FPM (one request per process at a time); unsafe under Octane, Swoole, or RoadRunner, where a long-lived worker serving a French request can leak locale into a German one. If the roadmap includes Octane, this is a design constraint, not a bug to patch later.setlocale(LC_ALL, …) also changes strcoll, strftime, and printf decimal separators. Set LC_MESSAGES only, and audit any code doing locale-sensitive formatting.msgmerge + fuzzy-review workflow in PoEdit, not ad-hoc editing.Key questions (gating).
composer.json constraint, and when was the last release?.po is only an advantage if the answer is the latter..po files are portable, the wrapper is ~a day of work to replace.)Stack fit. Clean fit for a server-rendered Laravel monolith on FPM with a Docker-based build. Poor fit for API-only services with no user-facing copy, and a poor fit for teams standardized on Laravel's JSON translation files with no vendor relationship.
Migration path. Do not big-bang. Three viable options, in order of preference:
lang/*.php into .pot using the current keys as msgids. Mechanical and safe, but produces permanently unreadable catalogs — you lose the main reason to adopt gettext.Compatibility work. (1) Locale-code mapping config, Laravel ↔ gettext. (2) A bootstrap step that sets LC_MESSAGES explicitly and verifies the locale resolved, throwing or alerting rather than silently falling back. (3) Decide Octane posture now. (4) Confirm ext-gettext and msgfmt in the base image and in CI, not just on developer laptops.
Sequencing.
.mo as a build artifact; health check; locale mapping..pot extraction command wired into CI to catch drift (msgids in code with no catalog entry), and a written rule for editing source strings.Maintenance. .po files are line-oriented text and conflict badly when multiple developers touch the same catalog in one merge window. Mitigate with a single owner per locale, or a TMS that stores translations out-of-band and exports .po in CI. Budget real ongoing effort for msgmerge hygiene; this is the recurring tax that offsets gettext's translator benefits. Commit .po, never commit .mo — compile as a build artifact so binary diffs and stale catalogs can't reach production.
Support. Two distinct support audiences. Developers need to internalize msgid semantics and the "don't edit English strings casually" rule. Translators need PoEdit (or TMS) competence, which they usually already have — that is the point of the package. First-line support for "text not translating" should be a runbook entry pointing at locale availability and .mo freshness, because those two causes cover most incidents.
Scaling. Runtime scaling is genuinely good: gettext loads catalogs into process memory and does hash lookups, so it is faster than parsing large PHP arrays and flat JSON files, and it scales cleanly to dozens of locales. The 6.38 opportunity score reflects a real ceiling, though — adoption is bounded by how many teams are willing to run gettext plumbing in 2025 versus a TMS with an API. The ceiling is organizational, not technical.
Failure modes and blast radius.
| Failure | Symptom | Blast radius | Detection |
|---|---|---|---|
| Locale absent on host | English rendered silently | Whole locale | Needs explicit startup check — otherwise invisible |
| Locale leak under Octane | Wrong-language responses, intermittent | Per-worker, hard to reproduce | Only visible in load/concurrency testing |
.mo not redeployed |
Old copy persists after release | Whole locale | Add catalog checksum to deploy verification |
| Source string edited | Translations drop to fuzzy/English | Single string × all locales | CI msgid drift check |
| Charset mismatch | Mojibake | Whole locale | Enforce UTF-8 end to end |
Every one of these fails silently by default. The single highest-value operational investment is converting silent failures into loud ones.
Ramp-up. Engineering: ~1 sprint to competent, including CI and Docker work. Translators: negligible if they already use CAT tools, which is the package's core selling point. Overall time-to-first-translated-string in production: roughly 3–4 weeks for a small team, most of it infrastructure rather than Laravel code. If that budget is unavailable, Laravel's native JSON translations with English source strings deliver ~70% of the ergonomic benefit at ~10% of the setup cost — worth pricing explicitly before committing to this package.
How can I help you explore Laravel packages today?