dejwcake / admin-translations
Translation manager for brackets/admin-ui
Requires
- php: ^8.5
- dejwcake/admin-listing: ^2.0
- dejwcake/admin-ui: ^2.0
- dejwcake/craftable-translatable: ^2.0
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- maatwebsite/excel: 4.x-dev
Requires (Dev)
- larastan/larastan: ^3.9
- orchestra/testbench: ^11.1
- phpunit/phpunit: ^13.1
README
Admin Translations is a Laravel translation manager package. It scans your application for translation keys, stores them in the database, and provides a clean admin UI to review and edit them. The package ships with a custom translation loader that overrides Laravel’s default loader, so translations are automatically loaded from the database at runtime.
This package is part of Craftable (dejwCake/craftable), an administration starter kit for Laravel 13, forked from Craftable (brackets/craftable).
Documentation
You can find full documentation at https://docs.getcraftable.com/#/admin-translations
Collecting keys: admin-translations:scan-and-save
The command builds the key set from two sources and stores each key exactly once:
- scanning the directories in
admin-translations.scanned_directoriesfortrans()and__()calls, limited to the extensions inscanned_extensions; - reading the lang files — the groups named in
imported_groups, pluslang/{locale}.jsonwhenimported_jsonis on.
The second source exists because some keys are unreachable by scanning. Laravel assembles
validation.* and passwords.* at runtime, and a package may declare keys that only its
published frontend consumes. imported_groups defaults to ['*'] — every group under
lang/{locale} and lang/vendor/{namespace}/{locale}, for the locales in
translatable.locales. Name groups explicitly ('validation', 'brackets/admin-ui::admin') to
narrow it, or set an empty array to import nothing.
Keeping test files out
scanned_extensions covers .vue, .ts and friends, which means a test that exercises the
translation helper looks exactly like real source to the scanner. A line such as
expect(__('greet', { name: 'Sam' })).toBe('Hi Sam') contributes greet as a translation key, and
nothing in the call itself says otherwise.
Use excluded_paths — glob patterns matched against the whole file path, applied before the file is
read:
'excluded_paths' => [ '*tests*', '*node_modules*', '*.test.ts', '*.spec.ts', ],
It is empty by default, so nothing that is scanned today stops being scanned on upgrade.
Storing the translations too
By default the command stores keys only — the text stays in the lang files, and the loader merges file and database at runtime with the database winning.
| Flag | Effect |
|---|---|
| (none) | Keys only. Existing text is untouched. |
--with-text |
Also read each key's current value from the lang files and store it, but only into locales that are still empty. Anything edited in the admin UI survives. |
--with-text --overwrite |
Replace stored translations with the file value, for every locale the files supply. Prompts for confirmation, reporting how many rows are affected. |
--force |
Skip that confirmation. For CI only. |
--overwrite without --with-text is refused rather than ignored.
Seeding is worth doing once the key set is final: until a translation has a stored value, the admin list renders it through the runtime fallback, so searching by translated text cannot find it.
Seeding reads the lang files only, never the database. The registered
TranslationLoaderManagermerges file and database values, with the database winning — so resolving through it would feed stored values back into themselves. An accidental edit would reseed itself on every run and could never be corrected from the files.--with-texttherefore resolves against a plainIlluminate\Translation\FileLoader. Keep it that way if you touchFileTranslationResolver.
Where translations are authored
The two directions are deliberately not symmetric:
| Where | Mechanism | |
|---|---|---|
| Seed translations | development | files → database, via --with-text |
| Author translations | production | admin UI → database, which persists |
Which is why:
- There is no database → file write-back, and no
--sync-jsoncommand. Such a command could only run in development, where the edits are throwaway by nature — losing them to amigrate:freshis not a defect. The real work happens against the production database. - The lang files are frozen after release, not deleted. They stay checked in as the seed and
as the fallback if the
translationstable is ever empty or unreachable; they simply stop being edited.
Translation key collation
A translation is identified by its namespace, group and key. Those three columns are stored
with a case- and accent-sensitive collation (utf8mb4_bin on MySQL and MariaDB; PostgreSQL and
SQLite compare text exactly by default), because Log in and log in, or Uložiť and Ulozit, are
different keys and each needs its own row.
This matters because the usual Laravel default, utf8mb4_unicode_ci, is insensitive to both. Under
it the two spellings collide, only one of them can ever be stored, and
admin-translations:scan-and-save reports more translations than it saved.
Searching in the admin UI is not affected: admin-listing normalises the comparison itself
instead of relying on the column collation, so search stays case- and accent-insensitive on every
driver.
That order is a dependency, not a coincidence. On MySQL, case-insensitive search was historically a side effect of
utf8mb4_unicode_ci—LIKE '%dash%'matchedDashboardonly because the column collation said so. Switching toutf8mb4_bintherefore breaks key search unless the search normalises the comparison itself first.admin-listingdoes that from the version required here, applying an explicitCOLLATEoverride that still wins over autf8mb4_bincolumn. Do not runmake_translation_keys_case_sensitiveagainst an olderadmin-listing— equality becomes correct while search silently stops finding things.
Warning — customised columns. The
make_translation_keys_case_sensitivemigration writes the full column definition, using the package's own shape:VARCHAR(255)fornamespaceandgroup(withnamespacedefaulting to'*'), andTEXTforkey. If your application has altered any of those — a wider type, a different default, a changed nullability — the migration will reset them to the package's definition. Check the table first and adjust the published migration if needed.
Issues
Where do I report issues? If something is not working as expected, please open an issue in the main repository https://github.com/dejwCake/craftable.
How to develop this project
Composer
Update dependencies:
docker compose run -it --rm test composer update
Composer normalization:
docker compose run -it --rm php-qa composer normalize
Run code analysis tools (php-qa)
PHP compatibility:
docker compose run -it --rm php-qa phpcs --standard=.phpcs.compatibility.xml --cache=.phpcs.cache
Code style:
docker compose run -it --rm php-qa phpcs -s --colors --extensions=php
Fix style issues:
docker compose run -it --rm php-qa phpcbf -s --colors --extensions=php
Static analysis (phpstan):
docker compose run -it --rm php-qa phpstan analyse --configuration=phpstan.neon
Mess detector (phpmd):
docker compose run -it --rm php-qa phpmd ./config,./database,./lang,./resources,./routes,./src,./tests ansi phpmd.xml --suffixes php --baseline-file phpmd.baseline.xml
Run tests
Run tests against mariadb:
docker compose run -it --rm -e DB_CONNECTION=mysql test ./vendor/bin/phpunit
Run tests against postgresql:
docker compose run -it --rm -e DB_CONNECTION=pgsql test ./vendor/bin/phpunit
Run tests with coverage:
docker compose run -it --rm test ./vendor/bin/phpunit --coverage-text
Run the whole PHP suite
Run every PHP check and the test suite against both databases in sequence (stops at the first failure):
docker compose run -it --rm test composer update \ && docker compose run -it --rm php-qa composer normalize \ && docker compose run -it --rm php-qa phpcs --standard=.phpcs.compatibility.xml --cache=.phpcs.cache \ && docker compose run -it --rm php-qa phpcs -s --colors --extensions=php \ && docker compose run -it --rm php-qa phpstan analyse --configuration=phpstan.neon \ && docker compose run -it --rm php-qa phpmd ./config,./database,./lang,./resources,./routes,./src,./tests ansi phpmd.xml --suffixes php --baseline-file phpmd.baseline.xml \ && docker compose run -it --rm -e DB_CONNECTION=mysql test ./vendor/bin/phpunit \ && docker compose run -it --rm -e DB_CONNECTION=pgsql test ./vendor/bin/phpunit