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

Translation Log Bundle Laravel Package

th/translation-log-bundle

Logs every translation lookup in your Symfony app so you can spot missing keys, unused messages and other gaps in your translation files. Browse and filter the collected data from a simple backend interface to keep your locales clean and complete.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to know, before any install step: th/translation-log-bundle is a Symfony bundle. There is no composer require path into a Laravel app, no service provider, no package:discover entry. The repository is a Symfony 2-era artifact (the *Bundle suffix, the DI extension + compiler-pass layout) and its constraints will not resolve against PHP 8.1+ / Laravel 10–11.

So treat the repo as a specification, not a dependency. Read it in this order — it is small, and the three files map 1:1 onto what you will write:

  1. The event subscriber that listens for translator lookups and misses. This is the whole idea.
  2. The storage/log writer — the pluggable sink.
  3. The DI extension / bundle config — becomes your config/translation-audit.php.

Then build the Laravel equivalent, which is roughly 80 lines plus tests. The minimum viable version is a decorator around the translator binding:

// app/Translation/LoggingTranslator.php
namespace App\Translation;

use Illuminate\Contracts\Translation\Translator as TranslatorContract;
use Illuminate\Support\Facades\{Cache, Log};

class LoggingTranslator implements TranslatorContract
{
    public function __construct(private TranslatorContract $inner) {}

    public function get($key, array $replace = [], $locale = null)
    {
        $locale ??= $this->inner->getLocale();
        $value = $this->inner->get($key, $replace, $locale);

        if ($this->looksMissing($key, $value)) {
            $this->record($key, $replace, $locale);
        }

        return $value;
    }

    public function choice($key, $number, array $replace = [], $locale = null)
    {
        return $this->inner->choice($key, $number, $replace, $locale ?? $this->inner->getLocale());
    }

    public function getLocale() { return $this->inner->getLocale(); }
    public function setLocale($locale) { $this->inner->setLocale($locale); }

    // Loader, has(), hasForLocale(), etc. stay reachable
    public function __call($method, $args) { return $this->inner->$method(...$args); }

    private function looksMissing(string $key, $value): bool
    {
        return is_string($value) && $value === $key && ! str_contains($key, '|');
    }
}

Bind it in a service provider:

public function register(): void
{
    $this->mergeConfigFrom(__DIR__.'/../config/translation-audit.php', 'translation-audit');

    $this->app->extend('translator', fn ($translator) => new LoggingTranslator($translator));
}

Add a dedicated log channel, then hit any page in a non-primary locale and tail storage/logs/translations.log. That is your first use case, and it is worth doing on one service, one locale, staging only, for a week — compare the misses against whatever manual QA list you have to confirm the signal is real before scoping anything larger.

Before you build any of it, spend ten minutes on feature detection: method_exists(app('translator'), 'handleMissingKeysUsing'). On newer Laravel minors the concrete translator exposes a missing-key callback, which is a far smaller hook than a decorator. Verify against your version's source rather than a blog post; the event/callback surface has moved between minors, and the decorator is the version-safe choice when in doubt.

Implementation Patterns

Weekly triage digest → vendor backlog. The highest-value routine is a scheduled command that parses the log and ranks misses by (key, locale) frequency, then posts a top-N table to Slack or opens a ticket. This turns "translate everything" into a ranked queue. Ship the parser alongside the decorator or nobody will ever build it.

Unused-key pruning. The package logs every lookup, not just misses, and so should you (behind a separate, heavily-sampled flag). Run it for a week in production, then diff the logged key set against the keys statically extracted from lang/{locale}/*.php and lang/{locale}.json. Keys with zero hits across a full traffic cycle are deletion candidates. This is the concrete answer to "which strings should we keep?" and it is the same question the bundle was built for.

Release gating. A query, not a tool: zero observed misses in the top 3 locales over the last 7 days is a defensible pre-launch gate for a new market and a regression alarm for an existing one. Wire it as a step in the release checklist, and attribute each miss with release / APP_VERSION so you can tell a new regression from a long-standing gap.

Record shape. One structured line per miss, JSON formatted:

Log::channel(config('translation-audit.channel'))->warning('translation.miss', [
    'key'     => $key,
    'locale'  => $locale,
    'params'  => array_keys($replace),          // names only — never values
    'route'   => optional(request()->route())->getName(),
    'release' => config('app.release'),
]);

Deduplicate and sample from day one. Guard the write with Cache::add("trmiss:{$locale}:".md5($key), 1, now()->addMinutes(10)) — first occurrence per window wins, so a loop rendering the same key 500 times costs one line. On top of that, a global sample rate (random_int(1, 100) <= $rate) for high-traffic services.

Async sink. Route to a dedicated channel with a JsonFormatter and, if the volume justifies it, push to a queue/Redis stream rather than a synchronous file handler. If the sink backs up, drop misses — never add latency to a page render.

Config surface that you will actually need: enabled, channel, sample, ignore_prefixes (seed with validation., pagination., passwords., auth. — these flood the log with framework keys nobody on your team owns), max_key_length.

Complement, don't replace. Runtime logging is the only thing that catches dynamic keys; a build-time key diff in CI catches missing keys before deploy and is cheaper. Both, not either.

Gotchas and Tips

  • It will not install, and the spike will burn a sprint if nobody says so up front. Run the failed composer require once in a scratch container so the decision is documented, then move on.
  • Fail open, always. The decorator sits on the hot path of every __(), @lang, validator message, and Lang::get(). Wrap the entire record path — including the Cache call and the Log call — in try { … } catch (\Throwable) {}. An observability helper must never be able to 500 a page.
  • JSON translation groups produce false positives. With lang/en.json, the key is the English string, so returning the key is the intended fallback, not a miss. Gate your heuristic: only treat $value === $key as missing for PHP-group style keys (group.item, package::group.item), and skip detection entirely when the key contains spaces or matches a configured JSON locale. Otherwise your log is 90% noise and gets disabled within a month.
  • extend('translator', …) must run before the translator is resolved. The provider must be registered after TranslationServiceProvider (it is in the default provider list, so a normal config/app.php / bootstrap/providers.php entry is fine), and you should verify end-to-end with a real route — Lang facade, __(), Blade @lang, and validator messages all resolve through the translator binding, but a package that type-hints Illuminate\Translation\Translator in a constructor and gets resolved first may hold the undecorated instance.
  • Don't reimplement pluralization. Forward choice(), and forward everything else via __call so getLoader(), has(), and hasForLocale() still work — dropping getLoader() breaks packages that introspect namespaces.
  • Fallback-locale hits are misses too, and are invisible to the $value === $key check. If config('app.fallback_locale') is en and the locale is fr, a French user silently sees English. Use hasForLocale() (feature-detect it) to catch that class; it is the more damaging defect and the one users never report.
  • PII is the one failure mode with regulatory consequences. Replacement parameters routinely contain names, emails, and addresses. Log array_keys($replace), or hash the values — never the raw array. This should be a hard requirement, not a config option.
  • Cardinality, not compute, is the risk. Keys built from IDs, user content, or interpolated strings generate an unbounded unique-key stream. Cap key length (200 chars, truncate), honour the ignore-prefix list, and set an explicit log budget per service with an alert — otherwise the diagnostic becomes the incident.
  • Log format matters more than you think. Without 'formatter' => Monolog\Formatter\JsonFormatter::class on the channel, your "structured" log is a text blob and every query becomes a regex. With JSON, triage is jq -r 'select(.message=="translation.miss") | .context.key' | sort | uniq -c | sort -rn | head -50.
  • Name an owner before rollout. The realistic failure is not a crash — it's that the logger works, floods the channel, nobody triages it, and the team turns it off in a month. Pair the production rollout with a recurring triage slot and a short written runbook, or keep it staging-only.
  • Extension points worth keeping open: swap the sink behind an interface (log / cache / DB table / Redis) so the decorator never changes; add route and release attribution via middleware rather than in the translator; keep enabled togglable by env so a burst of noise is one deploy away from silence.
  • Licensing hygiene: if you lift logic from the bundle (the de-dupe window, for instance), read the license, cite it in a comment, and keep the reimplementation a first-class internal package with tests — not a fork you'll have to rebase against an upstream that has never had a release.
  • Budget a few hours per Laravel major to re-verify the translator contract and the container binding. That is the entire maintenance cost of the internal version, and it is far less than integrating and security-reviewing an abandoned dependency.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle