syriable / filament-translator
Convention-based automatic translations for Filament forms, tables, actions, and pages.
Requires
- php: ^8.3
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- spatie/invade: ^2.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- spatie/laravel-ray: ^1.26
This package is auto-updated.
Last update: 2026-06-05 22:32:41 UTC
README
Syriable Filament Translator
Convention-based automatic translations for Filament panels — forms, tables, actions, infolists, resources, pages, widgets, importers, and exporters.
Syriable Filament Translator derives translation keys from your PHP class names and Filament component names so UI code stays free of hard-coded copy. The package registers lazy label resolvers at boot; when a lang entry is missing, Filament’s default label is preserved.
Features
- Convention-based labels — keys are derived from the owner class and component name; no hard-coded copy. (what gets translated)
- Broad coverage — forms, infolists, tables, columns, filters, summarizers, grouping, actions, importers/exporters, plus page/resource/cluster/widget metadata.
- Path aliases — map namespaces outside Filament’s defaults, e.g.
App\Livewire→livewire. (docs) - Component macros — override or pin a component’s key, including absolute keys. (docs)
- Automatic key creation — scaffold missing required keys into your lang files as you browse, during local development. (docs)
- Configurable required attributes — choose which attributes must be translated via config. (docs)
- Custom schema components — register your own components for convention resolution; Filament’s
Textis supported out of the box. (docs) - Hint-icon tooltips — translate
hintIconTooltipvia thehint_icon_tooltipkey. (docs) - Translatable base classes & traits — drop-in bases/traits for pages, resources, clusters, widgets, importers, and more. (docs)
- Graceful fallback — any key you omit falls back to Filament’s native label.
Requirements
- PHP 8.3+
- Laravel 11, 12, or 13
- Filament 4 or 5
Installation
You can install the package via Composer:
composer require syriable/filament-translator
Register the plugin on your Filament panel:
use Filament\Panel; use Syriable\Filament\Plugins\Translator\TranslatorPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ TranslatorPlugin::make() ->pathAliases([ 'App\\Livewire' => 'livewire', ]), ]); }
The TranslatorServiceProvider is auto-discovered and registers conventionKey() macros on Filament schema components.
Usage
Panel plugin
TranslatorPlugin boots the convention registry when the panel starts. Register it on every panel that should resolve labels automatically.
Path aliases
Map namespace fragments to lang file paths when your classes live outside Filament’s default directory structure:
TranslatorPlugin::make() ->pathAliases([ 'App\\Livewire' => 'livewire', ]); // Replace all aliases instead of merging: TranslatorPlugin::make() ->pathAliases(['App\\Livewire' => 'livewire'], merge: false);
Automatic key creation (local development)
Hand-writing every lang key while building an interface is tedious. Enable
createMissingTranslationKeys() and the package scaffolds any missing required label into the
correct lang file as you load pages — creating the file, the nested array path, and a humanised
default value — so you’re left with a ready-to-translate stub instead of a raw key on screen.
TranslatorPlugin::make() ->createMissingTranslationKeys();
Requesting livewire/auth/login.form.components.actions.forgot-password.label, for example, writes
lang/{locale}/livewire/auth/login.php:
return [ 'form' => [ 'components' => [ 'actions' => [ 'forgot-password' => [ 'label' => 'Forgot Password', ], ], ], ], ];
- Local only. Writes are always skipped in production regardless of the flag — lang files are never written on live requests.
- Required labels only. Optional attributes (placeholder, helper text, tooltip, …) are skipped to keep lang files lean, and existing values are never overwritten.
Customise the seeded value, or gate activation behind a condition, with the method arguments:
// Seed every new key with an empty string instead of a humanised guess: TranslatorPlugin::make()->createMissingTranslationKeys(using: fn (string $key) => ''); // Enable only in the local environment: TranslatorPlugin::make()->createMissingTranslationKeys(fn () => app()->isLocal());
Plugin helpers
TranslatorPlugin::get(); // plugin instance on the current panel TranslatorPlugin::isActive(); // whether the plugin is registered
Standalone Livewire pages
Filament schemas on guest routes still need the registry booted once:
use Syriable\Filament\Plugins\Translator\ConventionRegistry; app(ConventionRegistry::class)->registerDefaults();
Register TranslatorPlugin on a panel with pathAliases() when you need namespace remapping; aliases are read from the active plugin during label resolution. When the plugin is not registered on the active panel, resolution falls back to empty path aliases instead of throwing, so guest routes keep working.
Boot behaviour & double registerDefaults()
TranslatorPlugin::boot() calls registerDefaults() automatically for every panel the plugin is
registered on, so panel pages need no manual boot. Standalone Livewire components rendered
outside a panel (guest/auth routes such as /login) are not covered by panel boot, so the host
app must call registerDefaults() itself — typically once from a service provider:
// app/Providers/AppServiceProvider.php public function boot(): void { app(ConventionRegistry::class)->registerDefaults(); }
It is safe to use both patterns at once (panel plugin + manual boot). registerDefaults() is
idempotent: it tracks the active Filament component manager and a signature of the current
configuration, so repeated calls within the same request — and repeated worker boots under
Laravel Octane — skip re-wiring instead of stacking duplicate
configureUsing hooks. Re-registration only happens when the component manager instance changes
(a fresh app) or when the relevant configuration (required, custom components, path aliases)
changes.
Boot cost trade-off. Wiring is registered eagerly for all supported component families in a single pass rather than lazily per component type. The pass is cheap (it only registers
configureUsingclosures; no resolution happens until a component is rendered) and the idempotency guard above ensures it runs at most once per app instance. Eager-but-idempotent wiring keeps the boot path simple and predictable; lazy per-type registration is intentionally not used because its complexity is not justified once duplicate boots are already elided.
Path aliases without an active panel
Path aliases live on the TranslatorPlugin instance and are read from the active panel during
resolution. On a standalone Livewire route there may be no panel plugin active, in which case
TranslatorPlugin::get() returns a plugin with empty path aliases and resolution falls back to
the default Filament-prefixed namespace derivation (it never throws). If a guest route needs
custom namespace remapping, register TranslatorPlugin (with your pathAliases()) on the default
panel so the aliases are available even when that panel is not the one rendering the page.
| Scenario | Path aliases | Boot requirement |
|---|---|---|
| Panel page (plugin registered) | From the active panel | Automatic via TranslatorPlugin::boot() |
| Standalone Livewire, plugin on any panel | From that panel's plugin | Manual registerDefaults() in a provider |
| Standalone Livewire, no plugin anywhere | Empty (default derivation) | Manual registerDefaults() in a provider |
Configuration
By default only the primary attributes (label, section heading, placeholder content, model labels, …) are required — a missing translation surfaces the convention key — while attributes such as placeholder, tooltip, helperText, and hint are optional and fall back to null.
Publish the config file to change which attributes are required:
php artisan vendor:publish --tag="filament-translator-config"
config/filament-translator.php exposes a required map keyed by attribute (method) name. true makes the attribute required, false makes it optional; anything not listed keeps the default. Overrides apply across every context where the attribute appears (forms, tables, columns, filters, actions, summarizers):
return [ 'required' => [ 'placeholder' => true, // require placeholders everywhere 'tooltip' => true, // require tooltips 'label' => false, // make labels optional ], ];
Required attributes also participate in automatic key creation when that feature is enabled.
Custom schema components
Register your own schema components (extending Filament\Schemas\Components\Component) so their
attributes resolve through the same convention pipeline as first-party fields. Map each component
class to an attribute => allowNull list (false = required, true = optional), in the same
shape as the built-in attribute maps:
// config/filament-translator.php 'components' => [ \App\Filament\Schemas\Components\Separator::class => [ 'text' => false, ], ],
// Used unchanged in a form… Separator::make('or'), // …resolved from lang/{locale}/livewire/auth/login.php: 'form' => ['components' => ['or' => ['text' => 'Or']]],
The required overrides above apply to registered attributes too, and they participate in automatic
key creation when enabled.
Filament’s first-party Filament\Schemas\Components\Text (static schema copy) is supported out of
the box. Address it via key() so the content resolves from lang; explicit content stays literal:
Text::make(null)->key('or'); // resolves {namespace}.form.components.or.content Text::make('Or'); // literal — no lang lookup
Hint-icon tooltips
Hint-icon tooltips are translated from the hint_icon_tooltip key. Set the icon in PHP with a
single argument and put the copy in lang:
TextInput::make('password') ->hintIcon('heroicon-o-question-mark-circle'); // one argument — tooltip comes from lang
'form' => ['components' => ['password' => ['hint_icon_tooltip' => 'Minimum 8 characters.']]],
Passing a second argument — ->hintIcon($icon, $tooltip) — sets the tooltip explicitly and skips
the translation. This needs a Filament version that guards hintIcon() with func_num_args();
older releases clear the tooltip even with a single argument.
Filament version compatibility
Single-argument ->hintIcon($icon) tooltip preservation depends on Filament guarding hintIcon()
with func_num_args() so a single call does not null out a previously wired tooltip:
| Filament | Single-arg hintIcon() preserves the wired tooltip |
|---|---|
| 4.x / 5.x (with the guard) | Yes |
| Older releases without the guard | No — the tooltip is cleared even with one argument |
The package does not hard-pin this behaviour; instead the suite probes it at runtime via
preservesSingleArgHintIconTooltip() (tests/Feature/SchemaResolverTest.php) and skips the
relevant assertion when the running Filament version lacks the guard. Mirror that probe in your own
CI if you support a wide Filament range, and watch this section's matrix when upgrading.
Infolist scope (reserved API)
The Enums\InfolistScope enum is a reserved/future API placeholder. Infolist entries are
fully translated today, but they resolve through the shared schema pipeline under the form /
infolist context rather than a dedicated infolist scope — so unlike ActionScope, SchemaScope,
and TableScope, InfolistScope is intentionally not wired into a resolution path yet. It is
kept as a stable marker for a possible infolist-specific scope; treat it as reserved and do not
depend on additional cases until that work ships.
What gets translated
ConventionRegistry wires lazy resolvers through Filament’s configureUsing hooks. Missing translations fall back to Filament’s native labels.
| Area | Translated attributes |
|---|---|
| Actions | Label, tooltip, badge, modal heading/description, submit/cancel labels, success/failure notification titles |
| Forms & infolists | Field labels, placeholders, helper text, hints, hint-icon tooltips, prefixes/suffixes, validation attributes, section headings/descriptions, tabs, wizard steps, Text content, repeater action labels, select create/edit modal headings, loading messages |
| Tables | Search placeholder, model labels, heading/description, default sort label, empty state heading/description, actions column label |
| Columns | Label, description, tooltip, prefix/suffix, placeholder, default value, validation attribute |
| Filters | Label, indicator, placeholder, true/false labels, constraint labels |
| Summarizers & grouping | Label, prefix, suffix, grouping labels |
| Importers & exporters | Column and action labels |
Monitored column types. Automatic column-label resolution is wired for
TextColumn,IconColumn,ColorColumn,ToggleColumn, andSelectColumn. Custom or other column types are not translated automatically — set their key explicitly with theconventionKey()macro.
Static metadata on pages, resources, clusters, widgets, relation managers, and resource pages is resolved through the Resolves* traits (see below).
Translation key convention
{owner-path}.{context}.{component-name}.{attribute}
Examples:
| UI source | Lang key |
|---|---|
UserResource form field name |
filament/resources/user-resource.form.name.label |
Login page action login |
livewire/auth/login.actions.login.label |
| Field inside a tab | livewire/auth/login.form.components.tabs.tab.{tab}.schema.{field}.label |
| Page title | livewire/auth/login.title |
| Relation manager table | filament/resources/user-resource.relation_managers.posts.table.heading |
Place strings under lang/{locale}/ using nested arrays or dot keys.
Example lang file
For a UserResource form with name and email fields, create
lang/en/filament/resources/user-resource.php:
<?php return [ 'model_label' => 'user', 'plural_model_label' => 'users', 'navigation_label' => 'Users', 'navigation_group' => 'Access', 'form' => [ 'name' => [ 'label' => 'Full name', 'helper_text' => 'First and last name.', ], 'email' => [ 'label' => 'Email address', 'placeholder' => 'you@example.com', ], ], 'table' => [ 'name' => ['label' => 'Name'], 'email' => ['label' => 'Email'], ], ];
Any key you omit falls back to Filament's native label, so you only translate what you need.
Component macros
Override or prefix translation keys on individual schema components:
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; TextInput::make('name') ->conventionKey('custom.segment.name'); Toggle::make('active') ->conventionKey('globals.active', isAbsolute: true);
| Macro | Purpose |
|---|---|
conventionKey() |
Set or derive the translation segment |
getConventionKey() |
Read the evaluated key |
conventionKeyAbsolute() |
Mark the key as absolute (skip owner-path prefixing) |
isConventionKeyAbsolute() |
Check whether the key is absolute |
Base classes
Extend Syriable’s translatable bases instead of Filament’s when you want convention-based metadata out of the box:
| Base class | Replaces |
|---|---|
TranslatablePage |
Filament\Pages\Page |
TranslatableResource |
Filament\Resources\Resource |
TranslatableCluster |
Filament\Clusters\Cluster |
TranslatableCreateRecord |
CreateRecord |
TranslatableEditRecord |
EditRecord |
TranslatableViewRecord |
ViewRecord |
TranslatableListRecords |
ListRecords |
TranslatableManageRecords |
ManageRecords |
TranslatableManageRelatedRecords |
ManageRelatedRecords |
TranslatableRelationManager |
RelationManager |
TranslatableWidget |
Filament\Widgets\Widget |
TranslatableChartWidget |
ChartWidget |
TranslatableTableWidget |
TableWidget |
TranslatableStatsOverviewWidget |
StatsOverviewWidget |
TranslatableExporter |
Exporter |
TranslatableImporter |
Importer |
Example:
use Syriable\Filament\Plugins\Translator\Filament\Pages\TranslatablePage; use Syriable\Filament\Plugins\Translator\Filament\Resources\TranslatableResource; class Login extends TranslatablePage { /* … */ } class UserResource extends TranslatableResource { /* … */ }
Traits
Use traits directly on your own classes when you prefer not to extend the base classes:
| Trait | Resolves |
|---|---|
ResolvesPageLabels |
Title, subheading, navigation label, navigation group |
ResolvesResourceLabels |
Model label, plural model label, navigation label, navigation group, breadcrumb |
ResolvesResourcePageLabels |
Resource page title, subheading, navigation metadata |
ResolvesRelationManagerLabels |
Relation manager title, model label, table/form/action labels |
ResolvesClusterLabels |
Cluster navigation label and group |
ResolvesWidgetLabels |
Shared widget label resolution |
ResolvesChartWidgetLabels |
Chart widget heading and description |
ResolvesTableWidgetLabels |
Table widget heading |
ResolvesStatsOverviewLabels |
Stats overview heading and description |
ResolvesExporterLabels |
Exporter column labels |
ResolvesImporterLabels |
Importer column labels |
Implement TranslatesConventionally when a class exposes resolveLabel() for custom convention lookups.
Known limitations
- Hint-icon tooltips need single-argument
->hintIcon($icon)and a Filament version that guardshintIcon()withfunc_num_args(); a second argument (or older Filament) bypasses the translation. - Custom table column types are not auto-monitored (see What gets translated);
use the
conventionKey()macro for them. Custom schema components can be registered — see Custom schema components. - Nested modal action labels are resolved by walking a parent action's cached and
extra-modal-footer actions. To stay safe during action caching, discovery never evaluates an
action's modal-action closures (which may type-hint a
Model $recordthat is not yet available), so labels for uncached nested modal actions fall back to Filament's native label until the action is mounted/cached. Cached nested actions and extra modal footer actions resolve normally.
Filament compatibility & upgrade strategy
This package integrates deeply with Filament internals to wire convention-based labels. To resolve
nested action, schema, table, and modal labels it uses spatie/invade and reflection against
Filament internals (for example cachedActions / cachedModalActions and prebuilt component
introspection), guarded by class_exists() / method_exists() / ReflectionProperty::isInitialized()
checks so missing internals degrade gracefully instead of throwing.
Declared support: filament/filament: ^4.0 | ^5.0.
Because the integration touches internals, a Filament major release (e.g. v6) carries a higher upgrade risk than a typical dependency. Maintainer playbook for new Filament majors:
- Run the matrix first. The test suite is the canary — every wired surface (actions, schemas, tables, filters, summarizers, widgets, importers/exporters, nested modal actions) has a feature test. Run it against the new Filament version before declaring support.
- Expect renames at the boundary. Failures usually point at a renamed internal property or
method reached via
invade()/reflection. These are centralised inConventionRegistry; the long-term direction is to isolate them behind small adapter helpers so version branches live in one place. - Guard, don't assume. New internals must be probed with
class_exists()/method_exists()/ reflectionisInitialized()so the package keeps working when a hook is absent on a given version (this is howSchemas\Components\Textis handled across v4/v5). - Widen the constraint deliberately. Only bump the
filament/filamentconstraint once the full suite is green on the new major, and record the change in the CHANGELOG.
If you hit a label that stops resolving after a Filament upgrade, the native Filament label is used as a fallback (nothing breaks visually) — please open an issue with the Filament version so the matrix can be updated.
Security considerations
This package has a deliberately small security surface — it resolves translation strings and, in local development only, scaffolds missing keys. It performs no authorization, renders no HTML, handles no HTTP input, and runs no database queries. Two things are worth calling out:
- Lang files are trusted code. Like all Laravel
lang/*.phpfiles, they are loaded withrequireand therefore execute as PHP. The dev-only key writer reads existing lang files viarequirewhen merging new keys, so treat lang files as source code from a trusted origin. - Key writing is production-gated.
MissingTranslationKeyWriternever writes on production requests regardless of configuration, and the resolved target path is validated to stay withinlang_path()to prevent path traversal from a crafted convention key.
See SECURITY.md for the full security model and reporting policy.
Architecture
| Concept | Class / trait | Role |
|---|---|---|
| Panel plugin | TranslatorPlugin |
Registers the package on a panel; holds path aliases and key-creation opts |
| Label registry | ConventionRegistry |
Wires Filament configureUsing hooks; applies required/components config |
| Path aliases | ConfiguresPathAliases |
Maps namespace fragments to lang file paths |
| Key creation | MissingTranslationKeyWriter |
Scaffolds missing required keys into lang files (local dev) |
| Custom keys | conventionKey() macro |
Override the derived key on any schema component |
| Macro storage | Support\ConventionKeyStore |
WeakMap-backed store for macro key state (PHP 8.4 dynamic-property safe) |
| Internals | Support\FilamentInternals |
Single adapter isolating invade()/reflection reads of Filament internals |
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.