Système de Traduction Multilingue

Documentation complète du système de traduction multilingue, couvrant la traduction de l'interface, du contenu en base de données, l'intégration avec Filament, le cache, les modules, et les helpers.

Version 1.3 Dernière mise à jour : 2026-01-25
204+ Tests > 88% Couverture

Guides disponibles

🚀 Démarrage rapide

Guide pour démarrer en 5 minutes

Voir le guide →

⚙️ Configuration

Guide complet de configuration

Voir le guide →

🌍 Ajouter une langue

Comment ajouter une nouvelle locale

Voir le guide →

📦 Rendre un modèle traduisible

Guide étape par étape

Voir le guide →

💾 Traduction BDD

Guide complet pour les modèles

Voir le guide →

🎨 Filament

Intégration avec Filament v5

Voir le guide →

🛠️ Helpers

Tous les helpers disponibles

Voir le guide →

⚡ Cache

Système de cache des traductions

Voir le guide →

📁 Modules

Traductions dans les modules

Voir le guide →

❓ FAQ

Questions fréquentes

Voir la FAQ →

Vue d'ensemble

Traduction de l'Interface

Fichiers JSON pour les textes statiques (boutons, labels, messages d'erreur).

  • • Fichiers lang/*.json
  • • Support de fr et es
  • • Laravel natif

Traduction du Contenu

Stockage des traductions en JSON dans les colonnes de la base de données.

  • • Package spatie/laravel-translatable
  • • Format JSON dans les colonnes
  • • Trait ConditionallyTranslatable

Gestion dans Filament

Interface d'administration pour gérer les traductions avec sélecteur de langue.

  • • Helper TranslatableField
  • • Action LocaleAction
  • • Compatible Filament v5

Architecture du système

Composants du système

1. Traduction de l'Interface

Utilise le système natif de Laravel avec des fichiers JSON pour les textes statiques.

  • • Fichiers : lang/fr.json, lang/es.json
  • • Utilisation : {{ __('messages.welcome') }}
  • • Package : Laravel natif

2. Traduction du Contenu (BDD)

Stockage des traductions en JSON dans les colonnes de la base de données.

  • • Package : spatie/laravel-translatable: ^6.12
  • • Format : JSON dans les colonnes
  • • Trait : ConditionallyTranslatable
  • • Configuration : config/translatable.php

3. Gestion dans Filament

Solution personnalisée pour Filament v5 avec helpers et actions.

  • • Helper : App\Filament\Support\TranslatableField
  • • Action : App\Filament\Support\LocaleAction
  • • Sélecteur de langue dans le header

4. Routing et Localisation

URLs localisées avec détection automatique de la langue.

  • • Package : mcamara/laravel-localization
  • • Middleware : SetLocaleMiddleware
  • • URLs : /fr/blog, /es/blog

5. Système de Cache

Cache des traductions JSON et BDD pour améliorer les performances.

  • • Service : TranslationCacheService
  • • Commande : translate:clear-cache
  • • Invalidation automatique via Observer
  • • Support Redis, Memcached, File

6. Traductions dans les Modules

Support pour charger des traductions depuis les modules spécifiques.

  • • Structure : app/Specifics/{Module}/lang/
  • • Service Provider par module
  • • Chargement conditionnel
  • • Exemple : Module Blog

7. Helpers et Utilitaires

Helpers globaux et classe TranslationHelper pour faciliter l'utilisation.

  • • Helpers globaux : current_locale(), trans_model(), etc.
  • • Classe : TranslationHelper
  • • Routes localisées : localized_route()
  • • Gestion des locales

Configuration

1. Configuration globale

Active ou désactive tout le système de traduction.

Fichier : .env

# Activer toutes les traductions
TRANSLATIONS_ENABLED=true

# Désactiver toutes les traductions
TRANSLATIONS_ENABLED=false

2. Configuration par composant

Active ou désactive chaque composant indépendamment.

Fichier : .env

# Activer globalement
TRANSLATIONS_ENABLED=true

# Interface utilisateur (fichiers JSON)
TRANSLATIONS_INTERFACE_ENABLED=true

# Base de données (Spatie Translatable)
TRANSLATIONS_DATABASE_ENABLED=true

# Routing (URLs localisées)
TRANSLATIONS_ROUTING_ENABLED=true

# Filament (plugin admin)
TRANSLATIONS_FILAMENT_ENABLED=true

Cas 1 : Interface uniquement

TRANSLATIONS_ENABLED=true
TRANSLATIONS_INTERFACE_ENABLED=true
TRANSLATIONS_DATABASE_ENABLED=false
TRANSLATIONS_ROUTING_ENABLED=false
TRANSLATIONS_FILAMENT_ENABLED=false

→ Seuls les textes de l'interface sont traduits

Cas 2 : BDD sans routing

TRANSLATIONS_ENABLED=true
TRANSLATIONS_INTERFACE_ENABLED=true
TRANSLATIONS_DATABASE_ENABLED=true
TRANSLATIONS_ROUTING_ENABLED=false
TRANSLATIONS_FILAMENT_ENABLED=true

→ Contenu traduisible en BDD, mais URLs non localisées

3. Configuration par modèle

Active ou désactive la traduction pour chaque modèle spécifique.

Fichier : .env

# Activer pour des modèles spécifiques
TRANSLATIONS_MODEL_POST=true
TRANSLATIONS_MODEL_PRODUCT=true
TRANSLATIONS_MODEL_FEEDBACK=true
TRANSLATIONS_MODEL_POLL=true
TRANSLATIONS_MODEL_ROADMAPITEM=true

Traduction du contenu en base de données

Contenu du guide :

  • • Vue d'ensemble et modèles traduisibles (8 modèles migrés)
  • • Utilisation dans les modèles avec le trait ConditionallyTranslatable
  • • Migrations et structure des colonnes JSON
  • • Commande de migration des données existantes (translate:migrate-data)
  • • Actions de migration pour chaque modèle
  • • Factories et Seeders adaptés
  • • Intégration avec Filament (TranslatableField, LocaleAction)
  • • Tests complets (106 tests, 344 assertions)
  • • Activation/désactivation par modèle
  • • Bonnes pratiques et dépannage

Modèles traduisibles

Les modèles suivants ont été migrés vers le système de traduction :

Blog

  • Post : title, slug, excerpt, content

Shop

  • Product : name, slug, description

Pages

  • Page : title, slug, content

Shares

  • Category : name, slug, description
  • Tag : name, slug

FeedbackHub

  • Feedback : title, description
  • RoadmapItem : title, description
  • Poll : title

Commande de migration

Utilisez la commande translate:migrate-data pour migrer les données existantes vers le format JSON.

# Migrer tous les modèles
php artisan translate:migrate-data

# Migrer un modèle spécifique
php artisan translate:migrate-data --model=Post

# Mode dry-run (simulation)
php artisan translate:migrate-data --dry-run

Note : La commande génère un rapport détaillé avec les statistiques de migration (enregistrements traités, migrés, ignorés, erreurs).

Utilisation dans les modèles

Trait ConditionallyTranslatable

Utilisez le trait ConditionallyTranslatable au lieu de HasTranslations directement pour respecter le système d'activation/désactivation.

use App\Specifics\Shares\Models\Concerns\ConditionallyTranslatable;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use ConditionallyTranslatable;

    protected function getTranslatableFields(): array
    {
        return ['title', 'excerpt', 'content', 'slug'];
    }
}

Méthodes disponibles

Récupération des traductions

// Récupérer une traduction spécifique
$post->getTranslation('title', 'fr'); // "Titre en français"
$post->getTranslation('title', 'es'); // "Título en español"

// Récupérer selon la locale courante
$post->title; // Retourne selon app()->getLocale()

// Récupérer toutes les traductions
$post->getTranslations('title');
// ['fr' => 'Titre en français', 'es' => 'Título en español']

Sauvegarde des traductions

// Définir une traduction
$post->setTranslation('title', 'fr', 'Titre en français');
$post->setTranslation('title', 'es', 'Título en español');
$post->save();

// Définir plusieurs traductions
$post->setTranslations('title', [
    'fr' => 'Titre en français',
    'es' => 'Título en español',
]);
$post->save();

Vérification des traductions

// Vérifier si une traduction existe
$post->hasTranslation('title', 'fr'); // true/false

// Fallback automatique
$post->getTranslation('title', 'es', true);
// Utilise le fallback si 'es' n'existe pas

Utilisation dans Filament

Helper TranslatableField

Utilisez le helper TranslatableField pour créer des champs traduisibles dans vos formulaires Filament.

use App\Filament\Support\TranslatableField;
use Filament\Schemas\Schema;

class ProductForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                // Champ texte traduisible
                ...TranslatableField::makeTextInput('name', 'Nom',
                    fn ($field) => $field->required()->maxLength(255)
                ),

                // Champ textarea traduisible
                ...TranslatableField::makeTextarea('description', 'Description',
                    fn ($field) => $field->required()->rows(5)
                ),
            ]);
    }
}

Action LocaleAction

Ajoutez le sélecteur de langue dans le header de vos pages Filament.

use App\Filament\Support\LocaleAction;
use Filament\Resources\Pages\EditRecord;

class EditProduct extends EditRecord
{
    public ?string $locale = null;

    public function mount(int | string $record): void
    {
        parent::mount($record);
        $this->locale = current_locale();
    }

    protected function getHeaderActions(): array
    {
        return array_merge(
            LocaleAction::make() ?? [],
            parent::getHeaderActions()
        );
    }
}

Gestion des données dans les pages Filament standard

Note : Cette section concerne les pages Filament standard (sans composants Livewire imbriqués). Pour les pages avec composants imbriqués (comme EditPost), voir la section "Architecture simplifiée pour composants Livewire imbriqués" ci-dessous.

Utilisez les méthodes mutateFormDataBeforeFill et mutateFormDataBeforeSave pour transformer les données.

protected function mutateFormDataBeforeFill(array $data): array
{
    // Transformer JSON en champs séparés (title_fr, title_es)
    $translatableFields = ['title', 'description'];

    foreach ($translatableFields as $field) {
        if (isset($data[$field]) && is_string($data[$field])) {
            $translations = json_decode($data[$field], true);
            foreach (available_locales() as $locale) {
                $data["{$field}_{$locale}"] = $translations[$locale] ?? null;
            }
            unset($data[$field]);
        }
    }

    return $data;
}

protected function mutateFormDataBeforeSave(array $data): array
{
    // Supprimer les champs locaux (title_fr, title_es)
    $translatableFields = ['title', 'description'];
    foreach ($translatableFields as $field) {
        foreach (available_locales() as $locale) {
            unset($data["{$field}_{$locale}"]);
        }
    }

    return $data;
}

protected function afterSave(): void
{
    // Sauvegarder les traductions
    $translatableFields = ['title', 'description'];
    foreach ($translatableFields as $field) {
        foreach (available_locales() as $locale) {
            $value = $this->data["{$field}_{$locale}"] ?? null;
            if ($value !== null) {
                $this->record->setTranslation($field, $locale, $value);
            }
        }
    }
    $this->record->save();
    $this->fillForm();
}

Prise en charge des langues dans Filament

Sélecteur de langue (LocaleAction)

Le système fournit un sélecteur de langue intégré qui s'affiche dans le header des pages Filament. Il permet de changer de langue directement depuis l'interface d'administration.

Fonctionnalités :

  • • Badge affichant la langue actuelle avec drapeau
  • • Menu déroulant pour changer de langue
  • • Mise à jour automatique des formulaires lors du changement
  • • Synchronisation avec la session et l'application

Comportement lors du changement de langue

Lorsqu'un utilisateur change de langue via LocaleAction :

  1. Mise à jour de la locale : La locale de l'application et de la session est mise à jour
  2. Événement Livewire : Pour les pages avec composants imbriqués (EditPost, CreatePost), l'événement locale-changed est dispatché
  3. Sauvegarde automatique : Le composant imbriqué sauvegarde les valeurs actuelles dans $translations avant le changement
  4. Affichage nouvelle locale : Les champs traduisibles affichent les valeurs de la nouvelle locale depuis $translations
  5. Mise à jour du heading : Le titre de la page se met à jour avec la traduction correspondante

Exemple de flux :

// 1. Utilisateur modifie title en français → stocké dans $translations['fr']['title']
// 2. Utilisateur clique sur "Espagnol" dans LocaleAction
// 3. LocaleAction dispatch 'locale-changed' avec locale='es'
// 4. Composant Edit::onLocaleChanged('es') :
//    - Sauvegarde $this->title dans $translations['fr']['title']
//    - Change $this->locale = 'es'
//    - Affiche $translations['es']['title'] dans $this->title
// 5. L'utilisateur voit maintenant le titre espagnol (vide ou depuis BDD)
// 6. À l'enregistrement, TOUTES les traductions sont sauvegardées

Système de fallback automatique

Le système utilise automatiquement un fallback intelligent pour les traductions manquantes via la fonction trans_model() :

  1. Locale demandée : Essaie d'abord la langue sélectionnée
  2. Langue par défaut : Si vide, utilise la langue par défaut (fr)
  3. Autres langues : Si toujours vide, essaie toutes les autres langues disponibles
  4. Null : Retourne null uniquement si aucune traduction n'existe

Exemple :

// Article avec seulement le titre français
$post->setTranslation('title', 'fr', 'Mon article');

// Ces appels retourneront tous "Mon article"
trans_model($post, 'title', 'fr');  // "Mon article"
trans_model($post, 'title', 'es');  // "Mon article" (fallback vers fr)
trans_model($post, 'title');        // "Mon article" (locale actuelle ou fallback)

Initialisation de la locale

Toujours initialiser la propriété $locale dans la méthode mount() :

public ?string $locale = null;

public function mount(int|string $record): void
{
    parent::mount($record);
    $this->locale = current_locale(); // Important !
}

Cela garantit que la locale est synchronisée avec l'application, que les champs traduisibles affichent la bonne langue au chargement, et que le sélecteur de langue affiche la bonne langue active.

Architecture simplifiée pour composants Livewire imbriqués

Pour les composants Livewire imbriqués (comme EditPost ou CreatePost), utilisez un système simplifié avec un array $translations pour stocker toutes les traductions :

use Livewire\Attributes\On;
use Livewire\Component;

class Edit extends Component
{
    public Post $post;
    public string $title = '';
    public string $slug = '';
    public ?string $locale = null;

    /**
     * Stocke les traductions pour chaque locale.
     * Format: ['fr' => ['title' => '...', 'slug' => '...'], 'es' => [...]]
     */
    public array $translations = [];

    public function mount(Post $post, ?string $locale = null): void
    {
        $this->post = $post;
        $this->locale = $locale ?? current_locale();

        // Charger TOUTES les traductions depuis la BDD
        $this->loadAllTranslations();

        // Afficher les champs pour la locale courante
        $this->displayCurrentLocale();
    }

    /**
     * Charge TOUTES les traductions depuis la BDD.
     */
    protected function loadAllTranslations(): void
    {
        $locales = available_locales();
        $fields = ['title', 'slug', 'excerpt', 'content'];

        foreach ($locales as $loc) {
            $this->translations[$loc] = [];
            foreach ($fields as $field) {
                $this->translations[$loc][$field] =
                    $this->post->getTranslation($field, $loc, false) ?? '';
            }
        }
    }

    /**
     * Affiche les valeurs de la locale courante dans les champs.
     */
    protected function displayCurrentLocale(): void
    {
        $loc = $this->locale;
        $this->title = $this->translations[$loc]['title'] ?? '';
        $this->slug = $this->translations[$loc]['slug'] ?? '';
        $this->excerpt = $this->translations[$loc]['excerpt'] ?? '';
        $this->content = $this->translations[$loc]['content'] ?? '';
    }

    /**
     * Sauvegarde les valeurs actuelles dans $translations pour la locale courante.
     */
    protected function saveCurrentLocaleToTranslations(): void
    {
        $loc = $this->locale;
        $this->translations[$loc]['title'] = $this->title;
        $this->translations[$loc]['slug'] = $this->slug;
        $this->translations[$loc]['excerpt'] = $this->excerpt;
        $this->translations[$loc]['content'] = $this->content;
    }

    /**
     * Appelé quand la locale change depuis le parent.
     */
    #[On('locale-changed')]
    public function onLocaleChanged(string $locale): void
    {
        // 1. Sauvegarder les valeurs actuelles dans $translations
        $this->saveCurrentLocaleToTranslations();

        // 2. Changer la locale
        $this->locale = $locale;
        app()->setLocale($locale);
        session()->put('locale', $locale);

        // 3. Afficher les valeurs de la nouvelle locale
        $this->displayCurrentLocale();
    }

    /**
     * Appelé quand un champ traduisible est modifié.
     * Sauvegarde automatiquement dans $translations.
     */
    public function updated($property): void
    {
        $translatableFields = ['title', 'slug', 'excerpt', 'content'];
        if (in_array($property, $translatableFields)) {
            $this->translations[$this->locale][$property] = $this->{$property};
        }
    }

    public function save(): void
    {
        // Sauvegarder les valeurs actuelles avant l'enregistrement
        $this->saveCurrentLocaleToTranslations();

        // Exécuter l'action de mise à jour avec les données de la locale courante
        // ... (votre logique de sauvegarde)

        // Sauvegarder TOUTES les traductions dans la BDD
        $this->saveAllTranslations();
    }

    /**
     * Sauvegarde TOUTES les traductions dans la BDD.
     */
    protected function saveAllTranslations(): void
    {
        $fields = ['title', 'slug', 'excerpt', 'content'];

        foreach ($this->translations as $locale => $data) {
            foreach ($fields as $field) {
                $value = $data[$field] ?? null;
                if ($value !== null && $value !== '') {
                    $this->post->setTranslation($field, $locale, $value);
                }
            }
        }

        $this->post->save();
    }
}

Avantages de cette architecture :

  • Simplicité : Pas de communication asynchrone entre composants
  • Isolation : Chaque locale a son propre espace dans $translations
  • Fiabilité : Pas de copie accidentelle entre locales
  • Performance : Chargement unique de toutes les traductions au mount
  • Maintenabilité : Code beaucoup plus simple et lisible

Flux utilisateur : L'utilisateur modifie un champ → stocké dans $translations[$locale][$field] → changement de locale → affichage de $translations[$newLocale][$field] → enregistrement → toutes les traductions sont sauvegardées.

Traduction de l'interface

Fichiers JSON

Les traductions de l'interface sont stockées dans des fichiers JSON.

Fichier : lang/fr.json

{
  "Welcome": "Bienvenue",
  "Login": "Connexion",
  "Register": "Inscription"
}

Fichier : lang/es.json

{
  "Welcome": "Bienvenido",
  "Login": "Iniciar sesión",
  "Register": "Registrarse"
}

Utilisation : {{ __('Welcome') }} retournera "Bienvenue" si la locale est 'fr', ou "Bienvenido" si la locale est 'es'.

Routing et localisation

URLs localisées

Le système génère automatiquement des URLs localisées pour chaque route avec préfixes de langue.

// URLs générées automatiquement
/fr/blog                    → Blog en français
/es/blog                    → Blog en español
/fr/blog/mon-article        → Article en français
/es/blog/mi-articulo        → Article en español
/fr/shop/products           → Produits en français
/es/shop/products           → Productos en español

Helper : Utilisez localized_route('blog.index') pour générer automatiquement l'URL avec le préfixe de langue. Si le routing est désactivé, la route est générée sans préfixe.

Slugs traduits dans les routes

Les routes avec paramètres de modèle (Post, Product, Page) utilisent des slugs traduits pour une meilleure SEO et une expérience utilisateur optimale.

// Exemple : Un article de blog avec slugs traduits
Post::create([
    'slug' => [
        'fr' => 'mon-article',
        'es' => 'mi-articulo'
    ]
]);

// URLs générées
/fr/blog/mon-article    → Article en français
/es/blog/mi-articulo    → Article en español

// Si on accède à /fr/blog/mi-articulo
// → Redirection automatique vers /es/blog/mi-articulo

Queries pour récupérer les modèles par slug traduit

Des Queries dédiées sont utilisées pour résoudre les modèles par leur slug traduit :

  • GetPostByTranslatedSlugQuery - Pour les articles de blog
  • GetProductByTranslatedSlugQuery - Pour les produits
  • GetPageByTranslatedSlugQuery - Pour les pages

Gestion du cas TRANSLATIONS_ENABLED=false : Les Queries cherchent d'abord dans les colonnes JSON (pour la locale par défaut puis les autres locales) même si les traductions sont désactivées. Cela permet de migrer progressivement d'un site traduit à un site non traduit.

Service de traduction de slugs

Le service SlugTranslationService centralise la logique de traduction des slugs lors du changement de langue.

use App\Services\SlugTranslationService;

$service = app(SlugTranslationService::class);

// Traduire les paramètres de route lors d'un changement de langue
$translatedParams = $service->translateSlugParams(
    baseRouteName: 'blog.show',
    routeParameters: ['slug' => 'mon-article'],
    currentLocale: 'fr',
    targetLocale: 'es'
);

// Retourne : ['slug' => 'mi-articulo']

Utilisation : Ce service est utilisé automatiquement par le contrôleur SwitchLocaleController et le composant Livewire LanguageSelector lors du changement de langue.

Composant de sélection de langue

Le composant Livewire LanguageSelector permet aux utilisateurs de changer de langue tout en conservant la page actuelle (avec traduction automatique des slugs).

// Dans une vue Blade
<livewire:language-selector />

// Le composant :
// - Affiche les langues disponibles
// - Permet de changer de langue
// - Redirige vers la même page dans la nouvelle langue
// - Traduit automatiquement les slugs si nécessaire
// - Stocke la préférence en session

Fonctionnement

  1. L'utilisateur clique sur une langue dans le sélecteur
  2. Le composant identifie la route actuelle et ses paramètres
  3. Si la route contient un slug (blog.show, shop.products.show, pages.show), le slug est traduit via SlugTranslationService
  4. La nouvelle URL est générée avec la locale cible et le slug traduit
  5. L'utilisateur est redirigé vers la nouvelle URL

Redirections inter-langues

Si un slug est trouvé dans une autre langue que la locale courante, une redirection automatique est effectuée vers la bonne URL.

// Exemple : Un article avec slug FR "mon-article" et ES "mi-articulo"

// Accès à /fr/blog/mi-articulo
// → Le système détecte que "mi-articulo" est le slug ES
// → Redirection automatique vers /es/blog/mi-articulo

// Accès à /es/blog/mon-article
// → Le système détecte que "mon-article" est le slug FR
// → Redirection automatique vers /fr/blog/mon-article

Note : Les redirections inter-langues sont conditionnelles sur translation_component_enabled('routing'). Si le routing est désactivé, aucune redirection n'est effectuée.

Système de cache des traductions

Contenu du guide :

  • • Configuration du cache (TTL, store, activation)
  • • Cache des traductions JSON (interface)
  • • Cache des traductions BDD (modèles)
  • • Invalidation automatique via Observer
  • • Commande translate:clear-cache
  • • Support des drivers (Redis, Memcached, File)
  • • Tests de performance
  • • Bonnes pratiques

Configuration

Le système de cache est configuré dans config/translations.php :

'cache' => [
    'enabled' => env('TRANSLATIONS_CACHE_ENABLED', true),
    'ttl' => env('TRANSLATIONS_CACHE_TTL', 86400), // 24 heures
    'store' => env('TRANSLATIONS_CACHE_STORE', null), // null = cache par défaut
],

Utilisation

Le cache est automatiquement utilisé lors de la récupération des traductions :

// Cache automatique pour les traductions JSON
$translations = app(\App\Services\TranslationCacheService::class)
    ->getJsonTranslations('fr');

// Cache automatique pour les traductions BDD (via ConditionallyTranslatable)
$post->title; // Utilise le cache si activé

// Invalidation manuelle
app(\App\Services\TranslationCacheService::class)
    ->invalidateModelCache(Post::class, $post->id);

// Vider tout le cache
php artisan translate:clear-cache

Invalidation automatique

Le cache est automatiquement invalidé lors de la modification des modèles traduisibles grâce à l'Observer TranslatableModelObserver :

  • • Création d'un modèle → Invalidation du cache
  • • Mise à jour d'un modèle → Invalidation du cache
  • • Suppression d'un modèle → Invalidation du cache

Note : L'Observer est automatiquement enregistré pour tous les modèles utilisant le trait ConditionallyTranslatable.

Traductions dans les modules

Contenu du guide :

  • • Structure des fichiers de traduction dans les modules
  • • Création d'un Service Provider par module
  • • Chargement conditionnel des traductions
  • • Exemple complet avec le module Blog
  • • Tests et bonnes pratiques

Structure

Chaque module peut avoir ses propres fichiers de traduction dans un dossier lang/ :

app/Specifics/Blog/
├── lang/
│   ├── fr.json
│   └── es.json
├── Providers/
│   └── BlogServiceProvider.php
└── ...

Service Provider

Créez un Service Provider dans chaque module pour charger les traductions :

namespace App\Specifics\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class BlogServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if (translation_component_enabled('interface')) {
            $langPath = __DIR__.'/../lang';
            if (is_dir($langPath)) {
                $this->loadJsonTranslationsFrom($langPath);
            }
        }
    }
}

Important : Enregistrez le Service Provider dans bootstrap/providers.php.

Format des fichiers JSON

Utilisez une structure plate avec notation pointée :

// lang/fr.json
{
    "blog.title": "Blog",
    "blog.posts": "Articles",
    "blog.create_post": "Créer un article"
}

// Utilisation
__('blog.title'); // "Blog"

Helpers disponibles

Vérification de l'état

  • translations_enabled() - Vérifie si les traductions sont activées
  • translation_component_enabled('database') - Vérifie un composant
  • translation_model_enabled('post') - Vérifie un modèle

Gestion des locales

  • current_locale() - Locale courante
  • available_locales() - Locales disponibles
  • locale_name('fr') - Nom de la locale
  • locale_flag('fr') - Drapeau de la locale

Routing

  • localized_route('blog.index') - Route localisée
  • localized_route('blog.show', ['slug' => $slug]) - Route avec paramètres
  • route_is_localized('home') - Vérifie si la route actuelle correspond

Modèles

  • trans_model($post, 'title') - Traduction d'un modèle
  • trans_model($post, 'slug') - Slug traduit d'un modèle

Classe TranslationHelper

Tous les helpers sont également disponibles via la classe TranslationHelper :

use App\Support\TranslationHelper;

// Locales
$locale = TranslationHelper::currentLocale();
$locales = TranslationHelper::availableLocales();
$name = TranslationHelper::localeName('fr');

// Routes
$url = TranslationHelper::localizedRoute('blog.index');

// Modèles
$title = TranslationHelper::transModel($post, 'title');

// Vérifications
$enabled = TranslationHelper::enabled();
$componentEnabled = TranslationHelper::componentEnabled('database');
$modelEnabled = TranslationHelper::modelEnabled('post');

Note : Consultez le Guide complet des helpers pour tous les détails et exemples.

Exemples pratiques

Exemple 1 : Créer un modèle traduisible

use App\Specifics\Shares\Models\Concerns\ConditionallyTranslatable;

class Product extends Model
{
    use ConditionallyTranslatable;

    protected function getTranslatableFields(): array
    {
        return ['name', 'description', 'slug'];
    }
}

// Création avec traductions
$product = new Product();
$product->setTranslation('name', 'fr', 'Produit en français');
$product->setTranslation('name', 'es', 'Producto en español');
$product->save();

Exemple 2 : Afficher selon la locale

// Dans une vue Blade
<h1>{{ $product->name }}</h1>
// Affiche automatiquement selon app()->getLocale()

// Ou explicitement
<h1>{{ $product->getTranslation('name', 'es') }}</h1>
// Affiche toujours en espagnol

Exemple 3 : Migration de données existantes

// Convertir une colonne string en JSON
Schema::table('posts', function (Blueprint $table) {
    $table->json('title')->nullable()->change();
});

// Migrer les données existantes
$posts = Post::all();
foreach ($posts as $post) {
    $oldTitle = $post->title; // Ancienne valeur (string)
    $post->setTranslation('title', 'fr', $oldTitle);
    $post->save();
}

Exemple 4 : Utiliser les slugs traduits dans les vues


@foreach($posts as $post)
    <a href="{{ localized_route('blog.show', ['slug' => trans_model($post, 'slug')]) }}">
        {{ trans_model($post, 'title') }}
    </a>
@endforeach


<a href="{{ localized_route('shop.products.show', ['slug' => trans_model($product, 'slug')]) }}">
    {{ trans_model($product, 'name') }}
</a>




Important : Toujours utiliser localized_route() au lieu de route() pour les routes localisées. Cela garantit que les URLs sont correctement générées avec ou sans routing activé.

Exemple 5 : Vérifier la route actuelle avec localisation

Le helper route_is_localized() permet de vérifier si la route actuelle correspond à une route donnée, en tenant compte automatiquement de la localisation.


<x-application-logo class="{{ route_is_localized('home') ? 'active' : '' }}" />


@if(route_is_localized(['home', 'blog.index']))
    <div class="active-menu">Menu actif</div>
@endif


@if(route_is_localized('blog.*'))
    <div class="blog-section">Section blog</div>
@endif


<nav>
    <a href="{{ localized_route('home') }}"
       class="{{ route_is_localized('home') ? 'active' : '' }}">
        Accueil
    </a>
    <a href="{{ localized_route('blog.index') }}"
       class="{{ route_is_localized('blog.*') ? 'active' : '' }}">
        Blog
    </a>
</nav>

Note : Cette fonction fonctionne automatiquement avec ou sans localisation activée. Si le routing est activé, elle teste toutes les variantes (fr.home, es.home, etc.). Sinon, elle utilise routeIs() normal.

FAQ - Questions fréquentes

Pour une FAQ complète avec toutes les questions et réponses détaillées, consultez le Guide FAQ.

Comment activer/désactiver les traductions ?

Utilisez la variable TRANSLATIONS_ENABLED dans .env :

TRANSLATIONS_ENABLED=true  # Activer
TRANSLATIONS_ENABLED=false # Désactiver

Puis-je activer les traductions pour seulement certains modèles ?

Oui, utilisez les variables TRANSLATIONS_MODEL_* pour chaque modèle.

Comment rendre un modèle traduisible ?

Consultez le Guide pour rendre un modèle traduisible.

Comment ajouter une nouvelle langue ?

Consultez le Guide pour ajouter une nouvelle langue.

Comment vider le cache des traductions ?

# Vider tout le cache
php artisan translate:clear-cache

# Vider uniquement le cache JSON
php artisan translate:clear-cache --json

# Vider uniquement le cache BDD
php artisan translate:clear-cache --db

# Vider pour une locale spécifique
php artisan translate:clear-cache --locale=fr

Comment utiliser les traductions dans les modules ?

Consultez le Guide des traductions dans les modules.

Quels tests sont disponibles ?

Le système dispose de 204+ tests avec une couverture estimée de > 88%. Le rapport de couverture complet est disponible dans docs/testing/translation-test-coverage-report.md.

Dépannage

Problème : Les traductions ne sont pas sauvegardées

  • • Vérifier que TRANSLATIONS_ENABLED=true
  • • Vérifier que TRANSLATIONS_DATABASE_ENABLED=true
  • • Vérifier que TRANSLATIONS_MODEL_{MODEL}=true
  • • Vérifier que la colonne est de type JSON

Problème : Le fallback ne fonctionne pas

  • • Vérifier use_fallback => true dans config/translatable.php
  • • Vérifier que fallback_locale est correctement configuré

Problème : Les champs Filament ne s'affichent pas

  • • Vérifier que TRANSLATIONS_FILAMENT_ENABLED=true
  • • Vérifier que LocaleAction::make() est ajouté dans getHeaderActions()
  • • Vérifier que $locale est initialisé dans mount()

Problème : RouteNotFoundException lors du changement de langue

  • • Vérifier que vous utilisez localized_route() et non route() pour les routes localisées
  • • Vérifier que la route existe avec le préfixe de locale (ex: fr.appointments.index)
  • • Vérifier que TRANSLATIONS_ROUTING_ENABLED=true si vous utilisez des routes localisées

Problème : 404 sur les pages avec slugs traduits

  • • Vérifier que les Queries (GetPostByTranslatedSlugQuery, etc.) sont correctement configurées
  • • Vérifier que les slugs sont bien stockés en JSON dans la base de données
  • • Si TRANSLATIONS_ENABLED=false, vérifier que les Queries cherchent aussi dans les colonnes JSON (fonctionnalité déjà implémentée)
  • • Vérifier que le trait ConditionallyTranslatable est utilisé sur le modèle

Problème : TypeError avec htmlspecialchars() lors de TRANSLATIONS_ENABLED=false

  • • Vérifier que le trait ConditionallyTranslatable est utilisé sur le modèle
  • • Le trait ajuste automatiquement les casts (array si traductions activées, string sinon)
  • • Les accesseurs extraient automatiquement la valeur de la locale par défaut depuis les colonnes JSON

Références

Fichiers du projet

  • config/translatable.php
  • config/translations.php
  • bootstrap/helpers.php - Helpers globaux
  • app/Support/TranslationHelper.php - Classe helper
  • app/Services/TranslationCacheService.php - Service de cache
  • app/Console/Commands/TranslateClearCacheCommand.php - Commande de cache
  • app/Observers/TranslatableModelObserver.php - Observer pour invalidation
  • app/Services/TranslationMigrationService.php - Service de migration
  • app/Console/Commands/TranslateMigrateDataCommand.php - Commande de migration
  • app/Filament/Support/TranslatableField.php - Helper Filament
  • app/Filament/Support/LocaleAction.php - Action Filament
  • app/Specifics/Shares/Models/Concerns/ConditionallyTranslatable.php - Trait
  • app/Specifics/{Module}/Providers/{Module}ServiceProvider.php - Service Providers modules
  • tests/Feature/TranslationRegressionTest.php - Tests de régression
  • docs/testing/translation-test-coverage-report.md - Rapport de couverture

Prendre rendez-vous