rdcstarr / laravel-multilanguage
A simple multilanguage package for Laravel with metadata support.
Installs: 21
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/rdcstarr/laravel-multilanguage
Requires
- php: ^8.0
- illuminate/contracts: ^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
Elegant package for managing multilanguage locale data in Laravel — with intelligent per‑language caching and a fluent API.
✨ Features
- 🌍 Multiple languages – easily switch & manage localized content
- 🔑 Key-value locale data – structured, namespaced keys per language
- ⚡ Smart cache – per-language forever cache with auto invalidation
- 📦 Batch operations – set or fetch many keys at once
- 🔄 Fluent API – expressive chaining (
localedata()->lang('en')->set(...)) - 🧩 Blade directives – simple templating helpers & conditionals
- 🗄️ Clean schema – normalized tables with FK constraints
📦 Installation
Install via Composer:
composer require rdcstarr/laravel-multilanguage
- (Optional) Publish migration files:
php artisan vendor:publish --provider="Rdcstarr\Multilanguage\MultilanguageServiceProvider" - Run migrations:
php artisan migrate
- (Recommended) Use the install command (runs migrations & seeds default languages):
php artisan multilanguage:install
Default seeded languages: English (en), Romanian (ro), French (fr).
🛠️ Artisan Commands
Install package (migrations + seed languages)
php artisan multilanguage:install [--force]
- --force : Skip interactive confirmation.
🔑 Usage
Set Values
// Single key per language localedata()->lang('en')->set('site.title', 'Welcome to our website'); localedata()->lang('ro')->set('site.title', 'Bine ai venit pe site-ul nostru'); // Batch (multiple keys) localedata()->lang('en')->setMany([ 'nav.home' => 'Home', 'nav.about' => 'About', 'nav.contact' => 'Contact', 'site.description'=> 'Best website ever', ]);
Get Values
$title = localedata()->lang('en')->get('site.title'); $titleWithDefault = localedata()->lang('es')->get('site.title', 'Default Title'); // Multiple $navigation = localedata()->lang('en')->getMany(['nav.home', 'nav.about', 'nav.contact']); // All for a language $allEnglish = localedata()->lang('en')->all(); // Current app locale (app()->getLocale()) $localized = localedata()->get('site.title', 'Fallback');
Working with Languages
use Rdcstarr\Multilanguage\Models\Language; // Create a language Language::create([ 'name' => 'Spanish', 'code' => 'es', 'flag' => '🇪🇸' ]); // Retrieve $languages = Language::all(); $english = Language::where('code', 'en')->first(); // Seed multiple values dynamically foreach (['en','ro','fr','es'] as $lang) { localedata()->lang($lang)->set("demo.message", "Message for {$lang}"); }
Facade
use Rdcstarr\Multilanguage\Facades\LocaleData; LocaleData::lang('en')->set('app.name', 'My App'); LocaleData::lang('ro')->set('app.name', 'Aplicația Mea'); $appName = LocaleData::lang('en')->get('app.name');
Helper
// Manager instance localedata(); // same as app('localedata') // Direct access (current locale) $title = localedata('site.title', 'Default Title');
Extra Operations
localedata()->lang('en')->has('site.title'); // existence localedata()->lang('en')->forget('old.unused.key'); // delete one localedata()->lang('en')->flushCache(); // clear cache for one language localedata()->flushAllCache(); // clear cache for all languages
🎨 Blade Directives
{{-- Current locale value (with optional default) --}}
@localedata('site.title', 'Default Title')
{{-- Specific language --}}
@localedataForLang('"en"', '"site.title"', '"Default"')
{{-- Conditional (current locale) --}}
@hasLocaledata('site.title')
<h1>{{ localedata('site.title') }}</h1>
@endhasLocaledata
{{-- Conditional (specific language) --}}
@hasLocaledataForLang('"ro"', '"site.title"')
<h1>{{ localedata()->lang('ro')->get('site.title') }}</h1>
@endhasLocaledataForLang
Placeholders
// welcome.user => "Hello :NAME, your :type_plural are ready! :message_limit"; $result = localedata()->get('welcome.user')->placeholders([ 'name' => 'john doe', 'type' => 'order', 'message' => 'This is a very long message that will be truncated at 50 characters automatically', ]); // Result: "Hello JOHN DOE, your orders are ready! This is a very long message that will be truncat..."
💡 Examples
Website Content
localedata()->lang('en')->setMany([ 'home.hero.title' => 'Welcome to Our Platform', 'home.hero.subtitle' => 'The best solution for your business', ]); localedata()->lang('ro')->setMany([ 'home.hero.title' => 'Bine ai venit pe Platforma Noastră', 'home.hero.subtitle' => 'Cea mai bună soluție pentru afacerea ta', ]);
SEO Meta
localedata()->lang('en')->setMany([ 'seo.home.title' => 'Home - Best Platform Ever', 'seo.home.description' => 'Discover our amazing platform features', ]); <title>{{ localedata()->get('seo.' . request()->route()->getName() . '.title', 'Default') }}</title>
User Personalization
$userId = auth()->id(); localedata()->lang('en')->setMany([ "user.{$userId}.welcome" => 'Welcome back!', ]); localedata()->lang('ro')->setMany([ "user.{$userId}.welcome" => 'Bine ai revenit!', ]);
Product Catalog
$productId = 123; localedata()->lang('en')->setMany([ "product.{$productId}.name" => 'Premium Laptop', "product.{$productId}.description" => 'High-performance laptop', ]);
🎯 Placeholders
The package includes powerful placeholder support with default date/time placeholders and the ability to add custom ones.
Default Placeholders (Available Automatically)
Date & Time:
:year- 2025:month- 10:month_name- October:month_short- Oct:day- 20:day_name- Monday:day_short- Mon:quarter- 4:hour- 14 (24h):hour_12- 2 (12h):minute- 30:second- 45:am_pm- AM/PM:am_pm_lower- am/pm
Complete Formats:
:date- 2025-10-20:date_formatted- 20/10/2025:date_us- 10/20/2025:time- 14:30:45:time_short- 14:30:datetime- 2025-10-20 14:30:45:timestamp- Unix timestamp:iso- 2025-10-20T14:30:45+00:00:ago- "1 second ago" (human readable)
App Info:
:app_name- Application name from config:app_env- Environment (production, local, etc.):app_url- Application URL
Other:
:week- Week number:day_of_year- Day of year (1-365):days_in_month- Days in current month:timezone- UTC
Usage in Translations
localedata()->set('footer.copyright', '© :year :app_name. All rights reserved.'); // Output: © 2025 My App. All rights reserved. localedata()->set('report.generated', 'Generated on :date at :time_short'); // Output: Generated on 2025-10-20 at 14:30 localedata()->set('greetings.time', 'It is :time, :day_name :month_name :day, :year'); // Output: It is 14:30:45, Monday October 20, 2025
Custom Placeholders
Method 1: Config File (Recommended for static values)
Publish config:
php artisan vendor:publish --tag=multilanguage-config
Edit config/multilanguage.php:
return [ 'placeholders' => [ 'company_name' => env('COMPANY_NAME', 'My Company'), 'support_email' => 'support@example.com', 'support_phone' => '+40 123 456 789', 'copyright_year' => date('Y'), 'vat_number' => 'RO12345678', ], ];
Usage:
localedata()->set('footer.copyright', '© :copyright_year :company_name. All rights reserved.'); localedata()->set('contact.info', 'Contact us at :support_email or call :support_phone');
Method 2: In AppServiceProvider (For dynamic values)
use Rdcstarr\Multilanguage\LocaleDataPlaceholders; use App\Models\User; public function boot() { // Set multiple placeholders at once LocaleDataPlaceholders::setCustomPlaceholders([ 'total_users' => User::count(), 'active_users' => User::where('active', true)->count(), 'latest_post' => Post::latest()->first()?->title, 'site_status' => Cache::get('site_status', 'online'), ]); // Or add individually LocaleDataPlaceholders::addCustomPlaceholder('active_sessions', Session::where('active', true)->count()); }
Usage:
localedata()->set('dashboard.stats', 'Platform Stats: :total_users users, :active_users active'); // Output: Platform Stats: 1,234 users, 567 active
Method 3: Helper Function
// Single placeholder set_placeholder('username', 'John Doe'); // Multiple placeholders set_placeholder([ 'product_name' => 'Super Widget', 'product_price' => '$99.99', ]);
Method 4: Runtime (Context-specific)
// In controller public function show(User $user) { $greeting = localedata()->get('user.welcome', '', [ 'username' => $user->name, 'last_login' => $user->last_login_at->diffForHumans(), ]); return view('profile', compact('greeting')); }
Translation:
localedata()->set('user.welcome', 'Hello :Username! Last seen: :last_login'); // Output: Hello John Doe! Last seen: 2 hours ago
Placeholder Transformations
All placeholders (custom and default) support automatic transformations:
set_placeholder('company_name', 'my company'); // Available transformations: :company_name // my company :Company_name // My company (ucfirst) :COMPANY_NAME // MY COMPANY (uppercase) :company_name_camel // myCompany :company_name_studly // MyCompany :company_name_snake // my_company :company_name_kebab // my-company :company_name_slug // my-company :company_name_plural // my companies :company_name_singular // my company :company_name_upper // MY COMPANY :company_name_lower // my company :company_name_title // My Company
Placeholder Priority
Placeholders are applied in the following order (last one overrides):
- Default placeholders (date, time, app info)
- Config placeholders (
config/multilanguage.php) - Custom static placeholders (
LocaleDataPlaceholders::setCustomPlaceholders()) - Runtime placeholders (parameter in
get()method)
Safe Placeholder Usage with User Input
⚠️ Important: When using placeholders with user-generated content, always escape the input to prevent unwanted placeholder replacement.
The Problem
// User searches for ":year" $query = ':year'; // Translation localedata()->set('search.meta_title', 'Search results for: :query'); // Without escaping $title = localedata()->get('search.meta_title')->placeholders(['query' => $query]); // Output: "Search results for: 2025" ❌ WRONG! :year in query was replaced
The Solution: Use escape_placeholders()
// In Controller public function search(Request $request) { $query = $request->input('q'); // ESCAPE user query $safeQuery = escape_placeholders($query); $title = localedata()->get('search.meta_title')->placeholders([ 'query' => $safeQuery ]); return view('search.results', compact('title', 'query')); }
Now it works correctly:
$userInput = ':year in review'; $safe = escape_placeholders($userInput); $title = localedata()->get('search.meta_title')->placeholders(['query' => $safe]); // Output: "Search results for: :year in review" ✅ CORRECT!
When to Escape
✅ ALWAYS escape for:
- User input (search queries, comments, usernames)
- User-generated titles/descriptions
- Any user-provided data in placeholders
❌ NO need to escape for:
- Admin-controlled database values
- Code constants
- System-calculated values (counters, dates)
- Default placeholders (
:year,:app_name, etc.)
Alternative Escape Methods
// Method 1: Helper function (recommended) $safe = escape_placeholders($userInput); // Method 2: Static method use Rdcstarr\Multilanguage\LocaleDataPlaceholders; $safe = LocaleDataPlaceholders::escape($userInput); // Method 3: Facade use Rdcstarr\Multilanguage\Facades\Placeholders; $safe = Placeholders::escape($userInput);
Practical Examples
Search Results:
Route::get('/search', function (Request $request) { $query = escape_placeholders($request->input('q')); $title = localedata()->get('search.meta_title')->placeholders([ 'query' => $query, // Safe! ]); return view('search', compact('title')); });
User Profile:
public function show(User $user) { $metaTitle = localedata()->get('profile.meta_title')->placeholders([ 'username' => escape_placeholders($user->name), // Escape username // :app_name and :year are safe (default placeholders) ]); return view('profile.show', compact('user', 'metaTitle')); }
Comment System:
public function store(Request $request, Post $post) { $notification = localedata()->get('comment.notification')->placeholders([ 'username' => escape_placeholders(auth()->user()->name), 'comment_preview' => escape_placeholders(Str::limit($request->content, 50)), ]); }
For more details on safe placeholder usage, see the Safe Placeholder Usage with User Input section above.
Real-World Examples
Footer Copyright
// Config 'placeholders' => [ 'company_name' => 'Acme Corporation', 'year' => date('Y'), ], // Translation localedata()->set('footer.text', '© :year :company_name. Made with ❤️ in Romania'); // Output: © 2025 Acme Corporation. Made with ❤️ in Romania
Live Statistics
// AppServiceProvider LocaleDataPlaceholders::setCustomPlaceholders([ 'total_users' => Cache::remember('stats.users', 3600, fn() => User::count()), 'total_posts' => Cache::remember('stats.posts', 3600, fn() => Post::count()), 'total_comments' => Cache::remember('stats.comments', 3600, fn() => Comment::count()), ]); // Translation localedata()->set('stats.overview', 'We have :total_users users who created :total_posts posts with :total_comments comments!');
Personalized Messages
// In controller $message = localedata()->get('user.welcome', '', [ 'username' => auth()->user()->name, 'unread_count' => auth()->user()->unreadNotifications()->count(), ]); // Translation localedata()->set('user.welcome', 'Welcome back, :Username! You have :unread_count new notifications.'); // Output: Welcome back, John! You have 5 new notifications.
Product Catalog
// AppServiceProvider $productId = 123; LocaleDataPlaceholders::setCustomPlaceholders([ 'site_name' => config('app.name'), 'total_products' => Product::count(), ]); // Translation localedata()->set('home.hero', 'Welcome to :site_name! We have :total_products amazing products.');
Email Subject
// Mailable public function build() { $userName = escape_placeholders($this->user->name); return $this->subject( localedata()->get('email.welcome.subject')->placeholders([ 'username' => $userName, ]) ); } // Translation localedata()->set('email.welcome.subject', 'Welcome :username! Your account on :app_name');
Closure Support
For complex logic, use Closures:
LocaleDataPlaceholders::addCustomPlaceholder('dynamic_greeting', function($content) { $hour = now()->hour; if ($hour < 12) return "Good morning, $content"; if ($hour < 18) return "Good afternoon, $content"; return "Good evening, $content"; }); // In translation with XML tag localedata()->set('greeting', '<dynamic_greeting>:username</dynamic_greeting>, welcome to our site!');
Debugging Placeholders
// View all custom placeholders $custom = LocaleDataPlaceholders::getCustomPlaceholders(); dd($custom); // Clear custom placeholders (useful in tests) LocaleDataPlaceholders::clearCustomPlaceholders();
Best Practices
- Use config for static values (company name, contact info)
- Use static placeholders for cached values (user counts, statistics)
- Use runtime placeholders for context-specific data (user names, IDs)
- Cache expensive queries when using in placeholders
- Always escape user input before using in placeholders
- Test transformations to find the best fit for your use case
⚡ Performance
- Per-language cache buckets
- Forever cache via rememberForever
- Automatic invalidation on writes / deletes
- Minimal queries (one load per language as needed)
- Batch write operations
🧪 Testing
composer test
📖 Resources
- Changelog for recent changes.
👥 Credits
📜 License
- License for more information.