A powerful Filament v4 Select component that fully extends Filament's native Select while adding cross-locale search functionality for Spatie Laravel Translatable models. Search across all locales simultaneously, regardless of the current application locale, while maintaining 100% compatibility with all native Filament Select features.
- ๐ Cross-Locale Search: Search across all locales simultaneously, regardless of current app locale
- ๐ฏ Full Filament Compatibility: Inherits ALL native Select features (relationships, multi-select, preloading, etc.)
- โก Performance Optimized: Efficient JSON column queries with N+1 prevention
- ๐ง Highly Configurable: Custom search fields, locales, query modifiers, and more
- ๐งช Thoroughly Tested: 52 tests covering unit, feature, and integration scenarios
- ๐๏ธ Clean Architecture: Separation of concerns with dedicated services
- ๐ฆ Zero Breaking Changes: Drop-in replacement for standard Filament Select
- ๐ Database Agnostic: Optimized for MySQL, PostgreSQL, and SQLite
- ๐ค Case-Insensitive Search: Automatic case-insensitive search across all database engines
- ๐ข Multi-Tenancy Ready: Seamless integration with Laravel Filament's tenancy system
- PHP: ^8.1
- Laravel: ^10.0|^11.0
- Filament: ^4.0 (v4 only - clean, modern implementation)
- Spatie Laravel Translatable: ^6.0
Install the package via Composer:
composer require xoshbin/translatable-selectYou can publish the configuration file to customize default settings:
php artisan vendor:publish --tag="translatable-select-config"This will publish the configuration file to config/translatable-select.php where you can customize:
- Default search behavior and limits
- Locale resolution strategies
- Performance optimization settings
- Database-specific configurations
TranslatableSelect is a complete extension of Filament's native Select component. It inherits ALL standard functionality while adding powerful cross-locale search capabilities.
โ 100% Filament Select Compatibility: All native features work exactly as expected โ Cross-Locale Search: Search across all locales simultaneously โ Relationship Support: Full support for Eloquent relationships โ Multi-Select: Native multiple selection support โ Preloading: Efficient option preloading โ Query Modification: Custom query constraints and filters
// For direct model selection with cross-locale search
TranslatableSelect::forModel('currency_id', Currency::class, 'name')
->label('Currency')
->searchableFields(['name', 'code'])
->required()// For Eloquent relationships with cross-locale search
TranslatableSelect::make('category_id')
->relationship('category', 'name')
->searchableFields(['name', 'description'])
->multiple()
->preload()- Drop-in Replacement: Replace any
Select::make()withTranslatableSelect::make() - Enhanced Search: Automatically searches across all locales
- Performance Optimized: Efficient JSON column queries
- Highly Configurable: Extensive customization options
use Xoshbin\TranslatableSelect\Components\TranslatableSelect;
// Basic usage - searches across all locales automatically
TranslatableSelect::forModel('currency_id', Currency::class, 'name')
->label('Currency')
->required()// Works with Eloquent relationships
TranslatableSelect::make('category_id')
->relationship('category', 'name')
->label('Category')
->multiple()
->preload()// Full feature example
TranslatableSelect::forModel('product_id', Product::class, 'name')
->label('Product')
->searchableFields(['name', 'description'])
->searchLocales(['en', 'ar', 'ku'])
->modifyQueryUsing(fn($query) => $query->where('active', true))
->multiple()
->preload()
->required()TranslatableSelect::forModel('product_id', Product::class, 'name')
->searchableFields(['name', 'description', 'sku']) // Search multiple fieldsTranslatableSelect::forModel('category_id', Category::class, 'name')
->searchLocales(['en', 'ar']) // Only search in specific localesTranslatableSelect::forModel('tag_id', Tag::class, 'name')
->fallbackLocale('en') // Fallback when translation missingTranslatableSelect::forModel('user_id', User::class, 'name')
->modifyQueryUsing(fn($query) => $query->where('active', true))TranslatableSelect::make('category_id')
->relationship('category', 'name')
->modifyQueryUsing(fn($query) => $query->where('parent_id', null))TranslatableSelect::forModel('currency_id', Currency::class, 'name')
->preload() // Load all options immediatelyTranslatableSelect::forModel('product_id', Product::class, 'name')
->searchDebounce(300) // Delay search by 300msTranslatableSelect is designed as a complete drop-in replacement for Filament's native Select component:
// Before: Standard Filament Select
Select::make('category_id')
->relationship('category', 'name')
->searchable()
->preload()
// After: TranslatableSelect with cross-locale search
TranslatableSelect::make('category_id')
->relationship('category', 'name')
->searchableFields(['name']) // Now searches across all locales!
->preload()Add powerful search capabilities to existing selects:
// Standard select with limited search
Select::make('product_id')
->relationship('product', 'name')
->searchable()
// Enhanced with cross-locale search and multiple fields
TranslatableSelect::make('product_id')
->relationship('product', 'name')
->searchableFields(['name', 'sku', 'description']) // Search multiple fields
->searchLocales(['en', 'ar', 'ku']) // Across multiple locales
->searchDebounce(300) // Performance optimization-
Replace Import Statement:
// Old use Filament\Forms\Components\Select; // New use Xoshbin\TranslatableSelect\Components\TranslatableSelect;
-
Update Component Usage:
// Old Select::make('field_name') // New TranslatableSelect::make('field_name')
-
Add Search Configuration:
// Add these methods for enhanced functionality ->searchableFields(['name', 'code']) ->searchLocales(['en', 'ar', 'ku']) // Optional: specify locales
-
Review Query Modifiers (Important for Multi-Tenant Apps):
// Remove manual tenant filtering - let Filament handle it // Old (problematic) ->modifyQueryUsing(fn($query) => $query->where('company_id', $tenant->id)) // New (correct) ->modifyQueryUsing(fn($query) => $query->where('active', true))
Your translatable models should use the HasTranslations trait from Spatie Laravel Translatable:
use Spatie\Translatable\HasTranslations;
class Currency extends Model
{
use HasTranslations;
public array $translatable = ['name'];
protected $fillable = ['code', 'name', 'symbol'];
}use Spatie\Translatable\HasTranslations;
class Category extends Model
{
use HasTranslations;
public array $translatable = ['name', 'description'];
protected $fillable = ['name', 'description', 'active'];
protected $casts = [
'active' => 'boolean',
];
}use Spatie\Translatable\HasTranslations;
class Product extends Model
{
use HasTranslations;
public array $translatable = ['name', 'description'];
protected $fillable = ['name', 'description', 'sku', 'category_id'];
public function category()
{
return $this->belongsTo(Category::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}The package comes with a comprehensive configuration file. Here are the key settings:
return [
/*
|--------------------------------------------------------------------------
| Default Search Behavior
|--------------------------------------------------------------------------
*/
'default_search_limit' => 50,
'default_search_fields' => ['name'],
'enable_cross_locale_search' => true,
/*
|--------------------------------------------------------------------------
| Locale Configuration
|--------------------------------------------------------------------------
*/
'locale_resolution' => [
'strategy' => 'auto', // 'auto', 'config', 'manual'
'fallback_locale' => 'en',
'available_locales' => ['en', 'ar', 'ku'],
],
/*
|--------------------------------------------------------------------------
| Performance Settings
|--------------------------------------------------------------------------
*/
'performance' => [
'enable_query_caching' => true,
'cache_duration' => 300, // 5 minutes
'max_search_results' => 100,
],
/*
|--------------------------------------------------------------------------
| Database Optimization
|--------------------------------------------------------------------------
*/
'database' => [
'case_insensitive_search' => true,
'json_extraction_patterns' => [
'mysql' => 'LOWER(JSON_UNQUOTE(JSON_EXTRACT(`{field}`, "$.{locale}"))) LIKE LOWER(?)',
'pgsql' => 'LOWER(({field}->>\'{locale}\')) ILIKE ?',
'sqlite' => 'LOWER(json_extract(`{field}`, "$.{locale}")) LIKE LOWER(?)',
],
],
];TranslatableSelect seamlessly integrates with Laravel Filament's tenancy system. The component automatically respects tenant scoping when used in tenant-aware resources, but there are important considerations to ensure optimal performance and avoid conflicts.
โ INCORRECT - Causes Search Conflicts:
// DON'T DO THIS - Manual tenant filtering conflicts with Filament's automatic tenancy
TranslatableSelect::forModel('account_id', Account::class, 'name')
->modifyQueryUsing(fn($query) => $query->where('company_id', Filament::getTenant()->id))
->searchableFields(['name', 'code'])โ CORRECT - Let Filament Handle Tenancy:
// DO THIS - Filament automatically applies tenant scoping
TranslatableSelect::forModel('account_id', Account::class, 'name')
->searchableFields(['name', 'code'])
->modifyQueryUsing(fn($query) => $query->where('active', true)) // Only add business logic filters// Filament automatically adds tenant scoping - no manual filtering needed
TranslatableSelect::make('category_id')
->relationship('category', 'name')
->searchableFields(['name', 'description'])
->modifyQueryUsing(fn($query) => $query->where('active', true)) // Business logic only// Focus on business rules, not tenant filtering
TranslatableSelect::forModel('product_id', Product::class, 'name')
->searchableFields(['name', 'sku'])
->modifyQueryUsing(function($query) {
return $query->where('active', true)
->where('stock_quantity', '>', 0)
->orderBy('name');
})// For complex tenant relationships
TranslatableSelect::make('supplier_id')
->relationship('supplier', 'name')
->searchableFields(['name', 'company_name'])
->modifyQueryUsing(fn($query) => $query->where('approved', true))Ensure your models are properly configured for tenancy:
use Spatie\Translatable\HasTranslations;
use Illuminate\Database\Eloquent\Model;
class Account extends Model
{
use HasTranslations;
public array $translatable = ['name'];
protected $fillable = ['name', 'code', 'type', 'company_id'];
// Filament will automatically scope by company_id when using tenancy
public function company()
{
return $this->belongsTo(Company::class);
}
}TranslatableSelect::forModel('product_id', Product::class, 'name')
->label('Product')
->searchableFields(['name', 'description', 'sku'])
->searchLocales(['en', 'ar', 'ku'])
->modifyQueryUsing(fn($query) => $query->where('active', true)->with('category'))
->getOptionLabelUsing(fn($record) => "{$record->name} ({$record->sku})")
->multiple()
->preload()
->required()TranslatableSelect::make('category_id')
->relationship('category', 'name')
->searchableFields(['name', 'description'])
->modifyQueryUsing(fn($query) => $query->where('active', true)) // No manual tenant filtering!
->preload()
->required()TranslatableSelect::forModel('tags', Tag::class, 'name')
->label('Tags')
->searchableFields(['name'])
->searchDebounce(300)
->multiple()
->preload()
->getOptionLabelUsing(fn($record) => "๐ท๏ธ {$record->name}")
->placeholder('Select tags...')// Income accounts for product configuration
TranslatableSelect::forModel('income_account_id', Account::class, 'name')
->label('Income Account')
->searchableFields(['name', 'code'])
->modifyQueryUsing(fn($query) => $query->whereIn('type', [
AccountType::Income,
AccountType::OtherIncome
]))
->getOptionLabelUsing(fn($record) => "{$record->code} - {$record->name}")
->required()
// Expense accounts for product configuration
TranslatableSelect::forModel('expense_account_id', Account::class, 'name')
->label('Expense Account')
->searchableFields(['name', 'code'])
->modifyQueryUsing(fn($query) => $query->whereIn('type', [
AccountType::Expense,
AccountType::CostOfGoodsSold
]))
->getOptionLabelUsing(fn($record) => "{$record->code} - {$record->name}")
->required()TranslatableSelect::make('product_id')
->relationship('product', 'name')
->searchableFields(['name', 'sku', 'description'])
->modifyQueryUsing(fn($query) => $query->where('active', true))
->getOptionLabelUsing(fn($record) => "{$record->sku} - {$record->name}")
->preload()
->required()TranslatableSelect::forModel('customer_id', Customer::class, 'name')
->label('Customer')
->searchableFields(['name', 'company_name', 'email'])
->modifyQueryUsing(fn($query) => $query->where('active', true))
->getOptionLabelUsing(fn($record) => $record->company_name
? "{$record->name} ({$record->company_name})"
: $record->name)
->searchDebounce(300)
->required()The package consists of three main components:
- TranslatableSelect Component: Extends Filament's Select with cross-locale search
- LocaleResolver Service: Handles locale detection and management
- TranslatableSearchService: Manages cross-locale search logic
-
Locale Detection: Automatically detects available locales from:
- Laravel application configuration (
config/app.php) - Model's translatable configuration
- Manual configuration via
searchLocales()
- Laravel application configuration (
-
Field Analysis: Identifies translatable fields from model's
$translatablearray -
Query Construction: Builds efficient database queries using JSON extraction:
-- Example MySQL query for searching "tech" across locales (case-insensitive) SELECT * FROM categories WHERE LOWER(JSON_UNQUOTE(JSON_EXTRACT(`name`, "$.en"))) LIKE LOWER('%tech%') OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(`name`, "$.ar"))) LIKE LOWER('%tech%') OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(`name`, "$.ku"))) LIKE LOWER('%tech%') LIMIT 50
-
Result Processing: Formats results using current locale with fallback support
The component automatically performs case-insensitive searches across all supported database engines:
- MySQL: Uses
LOWER()functions for both JSON fields and search terms - PostgreSQL: Uses
ILIKEoperator for case-insensitive pattern matching - SQLite: Uses
LOWER()functions similar to MySQL
This means searching for "product", "Product", or "PRODUCT" will all return the same results.
- Efficient JSON Queries: Database-specific optimized JSON extraction
- Query Limits: Configurable result limits prevent performance issues
- N+1 Prevention: Eager loading for relationships
- Search Debouncing: Configurable search delays
- Preloading Support: Load options immediately when needed
1. Component not found
# Clear cache and rediscover packages
php artisan config:clear
php artisan package:discover2. "No options match your search" despite valid data
This is often caused by conflicting query modifiers in multi-tenant applications:
// โ PROBLEM: Manual tenant filtering conflicts with Filament's automatic tenancy
TranslatableSelect::forModel('account_id', Account::class, 'name')
->modifyQueryUsing(fn($query) => $query->where('company_id', $tenant->id)) // Causes conflicts!
// โ
SOLUTION: Remove manual tenant filtering, let Filament handle it
TranslatableSelect::forModel('account_id', Account::class, 'name')
->searchableFields(['name', 'code'])
->modifyQueryUsing(fn($query) => $query->where('active', true)) // Business logic only3. Case sensitivity issues
The component automatically handles case-insensitive search, but if you're experiencing issues:
// Ensure you're not overriding the search behavior
TranslatableSelect::forModel('product_id', Product::class, 'name')
->searchableFields(['name']) // Let the component handle case sensitivity4. No search results
// Ensure your model has the HasTranslations trait
use Spatie\Translatable\HasTranslations;
class YourModel extends Model
{
use HasTranslations;
public array $translatable = ['name'];
}5. Search not working across locales
// Check if translatable fields are properly configured
TranslatableSelect::forModel('model_id', YourModel::class, 'name')
->searchableFields(['name']) // Explicitly set searchable fields
->searchLocales(['en', 'ar', 'ku']) // Specify locales if needed6. Performance issues
// Optimize with search limits and debouncing
TranslatableSelect::forModel('product_id', Product::class, 'name')
->searchDebounce(300) // Add 300ms delay
->modifyQueryUsing(fn($query) => $query->limit(25))7. Relationship search not working
// Ensure the relationship method exists and is properly defined
TranslatableSelect::make('category_id')
->relationship('category', 'name') // 'category' method must exist on the model
->searchableFields(['name'])// Check available locales
$localeResolver = app(\Xoshbin\TranslatableSelect\Services\LocaleResolver::class);
dd($localeResolver->getAvailableLocales());
// Test search service directly
$searchService = app(\Xoshbin\TranslatableSelect\Services\TranslatableSearchService::class);
dd($searchService->getFilamentSearchResults(
Account::class,
'product', // Search term
['name', 'code'], // Search fields
['en', 'ar', 'ku'], // Search locales
null, // Query modifier
50 // Limit
));
// Check current locale
dd(app()->getLocale());
// Test model translatable configuration
dd(Account::make()->getTranslatableAttributes());
// Check if tenancy is affecting queries (in tenant-aware resources)
dd(Filament::getTenant()?->getKey());If search is not working, enable query logging to see what's happening:
// Add this to your resource or form to debug queries
use Illuminate\Support\Facades\DB;
// Enable query logging
DB::enableQueryLog();
// Perform your search...
// Check the queries
dd(DB::getQueryLog());Problem: Duplicate tenant filtering
-- This query shows duplicate company_id conditions
SELECT * FROM accounts
WHERE company_id = 1 -- Filament's automatic tenancy
AND company_id = 1 -- Your manual filtering (duplicate!)
AND (LOWER(JSON_UNQUOTE(JSON_EXTRACT(`name`, "$.en"))) LIKE LOWER('%product%'))Solution: Remove manual tenant filtering
// Let Filament handle tenancy automatically
TranslatableSelect::forModel('account_id', Account::class, 'name')
->searchableFields(['name', 'code'])
// No manual company_id filtering needed!The package includes a comprehensive test suite with 52 tests covering:
- Unit Tests: LocaleResolver and TranslatableSearchService
- Feature Tests: TranslatableSelect component functionality
- Integration Tests: Filament compatibility and real-world usage
# Run all tests
composer test
# Run tests with coverage
./vendor/bin/pest --coverage
# Run specific test types
./vendor/bin/pest tests/Unit
./vendor/bin/pest tests/Feature
./vendor/bin/pest tests/IntegrationThis is a complete rewrite of the package with:
- โ Filament v4 Only: Clean, modern implementation
- โ Full Select Compatibility: Inherits ALL native Filament Select features
- โ Enhanced Performance: Optimized queries and N+1 prevention
- โ Comprehensive Tests: 52 tests with full coverage
- โ Clean Architecture: Separation of concerns with dedicated services
- โ Zero Breaking Changes: Drop-in replacement for standard Select
- โ Case-Insensitive Search: Automatic case-insensitive search across all database engines
- โ Multi-Tenancy Integration: Seamless compatibility with Filament's tenancy system
- โ Improved Error Handling: Better debugging and troubleshooting capabilities
- Case Sensitivity: Resolved case-sensitive search problems where "product" wouldn't match "Product Sales"
- Multi-Tenancy Conflicts: Fixed conflicts between manual tenant filtering and Filament's automatic tenancy system
- Query Optimization: Improved database query generation for better performance
- Automatic Tenant Scoping: Works seamlessly with Filament's tenant-aware resources
- Conflict Prevention: Prevents duplicate company/tenant filtering that caused search failures
- Best Practices Documentation: Comprehensive guide for multi-tenant applications
- Cross-Locale Search: Search "sales" and find both "Product Sales" and "Sales Discounts & Returns"
- Case-Insensitive: Search "product" and match "Product Sales" regardless of case
- Database Agnostic: Optimized queries for MySQL, PostgreSQL, and SQLite
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.