Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Translation Handler Laravel Package

brunoscode/laravel-translation-handler

A Laravel package for handling application translations, offering tools to manage and work with language files in your project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish. The package ships a config file and (for the db format) migrations. Do both immediately, before touching any translation:

composer require brunoscode/laravel-translation-handler

php artisan vendor:publish --provider="BrunosCode\TranslationHandler\TranslationHandlerServiceProvider"
php artisan migrate   # only needed if you plan to use the `db` format

Look here first. Everything flows from config/translation-handler.php. Read it top-to-bottom once and confirm four things against your app:

  • phpPath / jsonPath / csvPath actually point where your translations live (csvPath defaults to storage_path('lang'), not lang_path() — a common surprise).
  • locales lists every locale you care about, or you'll silently export/import only the first one.
  • keyDelimiter is . and csvDelimiter is ; — these are different things and both matter.
  • defaultImportFrom / defaultExportFrom etc. match the direction you expect, so bare import/export commands don't run backwards.

First use case: audit before you migrate. The highest-value first command is not a conversion, it's the checker. It tells you what your codebase actually references versus what exists in your files:

php artisan translation-handler:check php_file --side=backend --show-keys

Run it per side (backend, then frontend if you have a JS app). Fix the reported gaps, then go and do the conversion that motivated the install:

# Peek at what's there without writing anything
php artisan translation-handler:list php_file --locale=en --group=auth
php artisan translation-handler:list-groups php_file --level=1

# Convert PHP arrays -> JSON
php artisan translation-handler:import php_file json_file --force

The one architectural concept to internalise on day one: four formats are interchangeable — php_file, json_file, csv_file, db — referenced via TranslationOptions::PHP, ::JSON, ::CSV, ::DB. They are storage targets, not features. PHP, JSON and CSV each rewrite the entire file on every write; db writes only the touched rows. That single fact should drive how you structure every write path (see below).


Implementation Patterns

The golden rule: write to db, sync to files once at the end. For anything involving more than one key, do the work against db and flush at the end. Fifteen keys against php_file is fifteen full-file rewrites, fifteen diff churn events, and fifteen chances for a half-written file.

use BrunosCode\TranslationHandler\Facades\TranslationHandler;
use BrunosCode\TranslationHandler\Data\TranslationOptions;

// 1. read into a collection
$c = TranslationHandler::get(TranslationOptions::PHP);

// 2. mutate in memory (any number of times, zero I/O)
$c->addTranslation(new Translation('auth.welcome', 'it', 'Benvenuto!'));
$c->replaceTranslation(new Translation('auth.welcome', 'fr', 'Bienvenue !'));

// 3. write once to db, then sync db -> files once
TranslationHandler::set($c, TranslationOptions::DB, force: true);
TranslationHandler::sync(TranslationOptions::DB, TranslationOptions::PHP, force: true);

Read–filter–write. TranslationCollection is where most day-to-day work happens. Filters are immutable and chainable, so a "export one locale to CSV for the translator" task is three lines:

$it = TranslationHandler::get(TranslationOptions::PHP)->whereLocale('it');
TranslationHandler::set($it, TranslationOptions::CSV, force: true);

Useful filters in practice: whereLocale, whereLocaleIn, whereKey, whereKeyIn, whereGroup / whereGroupIn (where 'auth' matches every auth.* key), whereValue, whereValueContains, whereValueIn. The value filters are how you hunt down copy-paste leftovers — whereValue('') or whereValue(null) finds untranslated placeholders without opening a single file.

Merge sources instead of overwriting. addTranslation skips when key+locale already exists; replaceTranslation overwrites. So merging is declarative rather than a manual diff:

$base = TranslationHandler::get(TranslationOptions::PHP);
$base->addTranslations(TranslationHandler::get(TranslationOptions::DB)); // only fills gaps
TranslationHandler::set($base, TranslationOptions::JSON, force: true);

Localise a whole feature in one shot. Group-level operations beat per-key loops. From the CLI/MCP side, set-translation-group-tool / a group write takes a nested map of subkey → locale → value. In PHP, build the collection once and set it once. This is the pattern to give an AI agent, a translator, or a one-off import script.

CI gate. check returns passed and totalMissing, and --show-keys / orphans: true give you the detail. Wire it into CI as a failing step on the PR that adds strings — the package is at its most valuable as a linter, not just a converter:

php artisan translation-handler:check php_file --side=backend --show-keys --orphans

Expect to fix the orphans output periodically (defined-but-unused keys are dead weight and confuse translators), but run the missing-key check on every build.

Wrap it in your own service, not scattered facade calls. Because TranslationOptions is a mutable config snapshot and the facade holds that state, ad-hoc setOption() calls in controllers are the fastest route to a bug where a later call reads the wrong path. Put the package behind one or two app-level classes (e.g. Translations\SyncService) that construct a fresh TranslationOptions, do the work, and return. Your domain code then never imports the facade.

Custom handlers for real-world divergence. All handler classes plus the checker are swappable in config or via setOption():

'phpHandlerClass'  => \App\Translations\MyPhpHandler::class,   // FileHandlerInterface
'dbHandlerClass'   => \App\Translations\MyDbHandler::class,    // DatabaseHandlerInterface
'checkerClass'     => \App\Translations\MyChecker::class,      // extends TranslationChecker

File handlers implement get(?string $path), put(TranslationCollection, ?string $path): int, delete(?string $path): int. Database handlers swap $path for ?string $connection and add delete(?string $connection = null, bool $hardDelete = false) — that $connection parameter is your hook for multi-tenant or per-connection translation stores, and $hardDelete is your soft-delete story.

Custom checker patterns for project helpers. If your codebase uses bespoke helpers (t('…'), $t('…'), an i18n.t('…') on the frontend), the default regexes will under-report and CI will go green on genuinely missing keys. Extend TranslationChecker and override patternsFor(string $side): array, returning ['static' => [...], 'dynamic' => [...]]. Group 1 of each regex must capture the key for static and the prefix for dynamic. Scanned paths/extensions per side still come from the check config entry, so you rarely need to touch those.

Scheduling. A nightly sync php_file db --force (or the reverse direction, depending on which side is authoritative) keeps the DB usable as a queryable translation store — handy for building an internal translation UI without reimplementing file parsing. Just pick one authoritative direction and never run both.


Gotchas and Tips

force is not optional in practice. Nearly every write method takes force: false by default, which means "don't overwrite". A set() that appears to succeed but changes nothing is almost always a missing force: true. Conversely, --fresh wipes the target before writing — that is a destructive flag, not a "clean up whitespace" flag. Commit lang/ to git and treat --fresh as a one-way door.

locale omitted means "all locales". deleteKey($from, 'auth.welcome') removes the key for every locale; deleteKey($from, 'auth.welcome', locale: 'fr') removes one. Same for listTranslations. The default is the broad one — pass locale deliberately.

sortKeys / sort-translations doesn't work on db. Sorting is a PHP/JSON/CSV-only operation. If your authoritative store is the DB, sort on the file target after sync or not at all.

Path defaults diverge by format. phpPath and jsonPath are lang_path(), csvPath is storage_path('lang'). When find returns null and you're certain the key exists, you're probably looking at the wrong path for that format — pass path: explicitly before you start debugging the data. fileNames and csvFileName are separate config values too.

csvDelimiter (;) is unrelated to keyDelimiter (.): one splits columns, one splits group.key. Don't "harmonise" them; Excel-driven CSV workflows depend on ; in many locales. If a CSV import produces one giant group with dots in it, you've changed keyDelimiter.

jsonNested / jsonFormat change the file shape. jsonFormat: true pretty-prints; jsonNested: false means flat keys. Flipping jsonNested on an existing JSON corpus doesn't convert in place — it changes how the file is interpreted. Run import json_file db --force first, flip the flag, then sync db json_file --force, and check the diff before committing.

TranslationOptions is state you can poison. setOption($k, $v) mutates the facade's snapshot for the rest of the request. In long-running workers (Octane, queues) that leaks across jobs. Construct new TranslationOptions([...]) and pass it to setOption() at the top of a job, or keep all package calls inside a scoped service. TranslationOptions::fromConfig()-style defaults are the safe baseline.

addTranslation vs replaceTranslation is a silent-data-loss axis. Bulk-merging with replaceTranslations where you meant addTranslations will clobber hand-edited translations. Default to add*, reach for replace* only when you're seeding from an authoritative source.

DB format needs migrations, and delete semantics differ. Nothing works against db until you've published and run migrations. When wiring a custom DB handler, remember the $hardDelete flag on delete() — if your table uses soft deletes and you don't honour it, "deleted" rows will keep reappearing after the next sync.

MCP tools only appear with laravel/boost v2 installed. The auto-registration is silent — there's no config toggle to check. If an agent reports no translation tools, the problem is the Boost version, not this package. Also note the MCP schemas live in the server; the package docs are the workflow, so tell agents the ordering (read → write to db → sync → re-run check) rather than letting them guess tool-by-tool.

Give agents and scripts the batch tools. set-translation-tool (one key, one locale) in a loop is the failure mode: N full-file rewrites and N chances to corrupt. Point them at group/all-locales writes plus a single sync.

Debug with reads, not writes. find (single key+locale) and list/list-groups are idempotent and cheap. When a write misbehaves, your first three moves are: list-translations on the target format, find on the exact key, then get-translation-config-tool / the config file to confirm the path and locale list being used. Only then touch a write command. list-groups --level=1 --search=… is also the fastest way to orient yourself in an unfamiliar key hierarchy before adding anything.

Zero stars, zero dependents — pin and read. This package has no downstream users to absorb breaking changes, so "brunoscode/laravel-translation-handler": "^x.y" with a committed composer.lock, and read the handler classes you rely on before you build workflows around undocumented behaviour. The interfaces (FileHandlerInterface, DatabaseHandlerInterface) are stable-looking and are your insurance: if the package stalls, a custom handler class you own keeps import/sync/set working against your own storage.

Highest-ROI extension point: the checker. Most teams adopt this package for the format conversion and stay for check in CI. Customising patternsFor() to cover your own helper functions (and your frontend t() calls) turns it from "reports some missing keys" into a genuine release gate — which is where the package earns its place in a daily workflow.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package