edulazaro/laratext
Laratext lets Laravel apps define translations with both a key and readable text, keeping language files clear and stable. It auto-translates missing keys via OpenAI or Google Translate, plus offers a @text Blade directive, text() helper, and scan/update commands.
Architecture fit. Laratext is an idiomatic Laravel package: a service provider, a publishable config/texts.php, a global text() helper, a @text Blade directive, an Artisan command (laratext:scan), and a strategy-style translator layer (OpenAITranslator, ClaudeTranslator, GoogleTranslator behind a default_translator config key). That is the shape a Laravel team expects, and the translator interface is the part that matters most architecturally — it gives us a seam to swap in DeepL, a self-hosted model, or an internal MT gateway without touching call sites.
The core design bet is the text('key.name', 'Default text') signature: it keeps the key stable for lookups while keeping the source string readable in code and diffable in review. That is a genuine improvement over Laravel's __() dilemma (keys-only = unreadable files; text-only = brittle on any character change), and it is the reason to consider the package at all.
Everything else is a build-and-integrate exercise: the scan command, the auto-translation loop, and the JSON language-file writer.
Integration feasibility. Low ceremony on paper — composer require, publish the config, set API keys in .env, run laratext:scan --dry to see scope. The stated compatibility matrix (PHP 8.2–8.5 × Laravel 10–13, every combination in CI, plus a weekly scheduled run against new framework releases) is better than most packages of this size and materially reduces the "will it break on the next Laravel minor" risk.
Two integration hazards need to be validated in a spike rather than assumed:
text() is a very generic helper name, and @text a very generic directive name. Any existing app helper, any other package helper, and any future Laravel addition collide here. We need to confirm the package guards with function_exists() and that we are comfortable reserving that name permanently.__(). The README never states whether Laratext registers itself into Laravel's translator such that __('key.name') resolves the same JSON entries, or whether it is a parallel lookup path. If it is parallel, we have two translation systems in one codebase and a migration that touches every view and controller. This is the single most important unknown.Technical risk.
--resync exists precisely because this is expected; that is a tell.laratext:lock escape hatch and a --resync, but no concept of a reviewer, an approval state, or a glossary beyond a free-text context string. For any product with brand terms, legal copy, or regulated content, the review loop is ours to build.text("activity.{$this->type->value}") is never seen by the scanner, is never created, and — per the README's own admission — surfaces the raw key to end users with nothing warning us. This is a production-visible failure class, not a lint nit.--prune deletes keys from language files based on static reference analysis. Combined with the silent-interpolation problem, a prune run can delete a key that is used but is dynamically constructed. Git review is mandatory, not optional.context blob (which the README explicitly encourages you to fill with product description, register, and do-not-translate terms) are sent to OpenAI, Anthropic, or Google. That is a third-party data-processing review, and possibly a DPA, in any organization with compliance obligations.Key questions to answer before committing.
__() and text() read the same JSON files?lang/es.json?retries config implies the latter is configurable, but the failure semantics matter more than the retry count.lang/es/messages.php)?Stack fit. Best fit is a Laravel 10+ application with ad-hoc, inconsistent copy handling — strings inline in Blade, some __() calls, some hardcoded English — and a small number of target locales (say 2–5) where machine translation plus light human review is an acceptable quality bar. Poor fit for anything with an established TMS/vendor relationship, per-locale professional translators, or compliance obligations around copy (medical, financial, legal). Those teams should take the key+text convention as a good idea and skip the auto-translation half.
Migration path. Do not adopt the package and the convention simultaneously across the codebase. Sequence it:
laratext:scan --dry. The dry run is the value here: it inventories new keys, drifted source texts, and orphan keys and costs nothing. That output is our scope estimate and our cost model.__() interop. If it does not exist, plan for a translation-layer shim so existing calls keep resolving while new code uses text().@text directive means view changes are mechanical; the risk is concentrated in dynamic keys, so audit each migrated surface for interpolation before and after.laratext:lock the keys that pass. That gives a reviewed baseline that future scans will not overwrite.Compatibility and sequencing concerns.
.env or in the app server's environment for production. Scans should run from a CI job or a dedicated worker with the key injected, not from developer laptops with a shared key.lang/ layout and any existing files. If we already have lang/es.json, establish whether Laratext merges into it or replaces it, and test on a copy first.gpt-5.4-nano, claude-haiku-4-5). Those names have a shelf life. Treat the model string as a managed config value with an owner and a review date, or a provider deprecation will silently change translation quality across the whole product.composer require edulazaro/laratext:1.x.y) and upgrade deliberately. With one maintainer and near-zero dependents, an upstream regression will not be caught by anyone but us.laratext:scan --dry as a non-blocking report initially, then as a gate once we trust the output. Never let --write run in CI unattended until we have a reviewed, locked baseline and a diff-review step.Maintenance. The package is small and the surface is well-tested, but the maintenance burden is unevenly distributed: the code is low-touch, the outputs are high-touch. Every scan run produces a content diff that someone must review, and that reviewer needs enough language competence to catch a wrong-gender noun or a placeholder that survived translation but moved. Budget for a translation owner, not just a package owner.
The MIT license and the clean translator interface mean the exit cost is low: forking or replacing the scan loop with an in-house Artisan command is a few days of work, not a quarter. That materially de-risks the 0-dependents signal. What we cannot cheaply replace is the accumulated reviewed translation corpus — so keep it in a format we control and back it up like any other data asset.
Support. Community-only. No SLA, no vendor. Issues land with the single maintainer. Internalize that: we are the escalation path, and we should be prepared to patch a fork. The changelog and release history are present and the test matrix is genuinely good, which raises confidence in the happy path but says nothing about our specific provider, model, or file layout.
Scaling. Cost and runtime scale as keys × locales × run frequency, and the multiplier that bites is retranslation, not initial translation:
source_locale is misconfigured relative to config('app.locale'), the README's own example shows the entire project being retranslated in a single --write. That misconfiguration will happen to someone. Set source_locale explicitly on day one, even when it matches the default.--write does not save money. Only --dry avoids provider calls. Anyone assuming "no --write = preview" will burn budget in a loop.retries defaults to 3 and timeouts to 20–60s. On a large project a single scan can run for a long time; run it as a scheduled job with a log, not interactively. Watch for provider 429s at scale — batching behavior is unverified.--resync should be a break-glass operation requiring sign-off, not a routine command. It ignores existing translations and retranslates everything.Failure modes to instrument.
| Failure | Blast radius | Mitigation |
|---|---|---|
| Interpolated key never scanned | Raw key shown to users in one locale | Style rule banning text() with interpolated keys; review checklist; a runtime fallback that logs missing keys |
| Provider outage mid-scan | Partial or truncated language file | Verify atomic writes; always run on a branch; diff before merge |
--prune removes a live dynamic key |
Missing translation in production | Never prune without a full git diff review and a staging smoke test |
| Provider deprecates the pinned model | Silent quality shift across all locales | Scheduled config review; diff a sample of --resync output against locked strings |
| Placeholder or plural form corrupted | Broken UI strings, wrong plural selection | Lock reviewed keys; spot-check :placeholder presence per locale after each write |
| Key leaked / expired | Scan fails, no user-facing impact if scans are decoupled from deploys | Keep the scan job out of the deploy path so translation failures never block a release |
The last row is an architectural decision worth making explicitly: translation is a batch process, not a request-time dependency. If a provider is down, deploys must still ship with the last committed language files.
Ramp-up. Developer ramp is small — text() and @text are self-explanatory, and the key+text convention is learnable in a code review. The organizational ramp is the real work:
nav.*, errors.*, emails.*) so keys double as context for the translator, as the README intends.context string — product description, register, and a do-not-translate list — owned by someone and reviewed when the product changes.--write, and what to do when the provider 429s.Realistically: one engineer-day for the spike and scope inventory, two to three days for the first surface migration plus config and secrets wiring, and then a recurring half-day per locale per release cycle for review. That recurring cost is the honest number to put in front of stakeholders — the package is cheap, the review is not.
How can I help you explore Laravel packages today?