savannabits/admin-translations
Laravel package for managing app translations from an admin interface instead of editing language files by hand. Browse, add, and edit translation keys and values across multiple locales, keeping strings organized and editable at runtime.
Before you install anything, verify compatibility. This package's last release is v7.0.1 (May 2020), and the "v7" in that release refers to the Laravel 7 generation, not a package-semver major. With 0 stars and 0 dependents, you are effectively adopting an unmaintained artifact. Do a dry run first:
composer require savannabits/admin-translations --dry-run
composer why-not savannabits/admin-translations
If your app is on Laravel 9+, expect the resolver to fail on transitive constraints (old illuminate/*, symfony/*, or PHP ^7.x pins). Resolve that before writing any code — a forked, constraint-relaxed copy pinned in repositories is the realistic path, not a composer require.
Minimal setup (assuming the dependency graph resolves):
composer require savannabits/admin-translationsphp artisan vendor:publish --provider="Savannabits\AdminTranslations\AdminTranslationsServiceProvider"
Use --tag=config, --tag=migrations, --tag=views if the full publish is noisy — check what tags the provider actually declares, since the README does not enumerate them.config/admin-translations.php immediately. The two things that matter are the driver (file vs database) and the locales list. Everything else is UI wiring.php artisan migrate if you chose the database driver.savannabits/admin-ui; it does not ship its own dashboard. If you are not already running admin-ui, installation stops here — you are integrating a subsystem into a host you don't have.Where to look first, in order:
config/admin-translations.php — driver, locales, and path/table names.register() — this is where the translation loader gets swapped when the database driver is active. That binding is the real behaviour of the package.resources/views/vendor/admin-translations/ after publishing — the editor view tells you exactly what mutation the package performs on save (flat key/value vs nested arrays).First use case: don't start with a migration of all your language files. Pick one non-critical group (e.g. lang/en/marketing.php), open it in the editor, change one string, save, and verify with:
php artisan tinker
>>> __('marketing.hero_title')
Then change the same key back via file and confirm which source wins. You need that answer — "which side is authoritative per key" — before you let anyone near production strings.
Pick the driver per key class, not per app. The realistic split:
| Content | Driver | Reason |
|---|---|---|
| Marketing copy, CMS strings, legal text | database |
Edited by non-developers, changes without deploy |
| Validation messages, mail subjects, package overrides | file |
Belongs in git, reviewed in PRs, needs to ship with code |
| Everything else | file |
Default; avoid making the DB a second source of truth |
If the package only allows one global driver at a time (check the config), the workaround is to keep the file driver on and use the package purely as an editor that writes back to resources/lang — then commit. That is the lower-risk mode and the one most teams should default to.
DB-driver integration is a loader swap. With the database driver active, Translator resolves through a custom loader that reads the translations table and falls back to files for missing keys. Verify the fallback exists before relying on it:
// tinker
get_class(app('translator')->getLoader()); // confirm it's the package loader
app('translator')->get('some.group.key'); // present in files, absent in DB
If the loader does not fall back, adding a key to a lang file will silently do nothing in an environment with DB translations enabled. Test this explicitly.
Treat the editor as a write path into a cache. Every save must invalidate whatever caches translations: Laravel's own file-cache of loaded groups, config:cache, and any package-level cache key. Wrap edits in a deploy-adjacent step:
php artisan cache:clear
php artisan config:clear
If the package registers an Artisan command for import/export (common in this family), use it as your sync contract:
php artisan translations:export # DB -> lang files, for committing
php artisan translations:import # lang files -> DB, after a deploy
Run export in a scheduled job or pre-deploy hook so translation changes land in git even when edited through the UI. That gives you review and rollback, which the UI alone does not.
Gate write access behind admin-ui permissions. The editor is a direct mutation surface for user-visible content. Ensure the route/menu registration is behind an authorization gate (policy or admin-ui role check), and that it is not reachable in production by anyone with a panel login. This is the single highest-value five minutes you will spend on the package.
Workflow for translators: add the locale to the config locales array, add it to config/app.php available_locales if you use one, then have them use the group/language filters to work through one file at a time. Give them a short written rule: never touch :attribute, :count, :name placeholders, and never remove the | plural separator. Both are trivially easy to break from a textarea.
Adding a new key from the UI should be a two-step: create it in the file driver (so it is in git), then let the DB override existing values. Keys that exist only in the database are invisible to a fresh clone and will break on a new environment.
It is 2020 software. No commit since May 2020, zero dependents. Assume php artisan works but that PHP 8.1+ deprecations, Laravel 9/10/11 helper removals, and doctrine/dbal version conflicts are all live risks. Budget a fork-and-patch afternoon, and pin the fork by commit hash rather than by version constraint — there will be no patch release to save you.
vendor:publish may overwrite your edits. Publishing views and config a second time silently clobbers customizations. Publish once, commit the published files, and never re-publish in CI.
File-driver writes vs. filesystem permissions. Writing into resources/lang or a lang/ directory requires the web user to have write permission on a path that is normally deploy-owned. That combination is a code-execution-adjacent risk on shared hosting, and it fights your deploy pipeline (rsync/symlink releases will discard writes). Prefer the DB driver for anything editorial, or accept that UI writes are ephemeral.
Nested arrays get flattened. Lang files can be nested ('nav' => ['home' => 'Home']), and editors typically flatten to dot keys. A save can rewrite your file into a flat structure — check git diff after the first save on a nested file before you let it touch a real one. Same concern for keys containing dots.
Placeholder and plural corruption is the most likely production incident. :count becoming : counts, or a | separator dropped, yields a runtime exception or a wrong-language string. Mitigate with a smoke test that renders a handful of parameterized keys after every translation deploy.
Caching will lie to you. config:cache freezes the driver and locale list, so per-environment or per-tenant driver switching does not work as you expect. After any config change: php artisan config:clear. After any DB edit that doesn't show up: check whether the translator loader caches groups per request lifecycle.
Missing keys return the key, not an error. marketing.hero_title rendering as literally marketing.hero_title means the key is missing in both the DB and the file — not a package bug. Grep the file first, then check translations table for locale/group mismatch.
Concurrency is last-write-wins. No locking, no revision history in the UI. Two editors on the same group means one silently loses. Serialize edits by group ownership, or keep the authoritative copy in git and treat the UI as a convenience.
Multi-tenant: don't. The config is a single global driver and locale list. Per-tenant translation sets require either separate databases (and queued driver swapping, which fights the config cache) or a custom loader. That is a build, not a config tweak — evaluate whether you want to own that on top of an unmaintained dependency.
Extension points worth knowing: override the view via --tag=views, swap the Translator loader binding in your own service provider registered after this one, and add a custom driver by implementing whatever contract the config's driver key resolves through (check the provider for the match/switch). The loader binding is the only one that matters day-to-day.
Debugging recipe:
get_class(app('translator')->getLoader()); // which loader is live
config('admin-translations.driver'); // driver actually cached
app('translator')->get('group.key'); // resolution result
DB::table('translations')->where(...)->get(); // what the DB really holds
If those four disagree with the UI, you have a cache problem, not a data problem.
Final recommendation: adopt only as a forked, pinned, file-driver-only editor with exported changes committed to git, behind an authorization gate, on a Laravel version you have verified in CI. Do not take the database driver as a source of truth, and do not put it on a critical path where an abandoned dependency becomes an upgrade blocker.
How can I help you explore Laravel packages today?