arbory/translation
Arbory Translation is a Laravel package for managing application translations from the database. It provides a practical workflow for editing, organizing, and maintaining multilingual strings, helping teams centralize localization and keep language content consistent across projects.
This release is a patch-level 3.1.0 on top of a package whose job is to sit between your translation lookups (__(), trans(), @lang) and whichever backing store holds the strings, caching the resolved values so you aren't hitting the database or disk on every render.
Minimal setup
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
3.1.0 live: the cache.store, the cache.ttl, and the tag/grouping behaviour. Read it before reading any source.redis, memcached, and array do; file and database do not.First use case
The entry-level win is: put a cached lookup in front of a DB-backed translation table so a page with 200 strings doesn't issue 200 queries. Configure the store, point the package at your translations table, load a page, and verify with DB::listen() or Telescope that the second request produces zero translation queries.
Where to look first
ttl, store, and tag settings.3.1.0 repaired, so understand what triggers invalidation before you rely on it.Flush on write, read through cache
The core workflow is write-through invalidation: whenever a translation row is created, updated, or deleted, flush the affected cache entry (or tag). 3.1.0 fixes the flushing path, so if you previously worked around broken invalidation with manual Cache::forget() calls, custom versioned key prefixes, or a nightly cache:clear, you can delete those workarounds now.
// Typical pattern: observer or model event
protected static function booted(): void
{
static::saved(fn ($t) => TranslationCache::flush($t->locale, $t->group));
static::deleted(fn ($t) => TranslationCache::flush($t->locale, $t->group));
}
Tag-scoped invalidation
Use tags to invalidate by locale or by translation group rather than nuking everything:
Cache::tags(['translations', "locale:{$locale}"])->flush();
Because 3.1.0 fixes taggable-store detection, this is now safe to write unconditionally — the package should degrade to a plain flush on stores without tag support instead of throwing or silently no-oping. Verify the behaviour on your configured store rather than assuming.
TTL tuning by content class
Split your keys into two mental buckets:
null (forever), flushed explicitly on write. These rarely change and are the bulk of the benefit.Warm the cache on deploy
Add a cache-warm step to your deploy pipeline so the first request after a release isn't a cold-cache stampede. If the package exposes an artisan warm command, call it after migrate; otherwise iterate locales and groups and prime them.
Locale and fallback handling
Cache keys must include the resolved locale — otherwise a request in fr can poison the cache for en. If you support a fallback locale, make sure the fallback resolution happens before the cache write, or you'll cache the fallback string under the primary locale key.
Testing
Bind array as the cache store in phpunit.xml so tests exercise the real code path without Redis. Assert both the hit path (query count stays at zero) and the invalidation path (write a translation, assert the old value is gone).
TTL units changed meaningfully — audit your config after upgrading. The 3.1.0 fix corrects TTL unit handling. If you had previously compensated for the bug by multiplying values (e.g. storing 3600 * 60 because a "minutes" value was being read as seconds, or vice versa), that compensation is now wrong. Re-read the config docblock, set a value, and assert the expiry empirically with Cache::flush() + sleep() rather than trusting the comment.
Taggable detection changes behaviour on non-taggable stores. Previously you may have been getting an exception, a silent no-op, or over-broad flushing on file/database cache drivers. After 3.1.0 the detection is corrected, so flush granularity on those stores may change. If you run file cache in local dev and Redis in production, test invalidation in both — the fix is precisely in the branch that differs between them.
Drop your flushing workarounds. Anything you added to compensate for the broken flush — a cache.version key bumped on save, a queue job that clears the whole store, a cron — is now redundant and may actively conflict (e.g. double-flushing, or a version bump that orphans old keys until TTL). Remove them deliberately rather than leaving them as belt-and-braces; orphaned keys will quietly consume memory.
Deprecation warnings fixed — the package is clean on modern PHP/Laravel, but if you've subclassed the cache repository or overridden methods to suppress warnings, re-check those overrides against the new signatures. Your subclass is the thing most likely to still emit deprecations.
Debugging a suspected cache miss
Cache::tags([...])->flush() on a non-taggable store is the #2 cause.config('app.debug') branch sometimes bypasses caching in local environments, which makes it look broken in dev and fine in prod (or the reverse).Extension points worth knowing
Upgrade risk assessment: low. No breaking API changes indicated; the risk is confined to behaviour changes where you had compensated for the bugs. Diff your config against the published stub and re-test invalidation on the cache driver you actually deploy with.
How can I help you explore Laravel packages today?