genl/matice
Matice is a Laravel package for creating simple, flexible menus. Build nested menu structures, add links and custom attributes, and render them in Blade views through a clean, configurable API. Ideal for application navigation, admin panels, and sidebar menus.
Matice is a translation manager for Laravel: it stores translation keys/values (in the DB, in lang files, or both), gives you a guarded admin UI at /matice for editing and machine-translating them, and exposes those translations to both Blade and your front-end JS.
Minimal path from zero to useful:
composer require genl/matice
php artisan vendor:publish --tag=matice.config
php artisan vendor:publish --tag=matice.migrations
php artisan vendor:publish --tag=matice.assets
php artisan migrate
Then, before you browse to the UI, open config/matice.php and set middleware / guard (and route_prefix if /matice collides). The UI can rewrite every string in your app — treat it like a CMS and put it behind auth plus an admin gate on day one, not later.
First use case worth doing end-to-end:
locales / default_locale at the languages you actually ship./matice, run the scanner so it picks up existing __(), trans() and @lang calls, and import your current lang/ files.@translations() to your main layout and confirm the payload renders.Where to look first: config/matice.php (every behaviour is there), then the @translations section of the README, then the UI's scan/import screens.
Scope the directive per page, not globally. @translations() with no arguments ships every key you have — fine for a demo, painful on a large app. The common pattern is one call per layout/partial, grouped by namespace:
@translations('post')
@translations('auth')
New in 1.3.0: @translations accepts only / except overrides, so you can trim a namespace per view without editing config or duplicating groups:
@translations('post', ['only' => ['post.index', 'post.show']])
@translations('post', ['except' => ['post.admin.*']])
This is the release's headline feature. It's the right tool for SPA-ish pages where a shared layout would otherwise drag in the whole namespace, and for A/B or admin-only screens. Verify the exact array shape against the README for your version — it's brand new in 1.3.0.
Pick a storage strategy deliberately. Matice supports files, database, and both. Files keep translations in VCS and in code review; database lets non-developers edit without a deploy. Both-at-once is the pragmatic middle: DB as the source of truth, with a periodic write-back to lang/ so changes land in git and survive a DB reset. Decide this before you have thousands of keys, because switching later means reconciling duplicates.
Locale switching. Set app()->setLocale() from your own middleware (session, route segment, user preference) before the directive renders; the directive emits the currently active locale. Don't expect the UI's locale toggle to affect the public site — they're independent.
Auto-translate workflow. Fill the source locale by hand, then use the UI's translate action to propagate to the others, then review. In practice you want a hard rule: machine output is a draft, and a human sign-off pass happens on any string that appears in checkout, auth, or email subject lines. Configure exactly one provider key in config/matice.php/env and keep the rest unset so nothing ambiguous gets picked.
Keep the scanner honest. It finds static keys; it can't see __("post.{$type}.title"). Anything built dynamically needs to be added manually or generated through a small seeder so the UI knows the key exists. Conversely, restrict the scan paths so vendor/ package translations don't flood your table.
On PHP 8.4, this release is the one you want. 1.3.0 fixes the deprecated preg_split() null-limit warning that 1.2.x emits on newer PHP. It's noise rather than breakage, but if your logs are drowning in deprecations, that alone justifies the bump. No breaking changes or deprecations in this release; 1.2.x → 1.3.0 should be a drop-in.
The only/except overrides interact with config. If you already limit keys globally via the config path/allow-list options, the directive-level filters are applied on top of that. If a key you expect isn't in the payload, check both layers — debugging only the directive argument will waste an afternoon.
Directive renders nothing? In order: is the directive registered (stale view:cache after upgrade — run php artisan view:clear)? Is the locale present in locales? Do rows actually exist for that locale, or only the default one? A key that exists in en but not fr silently falls through to the fallback locale or to the raw key, which looks identical to "Matice isn't working".
Admin UI 403s or redirect-loops are almost always guard vs middleware mismatch — e.g. auth middleware with a guard that isn't the one your users log in through, or an API-only app where auth tries to redirect to a login route that doesn't exist. Set middleware to something that returns 403 rather than redirecting in that case.
Re-running auto-translate can clobber edits. Machine translation typically overwrites the target locale. Snapshot the table (or write back to files and commit) before a bulk translate, and use the UI's per-key status rather than re-translating "everything" on a whim.
Asset republishing. 1.3.0 adds no new front-end assets, so upgrading is composer update plus view:clear. If you've customised published assets, keep them in your own resources path rather than editing the published copy — otherwise every asset republish is a merge conflict.
Changing table_name or the bound model after data exists means a hand-written migration; there's no built-in rename path. Decide the table name at install time.
CI note that doesn't affect you: the release disables Composer's security-advisory blocking in their CI to test older Laravel versions. That's a maintainer-side change — it doesn't relax anything in your app, but it does mean the older-Laravel legs of their matrix are tested against advisories they've silenced. Worth a glance at your own composer audit if you're pinned to an old Laravel.
Extension point worth knowing: the model and route behaviour are config-bound, so if you need multi-tenant translation sets (per-tenant overrides, for example), override the model and add a global scope rather than forking — the directive and UI both read through the configured model.
How can I help you explore Laravel packages today?