Internationalization (i18n) Guide
This document describes how to add multi-language support to your Tenanto-based application.
Overview
Tenanto comes with a prepared i18n infrastructure that makes it easy to add translations for your application. The system supports:
- Application-level locale - Default language for the entire application
- Tenant-level locale - Each tenant can have their own preferred language
- User preference - Users can override the locale via session or browser settings
- Automatic detection - Browser Accept-Language header detection
Directory Structure
lang/
├── en/ # English translations (default)
│ ├── auth.php # Authentication pages
│ ├── billing.php # Subscription & payment
│ ├── profile.php # User profile
│ ├── dashboard.php # Dashboard & general UI
│ ├── notifications.php # Email notifications
│ ├── admin.php # Filament admin panel
│ ├── api.php # API responses
│ ├── errors.php # Error pages
│ ├── common.php # Shared UI elements
│ ├── onboarding.php # Onboarding wizard
│ └── validation.php # Form validation messages
├── cs/ # Czech translations (example)
│ └── ...
└── de/ # German translations (example)
└── ...
Adding a New Language
Step 1: Copy English translations
cp -r lang/en lang/cs # Replace 'cs' with your locale code
Step 2: Translate the files
Edit each .php file in your new language directory. For example, lang/cs/auth.php:
<?php
return [
'welcome_back' => 'Vítejte zpět',
'sign_in' => 'Přihlásit se',
'email' => 'E-mailová adresa',
'password' => 'Heslo',
// ... translate all keys
];
Step 3: Register the locale
Edit app/Http/Middleware/SetTenantLocale.php and add your locale to the SUPPORTED_LOCALES array:
private const SUPPORTED_LOCALES = [
'en',
'cs', // Czech - add your locale here
];
Also update the getSupportedLocales() method:
public static function getSupportedLocales(): array
{
return [
'en' => 'English',
'cs' => 'Čeština', // Add display name
];
}
Step 4: Set as default (optional)
To change the default application locale, edit .env:
APP_LOCALE=cs
APP_FALLBACK_LOCALE=en
Tenant-Level Language Settings
Each tenant can have their own preferred locale stored in their settings.
Setting tenant locale
$tenant->update([
'settings' => array_merge($tenant->settings ?? [], [
'locale' => 'cs',
]),
]);
In the onboarding wizard
The locale preference can be set during onboarding (Step 3: Preferences). The OnboardingWizard Livewire component handles this automatically.
Via Filament Admin
System administrators can set tenant locale via the Tenant resource in Filament admin panel (Settings section).
How Locale Resolution Works
The SetTenantLocale middleware determines the locale in this priority order:
- Tenant setting -
$tenant->settings['locale'] - Session preference -
session('locale') - Browser detection -
Accept-Languageheader - Application default -
config('app.locale')
Using Translations in Code
In Blade Templates
{{-- Using the __() helper --}}
<h1>{{ __('auth.welcome_back') }}</h1>
{{-- With parameters --}}
<p>{{ __('dashboard.welcome', ['name' => $user->name]) }}</p>
{{-- Pluralization --}}
<span>{{ trans_choice('dashboard.minutes_ago', $count) }}</span>
{{-- Using @lang directive --}}
@lang('common.save')
In PHP Code
// Simple translation
$message = __('api.login_success');
// With parameters
$message = __('billing.plan_changed', ['plan' => $planName]);
// In notifications
return (new MailMessage)
->subject(__('notifications.subscription_created_subject', [
'app' => config('app.name'),
]))
->greeting(__('notifications.subscription_created_greeting'));
In Filament Resources
Filament has its own translation system. For custom labels:
public static function getNavigationLabel(): string
{
return __('admin.tenants');
}
public static function getModelLabel(): string
{
return __('admin.tenant');
}
Translation Key Conventions
Follow these conventions for consistent translation keys:
| Pattern | Example | Use Case |
|---|---|---|
{domain}.{key} |
auth.sign_in |
Simple labels |
{domain}.{key}_title |
billing.plans_title |
Page titles |
{domain}.{key}_help |
auth.forgot_password_help |
Help text |
{domain}.{key}_button |
auth.reset_password_button |
Button labels |
{domain}.{key}_success |
billing.subscription_created |
Success messages |
{domain}.{key}_error |
billing.checkout_error |
Error messages |
Email Notifications
All email notifications use translations from lang/en/notifications.php. When adding a new notification:
- Add translation keys to
notifications.php - Use
__()helper in your notification class - Include placeholders for dynamic content
Example:
// In notifications.php
'welcome_subject' => 'Welcome to :app!',
'welcome_line1' => 'Hello :name, thanks for joining us.',
// In Notification class
return (new MailMessage)
->subject(__('notifications.welcome_subject', ['app' => config('app.name')]))
->line(__('notifications.welcome_line1', ['name' => $user->name]));
API Response Messages
API responses use translations from lang/en/api.php:
return $this->success(
data: $resource,
message: __('api.project_created')
);
return $this->error(
message: __('api.not_found'),
code: 404
);
Adding Language Selector
To add a language selector to your UI:
<select wire:change="setLocale($event.target.value)">
@foreach(\App\Http\Middleware\SetTenantLocale::getSupportedLocales() as $code => $name)
<option value="{{ $code }}" @selected(app()->getLocale() === $code)>
{{ $name }}
</option>
@endforeach
</select>
In your Livewire component:
public function setLocale(string $locale): void
{
session(['locale' => $locale]);
$this->redirect(request()->header('Referer'));
}
Date & Number Formatting
For locale-aware date and number formatting:
// Dates (using Carbon)
$date->translatedFormat('F j, Y'); // "January 15, 2025" or "15. ledna 2025"
$date->diffForHumans(); // "2 hours ago" or "před 2 hodinami"
// Numbers
number_format($amount, 2,
__('common.decimal_separator'),
__('common.thousands_separator')
);
// Currency (consider using money package)
// or use Intl extension if available
$formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$formatter->formatCurrency($amount, 'USD');
Testing Translations
Check for missing translations
# Find hardcoded strings that should be translated
grep -r "\"[A-Z][a-z]" resources/views --include="*.blade.php" | grep -v "__("
Test with different locales
public function test_displays_in_czech(): void
{
App::setLocale('cs');
$response = $this->get('/login');
$response->assertSee('Přihlásit se');
}
FilamentPHP Translations
Filament has its own language files. To publish and customize:
php artisan vendor:publish --tag=filament-panels-translations
php artisan vendor:publish --tag=filament-forms-translations
php artisan vendor:publish --tag=filament-tables-translations
php artisan vendor:publish --tag=filament-notifications-translations
This creates files in lang/vendor/filament-* that you can customize.
Best Practices
- Never hardcode user-facing strings - Always use
__()helper - Use descriptive keys -
auth.login_buttonnotauth.btn1 - Group related translations - Keep billing strings in
billing.php - Include placeholders - Use
:variablefor dynamic content - Test with long strings - German translations are often longer than English
- Consider RTL - If supporting Arabic/Hebrew, test RTL layouts
- Keep fallback locale - Always have complete English translations as fallback
Troubleshooting
Translations not showing
- Check file exists in correct location (
lang/{locale}/{file}.php) - Verify locale is in
SUPPORTED_LOCALESarray - Clear config cache:
php artisan config:clear - Check for syntax errors in translation files
Wrong locale being used
- Check middleware priority in
bootstrap/app.php - Verify tenant settings are being read correctly
- Check session is starting before locale middleware runs
Missing pluralization
Ensure you're using trans_choice() for pluralized strings:
// In translation file
'items' => '{0} No items|{1} One item|[2,*] :count items',
// In code
trans_choice('common.items', $count, ['count' => $count]);
Related Documentation
- Multi-Tenancy Architecture - Tenant locale settings
- FilamentPHP Customization - Panel localization
- Deployment Guide - Production configuration