syriable/filament-translator
Convention-based automatic translations for Filament panels. Derives translation keys from class and component names to keep UI code free of hard-coded labels. Covers forms, tables, actions, resources/pages/widgets, importers/exporters, with aliases, macros, and dev-time key scaffolding.
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.
App\Livewire → livewire. (docs)Text is supported out of the box. (docs)hintIconTooltip via the hint_icon_tooltip key. (docs)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.
TranslatorPlugin boots the convention registry when the panel starts. Register it on every panel that should resolve labels automatically.
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);
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',
],
],
],
],
];
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());
TranslatorPlugin::get(); // plugin instance on the current panel
TranslatorPlugin::isActive(); // whether the plugin is registered
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.
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 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 |
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.
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.
Non-schema components are supported as well — table columns (extending Filament\Tables\Columns\Column),
filters, summarizers, and actions are classified from the class hierarchy and wired with the matching
resolver. A bare class-string entry (no attribute map) adopts that category's default attributes:
'components' => [
// Adopts the column default map (label, tooltip, description, …):
\App\Filament\Tables\Columns\ProgressColumn::class,
// Or pin specific attributes:
\App\Filament\Tables\Filters\StatusFilter::class => [
'label' => false,
],
],
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 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.
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.
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.
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).
{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.
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' => '[email protected]',
],
],
'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.
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 |
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 { /* … */ }
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.
->hintIcon($icon) and a Filament
version that guards hintIcon() with func_num_args(); a second argument (or older Filament) bypasses
the translation.conventionKey() macro for them. Custom schema components can be registered — see
Custom schema components.Model $record that 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.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:
invade()/reflection. These are centralised in ConventionRegistry; the
long-term direction is to isolate them behind small adapter helpers so version branches live in
one place.class_exists() / method_exists() /
reflection isInitialized() so the package keeps working when a hook is absent on a given
version (this is how Schemas\Components\Text is handled across v4/v5).filament/filament constraint 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.
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/*.php files, they are loaded with
require and therefore execute as PHP. The dev-only key writer reads existing lang files via
require when merging new keys, so treat lang files as source code from a trusted origin.MissingTranslationKeyWriter never writes on production
requests regardless of configuration, and the resolved target path is validated to stay within
lang_path() to prevent path traversal from a crafted convention key.See SECURITY.md for the full security model and reporting policy.
| Concept | Class / trait | Role |
|---|---|---|
| Panel plugin | TranslatorPlugin |
Registers the package on a panel; holds path aliases and key-creation opts |
| Orchestrator | ConventionRegistry |
Holds the required/components config, drives registration, and exposes the shared label-lookup utilities and the static resolve*Label() entry points |
| Wiring | Wiring\{Action,Schema,CustomComponent,Table}Wiring |
Attach the configureUsing hooks for each component family |
| Resolution | Resolvers\{Action,Schema,Table}LabelResolver |
Build the dotted convention key and walk the parent path to the owning page/resource |
| 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 |
composer test
Please see CHANGELOG for more information on what has changed recently.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.
How can I help you explore Laravel packages today?