bouleluciole/db-translatable

Package de facilitation d'internationalisation database

Installs: 52

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/bouleluciole/db-translatable

0.2.1 2026-01-29 14:38 UTC

This package is auto-updated.

Last update: 2026-03-01 00:48:55 UTC


README

DB-Translatable est une solution robuste et performante pour gérer les traductions de vos modèles Eloquent dans des tables dédiées. Contrairement au stockage JSON, cette approche permet des recherches SQL natives, un typage strict et une intégrité référentielle totale.

Le package respecte strictement les conventions snake_case et lowercase pour une compatibilité parfaite avec les outils comme Prisma ou les standards de bases de données modernes.

📋 Table des matières

🚀 Installation

  1. Installer via Composer :

    composer require bouleluciole/db-translatable
  2. Publier la configuration :

    php artisan vendor:publish --tag=db-translatable-config

Note : Le package utilise le Laravel Auto-Discovery. Le Service Provider est enregistré automatiquement via les métadonnées du composer.json.

⚙️ Configuration

Le fichier config/db-translatable.php centralise le comportement du package :

Tables et conventions

'tables' => [
    'prefix' => '',                  // Préfixe des tables de traduction
    'suffix' => '_translations',     // Suffixe des tables de traduction
    'locale_column' => 'locale',     // Nom de la colonne locale
    'foreign_key' => null,           // Clé étrangère (null = auto)
],

Exemple : Pour un modèle Post, la table de traduction sera post_translations.

Gestion des locales

'locale' => [
    // Driver de résolution de la locale
    'driver' => 'cookie',  // 'static' | 'session' | 'cookie'

    // Locale par défaut
    'default' => config('app.locale', 'fr'),

    // Configuration par driver
    'drivers' => [
        'session' => [
            'key' => 'translatable.locale',
        ],
        'cookie' => [
            'key'    => 'translatable_locale',
            'minutes' => 60 * 24 * 365,  // 1 an
        ],
        'static' => [
            'value' => config('app.locale', 'fr'),
        ],
    ],

    // Fallbacks globaux
    'fallback' => ['en', 'fr'],

    // Fallbacks spécifiques par langue
    'map_fallbacks' => [
        'en' => ['fr'],
        'es' => 'fr',
    ],

    // Utilisation des fallbacks par mode
    'use_fallback' => [
        'auto' => true,      // $model->title
        'forced' => false,   // $model->useLocale('en')
        'explicit' => false, // $model->translation('en')
    ],

    // Mode strict (lève une exception si traduction manquante)
    'strict' => false,

    // Locales autorisées (null = toutes)
    'allowed' => null,  // ['fr', 'en', 'es']

    // Autoriser les locales non listées
    'allow_unknown' => false,

    // Valeur retournée si traduction manquante
    'missing_value' => null,

    // Empêcher l'écriture de locales inconnues
    'prevent_writing_unknown_locale' => false,
],

Cache

'cache' => [
    'enabled' => true,              // Activer le cache
    'store' => null,                // Store Laravel (null = défaut)
    'prefix' => 'db-translatable',  // Préfixe des clés
    'ttl' => 3600,                  // Durée de vie en secondes
],

Comportement Eloquent

'eloquent' => [
    // Chargement automatique des traductions
    'autoload' => true,

    // Nom de la relation
    'relation' => 'translations',

    // Accès dynamique aux attributs ($model->title)
    'dynamic_access' => true,

    // Utiliser les fallbacks dans les scopes
    'use_fallback_in_scopes' => false,

    // Colonnes à ignorer en lecture
    'skipped_getters' => [],

    // Colonnes à ignorer en écriture
    'skipped_setters' => ['created_at', 'updated_at', 'deleted_at'],
],

🏗️ Mise en place

1. Définir un modèle traduisible

Ajoutez le trait HasTranslations et listez les attributs traduisibles :

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use DbTranslatable\Concerns\HasTranslations;

class Post extends Model
{
    use HasTranslations;

    public array $translatable = ['title', 'content', 'slug'];
}

2. Générer l'infrastructure

# Générer la migration de traduction
php artisan make:translation-migration Post

# Générer le modèle de traduction
php artisan make:translation-model Post

# Exécuter les migrations
php artisan migrate

Cela créera :

  • La table posts_translations avec les colonnes traduisibles
  • Le modèle PostTranslation

3. Configuration avancée du modèle

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use DbTranslatable\Concerns\HasTranslations;

class Post extends Model
{
    use HasTranslations, SoftDeletes;

    /**
     * Les attributs qui peuvent être traduits.
     * Ces champs doivent exister dans la table 'posts_translations'.
     */
    protected array $translatable = [
        'title',
        'content',
        'slug_translated',
    ];

    /**
     * OPTIONNEL : Définit explicitement le modèle de traduction.
     * Par défaut : NomDuModele + 'Translation' (ex: PostTranslation).
     */
    protected string $translationModel = PostTranslation::class;

    /**
     * OPTIONNEL : Liste des colonnes à ignorer lors de l'ACCÈS (getAttribute).
     * Ces colonnes seront toujours lues sur la table parente 'posts'.
     */
    protected array $skippedGetters = [
        'internal_reference',
    ];

    /**
     * OPTIONNEL : Liste des colonnes à ignorer lors de la MODIFICATION (setAttribute).
     * Utile si vous voulez que 'slug' soit géré par le modèle parent uniquement.
     */
    protected array $skippedSetters = [
        'slug',
        'user_id',
    ];

    /**
     * Attributs de base du modèle (table 'posts').
     */
    protected $fillable = [
        'slug',
        'user_id',
        'internal_reference',
        'is_published',
    ];
}

🌍 Gestion de la langue

Le package permet de définir la langue de manière persistante ou temporaire via le LocaleResolver.

Via le Helper

Le helper db_translatable() est le moyen le plus rapide d'interagir avec la configuration.

// Définir la langue (sera stockée en cookie/session selon le driver)
db_translatable()->setCurrentLocale('en');

// Récupérer la langue active
echo db_translatable()->current();  // 'en'

// Récupérer la langue de fallback
echo db_translatable()->fallback();  // 'fr'

// Vérifier si les fallbacks sont activés
$useFallback = db_translatable()->useFallback();  // true/false

// Vérifier si le mode strict est activé
$strict = db_translatable()->strict();  // true/false

Via la Facade

use DbTranslatable\Facades\LocaleResolver;

LocaleResolver::setCurrentLocale('es');

echo LocaleResolver::current();  // 'es'

Drivers disponibles

Static

La locale est fixe, définie dans la config.

'driver' => 'static',
'drivers' => [
    'static' => [
        'value' => 'fr',
    ],
],

Session

La locale est stockée en session Laravel.

'driver' => 'session',
'drivers' => [
    'session' => [
        'key' => 'translatable.locale',
    ],
],

Cookie

La locale est stockée dans un cookie (recommandé pour les applications web).

'driver' => 'cookie',
'drivers' => [
    'cookie' => [
        'key'    => 'translatable_locale',
        'minutes' => 60 * 24 * 365,  // 1 an
    ],
],

📝 Manipulation des données

Accès direct via locale courante

// Créer un post
$post = Post::create(['slug' => 'mon-post']);

// Définir la traduction pour la locale courante
$post->title = 'Titre FR';
$post->content = 'Contenu en français';
$post->save();

// Lire la traduction (locale courante)
echo $post->title;  // 'Titre FR'

// Changer de langue
db_translatable()->setCurrentLocale('en');

// Définir la traduction anglaise
$post->title = 'English Title';
$post->save();

echo $post->title;  // 'English Title'

Forcer une locale temporairement

// Forcer une locale pour cette instance uniquement
$post->useLocale('en');
echo $post->title;  // 'English Title'

$post->useLocale('fr');
echo $post->title;  // 'Titre FR'

// Réinitialiser la locale forcée
$post->resetLocale();
echo $post->title;  // Retour à la locale courante

Accès explicite via TranslationProxy

Le TranslationProxy permet d'accéder directement à une traduction spécifique sans modifier la locale globale ou celle du modèle.

// Lecture
echo $post->translation('fr')->title;  // 'Titre FR'
echo $post->translation('en')->title;  // 'English Title'

// Écriture
$post->translation('es')->title = 'Título en español';
$post->translation('es')->content = 'Contenido en español';
$post->save();

// Appeler des méthodes sur la traduction
$post->translation('en')->update(['title' => 'Updated Title']);

// Chaînage fluent
$post->translation('de')
    ->fill(['title' => 'Deutscher Titel'])
    ->save();

Exécuter un callback dans une locale

La méthode withLocale permet d'exécuter un callback en forçant temporairement une locale, puis restaure automatiquement l'état précédent.

$post->withLocale('en', function ($model) {
    $model->title = 'Hello World';
    $model->content = 'This is content in English';
});

// La locale précédente est automatiquement restaurée
echo $post->title;  // Utilise la locale courante

// Même en cas d'exception, la locale est restaurée
try {
    $post->withLocale('de', function ($model) {
        $model->title = 'Deutscher Titel';
        throw new Exception('Erreur !');
    });
} catch (Exception $e) {
    // La locale a été restaurée malgré l'exception
}

Récupérer toutes les traductions

// Récupérer toutes les traductions d'un champ
$translations = $post->getTranslations('title');
// ['fr' => 'Titre FR', 'en' => 'English Title', 'es' => 'Título en español']

// Récupérer toutes les traductions de tous les champs
$allTranslations = $post->getTranslations();
/*
[
    'fr' => ['title' => 'Titre FR', 'content' => '...', 'slug' => '...'],
    'en' => ['title' => 'English Title', 'content' => '...', 'slug' => '...'],
    'es' => ['title' => 'Título en español', 'content' => '...', 'slug' => '...'],
]
*/

// Accéder comme un attribut
$locales = $post->available_locales;
// ['fr', 'en', 'es']

🗑️ Suppression de traductions

// Supprimer une traduction spécifique
$post->deleteTranslation('fr');

// Supprimer plusieurs traductions
$post->deleteTranslation(['fr', 'en']);

// Supprimer toutes les traductions sauf certaines
$post->deleteTranslationsExcept(['fr', 'en']);

// Supprimer le modèle et toutes ses traductions
$post->delete();

// Avec SoftDeletes : force delete
$post->forceDelete();

🔍 Informations et utilitaires

// Liste des attributs traduisibles
$translatable = $post->getTranslatableAttributes();
// ['title', 'content', 'slug']

// Vérifier si un attribut est traduisible
if ($post->isTranslatableAttribute('title')) {
    // ...
}

// Locales disponibles pour cet enregistrement
$locales = $post->getAvailableLocales();
// ['fr', 'en', 'es']

// Ou via l'attribut
$locales = $post->available_locales;

// Vérifier si une traduction existe
if ($post->hasLocale('en')) {
    // La traduction anglaise existe
}

// Récupérer la locale actuellement utilisée
$locale = $post->getLocaleToUse();  // 'fr'

// Nom de la relation de traduction
$relationName = $post->getTranslationsRelationName();  // 'translations'

// Nom de la clé étrangère
$foreignKey = $post->getTranslationForeignKey();  // 'post_id'

// Classe du modèle de traduction
$translationClass = $post->getTranslationModelClass();  // 'App\Models\PostTranslation'

🎯 Query Scopes

Le package offre une gamme complète de scopes pour interroger les traductions en base de données.

Scopes de base (T)

Les scopes terminant par T utilisent la locale courante avec fallback (selon la configuration).

WHERE

// WHERE simple
Post::whereT('title', 'Bonjour')->get();
Post::whereT('title', 'LIKE', '%Laravel%')->get();

// OR WHERE
Post::whereT('title', 'Hello')
    ->orWhereT('title', 'Bonjour')
    ->get();

// WHERE NOT
Post::whereNotT('title', 'Test')->get();

// Callback (nested where)
Post::whereT(function($q) {
    $q->where('title', 'LIKE', '%Laravel%')
      ->orWhere('content', 'LIKE', '%PHP%');
})->get();

NULL / NOT NULL

Post::whereNullT('title')->get();
Post::whereNotNullT('title')->get();
Post::orWhereNullT('content')->get();

BETWEEN / NOT BETWEEN

Post::whereBetweenT('title', ['A', 'M'])->get();
Post::whereNotBetweenT('title', ['N', 'Z'])->get();
Post::orWhereBetweenT('title', ['A', 'F'])->get();

IN / NOT IN

Post::whereInT('title', ['Hello', 'Bonjour', 'Hola'])->get();
Post::whereNotInT('title', ['Test', 'Demo'])->get();
Post::orWhereInT('title', ['Hello', 'Hi'])->get();

LIKE

Post::whereLikeT('title', '%Laravel%')->get();
Post::orWhereLikeT('content', '%PHP%')->get();

ORDER BY

// Trier par colonne traduite
Post::orderByT('title', 'asc')->get();
Post::orderByT('title', 'desc')->get();

// Latest / Oldest
Post::latestT('created_at')->get();
Post::oldestT('created_at')->get();

GROUP BY

Post::groupByT('title')->get();

Locale spécifique

Tous les scopes T acceptent un paramètre $locale optionnel :

// Rechercher uniquement dans la locale 'en'
Post::whereT('title', 'Hello', locale: 'en')->get();

// Trier par titre français
Post::orderByT('title', 'asc', 'fr')->get();

Scopes stricts (StrictT)

Les scopes terminant par StrictT recherchent uniquement dans la locale spécifiée, sans fallback.

// WHERE strict (sans fallback)
Post::whereStrictT('title', 'Hello', locale: 'en')->get();

// OR WHERE strict
Post::whereStrictT('title', 'Hello', locale: 'en')
    ->orWhereStrictT('title', 'Bonjour', locale: 'fr')
    ->get();

// WHERE NOT strict
Post::whereNotStrictT('title', 'Test', locale: 'en')->get();

// NULL / NOT NULL strict
Post::whereNullStrictT('title', 'en')->get();
Post::whereNotNullStrictT('content', 'fr')->get();

// BETWEEN strict
Post::whereBetweenStrictT('title', ['A', 'M'], 'en')->get();

// IN strict
Post::whereInStrictT('title', ['Hello', 'Hi'], 'en')->get();

// LIKE strict
Post::whereLikeStrictT('title', '%Laravel%', 'en')->get();

// SEARCH strict
Post::searchStrictT('title', 'Laravel', 'en')->get();

// ORDER BY strict
Post::orderByStrictT('title', 'asc', 'en')->get();

// GROUP BY strict
Post::groupByStrictT('title', 'en')->get();

// LATEST / OLDEST strict
Post::latestStrictT('created_at', 'en')->get();
Post::oldestStrictT('created_at', 'en')->get();

Scopes intelligents (Smart)

Les scopes terminant par Smart détectent automatiquement si la colonne est traduisible ou non, et appliquent le scope approprié.

// WHERE smart : détecte automatiquement si 'title' est traduisible
Post::whereSmart('title', 'Hello')->get();  // Utilise whereT

// Si la colonne n'est pas traduisible, utilise le WHERE standard
Post::whereSmart('status', 'published')->get();  // Utilise where

// Combinaison de colonnes traduites et non traduites
Post::whereSmart('title', 'LIKE', '%Laravel%')
    ->whereSmart('status', 'published')
    ->whereSmart('is_featured', true)
    ->get();

// Tous les scopes ont une variante Smart
Post::whereNotSmart('title', 'Test')->get();
Post::orWhereSmart('content', 'LIKE', '%PHP%')->get();
Post::whereNullSmart('title')->get();
Post::whereNotNullSmart('content')->get();
Post::whereBetweenSmart('title', ['A', 'M'])->get();
Post::whereInSmart('title', ['Hello', 'Hi'])->get();
Post::whereLikeSmart('title', '%Laravel%')->get();
Post::orderBySmart('title', 'desc')->get();
Post::groupBySmart('title')->get();
Post::latestSmart('created_at')->get();
Post::oldestSmart('created_at')->get();

// Variantes strictes Smart
Post::whereStrictSmart('title', 'Hello', locale: 'en')->get();
Post::orderByStrictSmart('title', 'asc', 'en')->get();

Scopes de vérification

Vérifier l'existence de traductions

// Posts ayant une traduction en anglais
Post::hasTranslation('en')->get();

// Posts n'ayant PAS de traduction en français
Post::doesntHaveTranslation('fr')->get();

// Posts ayant des traductions dans TOUTES les locales données
Post::hasTranslations(['fr', 'en', 'es'])->get();

// Posts ayant une traduction dans AU MOINS UNE des locales
Post::hasAnyTranslation(['fr', 'en'])->get();

// Posts n'ayant AUCUNE traduction dans les locales données
Post::doesntHaveAnyTranslation(['fr', 'en'])->get();

Vérifier la complétude des traductions

// Posts dont TOUTES les colonnes traduites sont remplies pour la locale
Post::whereTranslationComplete('fr')->get();

// Posts ayant AU MOINS UNE colonne traduite remplie
Post::whereTranslationPartial('en')->get();

// Posts dont TOUTES les colonnes traduites sont vides
Post::whereTranslationEmpty('es')->get();

// Posts ayant des traductions complètes pour TOUTES les locales
Post::whereAllTranslationsComplete(['fr', 'en', 'es'])->get();

Vérifier des colonnes spécifiques

// Posts ayant le champ 'title' traduit en français
Post::whereColumnTranslated('title', 'fr')->get();

// Posts n'ayant PAS le champ 'content' traduit en anglais
Post::whereColumnNotTranslated('content', 'en')->get();

// Posts ayant TOUTES les colonnes spécifiées traduites
Post::whereColumnsTranslated(['title', 'content'], 'fr')->get();

// Posts ayant AU MOINS UNE des colonnes traduites
Post::whereAnyColumnTranslated(['title', 'content'], 'en')->get();

Compter les traductions

// Ajouter le nombre de traductions à chaque post
$posts = Post::withTranslationCount()->get();
echo $posts->first()->translations_count;  // 3

// Posts ayant exactement N traductions
Post::hasExactlyTranslations(3)->get();

// Posts ayant au moins N traductions
Post::hasMinTranslations(2)->get();

// Posts ayant au maximum N traductions
Post::hasMaxTranslations(5)->get();

Scopes de recherche

Recherche simple

// Recherche fulltext sur une colonne traduite
Post::searchT('title', 'Laravel')->get();

// Équivalent à whereLikeT avec %...%
Post::searchT('title', 'Laravel')->get();
// WHERE title LIKE '%Laravel%'

// Recherche smart (détecte si traduisible)
Post::searchSmart('title', 'Laravel')->get();

// Recherche stricte (sans fallback)
Post::searchStrictT('title', 'Laravel', 'en')->get();

Recherche multi-colonnes

// Recherche dans PLUSIEURS colonnes traduites (OR)
Post::searchAnyT(['title', 'content'], 'Laravel')->get();

// Version Smart
Post::searchAnySmart(['title', 'content'], 'Laravel')->get();

// Version Stricte
Post::searchAnyStrictT(['title', 'content'], 'Laravel', 'en')->get();

Recherche avec WHERE ANY / ALL

// WHERE ANY : au moins UNE des colonnes doit correspondre
Post::whereAnyT(['title', 'content'], 'LIKE', '%Laravel%')->get();

// WHERE ALL : TOUTES les colonnes doivent correspondre
Post::whereAllT(['title', 'content'], 'LIKE', '%PHP%')->get();

// Variantes Smart
Post::whereAnySmart(['title', 'content'], 'Hello')->get();
Post::whereAllSmart(['title', 'content'], 'Test')->get();

// Variantes Strictes
Post::whereAnyStrictT(['title', 'content'], 'Hello', locale: 'en')->get();
Post::whereAllStrictT(['title', 'content'], 'Test', locale: 'fr')->get();

Configuration des scopes

Forcer une locale pour une requête

// Utiliser une locale spécifique pour toute la requête
Post::forLocale('en', function($query) {
    $query->whereT('title', 'Hello')
          ->orderByT('title');
})->get();

// Ou sans callback (permanent pour cette requête)
Post::forLocale('en')
    ->whereT('title', 'Hello')
    ->get();

// Avec reset automatique
Post::forLocale('en', function($query) {
    $query->whereT('title', 'Hello');
}, reset: true)->get();

Désactiver les fallbacks pour une requête

// Désactiver temporairement les fallbacks
Post::withoutFallbacks(function($query) {
    $query->whereT('title', 'Hello')
          ->orderByT('title');
})->get();

// Ou sans callback
Post::withoutFallbacks()
    ->whereT('title', 'Hello')
    ->get();

Activer les fallbacks pour une requête

// Forcer les fallbacks (même si désactivés dans la config)
Post::withFallbacks(function($query) {
    $query->whereT('title', 'Hello');
})->get();

// Avec des locales de fallback personnalisées
Post::withFallbacks(['es', 'fr'], function($query) {
    $query->whereT('title', 'Hello');
})->get();

🔄 Migration des données existantes

Si vous avez déjà des données dans vos tables principales et que vous souhaitez les migrer vers les tables de traductions :

# Migrer un modèle spécifique
php artisan translations:migrate Post

# Migrer tous les modèles utilisant HasTranslations
php artisan translations:migrate --all

# Spécifier la locale cible
php artisan translations:migrate Post --locale=fr

# Forcer sans confirmation
php artisan translations:migrate Post --force

# Combiner les options
php artisan translations:migrate --all --locale=en --force

Comment ça fonctionne :

  1. La commande lit toutes les lignes de la table principale
  2. Pour chaque ligne, elle crée une traduction dans la locale spécifiée
  3. Les valeurs sont copiées depuis les colonnes traduisibles
  4. Utilise updateOrInsert pour éviter les doublons si relancée

Exemple :

# Vous avez une table 'posts' avec title, content
# Vous voulez migrer vers 'posts_translations'
php artisan translations:migrate Post --locale=fr

Note : Après la migration, vous pouvez supprimer les colonnes traduisibles de la table principale si vous le souhaitez.

⌨️ Commandes Artisan

Commande Utilité
make:translation-migration {model} Génère la migration SQL optimisée (BigInt/UUID/ULID).
make:translation-migration --all Génère les migrations pour tous les modèles traduisibles.
make:translation-model {model} Génère le modèle de traduction Eloquent.
make:translation-model --all Génère les modèles pour tous les modèles traduisibles.
translations:migrate {model} Migre les données existantes vers la table de traductions.
translations:migrate --all Migre toutes les données de tous les modèles traduisibles.
db-translatable:info Affiche l'état de santé et la config du package.

🔀 Fallbacks et cascade

Le package supporte un système de fallback sophistiqué pour gérer les traductions manquantes.

Configuration des fallbacks

// Fallbacks globaux (array ou string)
'fallback' => ['en', 'fr'],

// Fallbacks spécifiques par langue
'map_fallbacks' => [
    'en' => ['fr'],          // Si 'en' manque, chercher en 'fr'
    'es' => ['it', 'fr'],    // Si 'es' manque, chercher en 'it', puis 'fr'
    'de' => 'en',            // Si 'de' manque, chercher en 'en'
],

Utilisation

// Avec la configuration ci-dessus :
db_translatable()->setCurrentLocale('es');

// Si la traduction 'es' n'existe pas :
// 1. Cherche en 'it' (map_fallbacks['es'][0])
// 2. Si absent, cherche en 'fr' (map_fallbacks['es'][1])
// 3. Si absent, cherche en 'en' (fallback[0])
// 4. Si absent, cherche en 'fr' (fallback[1])
echo $post->title;

Contrôler les fallbacks par mode

'use_fallback' => [
    'auto' => true,      // $model->title
    'forced' => false,   // $model->useLocale('en')->title
    'explicit' => false, // $model->translation('en')->title
],

Exemple :

// AUTO : utilise les fallbacks
$post->title;  // Cherche en 'es', puis fallbacks

// FORCED : pas de fallbacks (configuration ci-dessus)
$post->useLocale('es')->title;  // Uniquement 'es', retourne null si absent

// EXPLICIT : pas de fallbacks
$post->translation('es')->title;  // Uniquement 'es'

Mode strict

// Lever une exception si la traduction est manquante
'strict' => true,

// Utilisation
$post->title;  // RuntimeException si manquant

🗄️ Gestion du cache

Le package utilise le cache Laravel pour améliorer les performances.

Configuration

'cache' => [
    'enabled' => true,              // Activer/désactiver
    'store' => null,                // Store (null = défaut)
    'prefix' => 'db-translatable',  // Préfixe des clés
    'ttl' => 3600,                  // Durée de vie (secondes)
],

Invalidation automatique

Le cache est automatiquement invalidé lors de :

  • Sauvegarde du modèle
  • Suppression du modèle
  • Suppression de traductions

Invalidation manuelle

// Via le service
app(\DbTranslatable\Support\TranslationCache::class)->forget($post);

// Ou forcer le rechargement
$post->loadMissing($post->getTranslationsRelationName());

💡 Cas d'usage avancés

Formulaire multi-langues

// Vue : afficher toutes les traductions
$translations = $post->getTranslations();

foreach ($translations as $locale => $data) {
    echo "<h3>$locale</h3>";
    echo "<input name='title[$locale]' value='{$data['title']}'>";
    echo "<textarea name='content[$locale]'>{$data['content']}</textarea>";
}

// Controller : sauvegarder
foreach ($request->input('title') as $locale => $title) {
    $post->translation($locale)->title = $title;
    $post->translation($locale)->content = $request->input("content.$locale");
}
$post->save();

API REST multilingue

// Endpoint : GET /api/posts/{id}?locale=fr
public function show(Post $post, Request $request)
{
    $locale = $request->input('locale', config('app.locale'));

    return [
        'id' => $post->id,
        'slug' => $post->slug,
        'title' => $post->translation($locale)->title,
        'content' => $post->translation($locale)->content,
        'created_at' => $post->created_at,
    ];
}

// Ou avec toutes les traductions
public function show(Post $post)
{
    return [
        'id' => $post->id,
        'slug' => $post->slug,
        'translations' => $post->getTranslations(),
        'available_locales' => $post->available_locales,
    ];
}

Migration progressive

// Étape 1 : Ajouter le trait et la propriété $translatable
class Post extends Model
{
    use HasTranslations;

    protected array $translatable = ['title', 'content'];
}

// Étape 2 : Créer la table de traductions
php artisan make:translation-migration Post
php artisan migrate

// Étape 3 : Migrer les données existantes
php artisan translations:migrate Post --locale=fr

// Étape 4 : (Optionnel) Supprimer les colonnes de la table principale
Schema::table('posts', function (Blueprint $table) {
    $table->dropColumn(['title', 'content']);
});

Recherche multi-critères

// Recherche complexe avec colonnes traduites et non traduites
$posts = Post::whereSmart('title', 'LIKE', '%Laravel%')
    ->whereSmart('status', 'published')
    ->whereBetweenSmart('created_at', [$start, $end])
    ->whereNotNullSmart('content')
    ->orderBySmart('title', 'asc')
    ->paginate(20);

Export de données

// Exporter toutes les traductions en CSV
$posts = Post::with('translations')->get();

$csv = [];
foreach ($posts as $post) {
    foreach ($post->getTranslations() as $locale => $data) {
        $csv[] = [
            'id' => $post->id,
            'locale' => $locale,
            'title' => $data['title'],
            'content' => $data['content'],
        ];
    }
}

Localisation dynamique par utilisateur

// Middleware
class SetUserLocale
{
    public function handle($request, Closure $next)
    {
        if (auth()->check()) {
            $locale = auth()->user()->preferred_locale ?? 'fr';
            db_translatable()->setCurrentLocale($locale);
        }

        return $next($request);
    }
}

// Utilisation
$post->title;  // Utilise automatiquement la locale de l'utilisateur

✅ Bonnes pratiques

1. Définir explicitement les champs traduisibles

// ✅ BON
protected array $translatable = ['title', 'content', 'description'];

// ❌ MAUVAIS
// Tout mettre sans réfléchir

2. Utiliser les scopes Smart pour les requêtes mixtes

// ✅ BON : détection automatique
Post::whereSmart('title', 'LIKE', '%Laravel%')
    ->whereSmart('status', 'published')
    ->get();

// ❌ MOINS BON : spécifier manuellement
Post::whereT('title', 'LIKE', '%Laravel%')
    ->where('status', 'published')
    ->get();

3. Eager loading des traductions

// ✅ BON : évite N+1
$posts = Post::with('translations')->get();

// ❌ MAUVAIS : N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->title;  // Query par itération
}

4. Utiliser withLocale pour les modifications groupées

// ✅ BON
$post->withLocale('en', function ($model) {
    $model->title = 'Title';
    $model->content = 'Content';
});

// ❌ MOINS BON
$post->useLocale('en');
$post->title = 'Title';
$post->content = 'Content';
$post->resetLocale();  // Oubli fréquent !

5. Configurer les fallbacks intelligemment

// ✅ BON : fallbacks logiques
'map_fallbacks' => [
    'en-GB' => ['en', 'fr'],
    'fr-CA' => ['fr', 'en'],
],

// ❌ MAUVAIS : boucles infinies
'map_fallbacks' => [
    'en' => ['fr'],
    'fr' => ['en'],  // Boucle !
],

6. Valider les locales en écriture

// Configuration
'allowed' => ['fr', 'en', 'es'],
'allow_unknown' => false,
'prevent_writing_unknown_locale' => true,

// Utilisation
try {
    $post->translation('de')->title = 'Test';  // Exception
} catch (InvalidArgumentException $e) {
    // Locale non autorisée
}

7. Utiliser les scopes de vérification

// ✅ BON : vérifier avant d'afficher
$posts = Post::hasTranslation('fr')
    ->whereTranslationComplete('fr')
    ->get();

// ❌ MAUVAIS : afficher et espérer
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->title ?? 'N/A';  // Beaucoup de 'N/A'
}

🧪 Tests

Le package utilise Pest + Orchestra Testbench :

composer test

Tester vos modèles

use Tests\TestCase;

class PostTranslationTest extends TestCase
{
    /** @test */
    public function it_can_save_translations()
    {
        $post = Post::create(['slug' => 'test']);

        $post->translation('fr')->title = 'Titre FR';
        $post->translation('en')->title = 'Title EN';
        $post->save();

        $this->assertEquals('Titre FR', $post->translation('fr')->title);
        $this->assertEquals('Title EN', $post->translation('en')->title);
    }

    /** @test */
    public function it_uses_fallbacks()
    {
        config(['db-translatable.locale.fallback' => ['en', 'fr']]);

        $post = Post::create(['slug' => 'test']);
        $post->translation('en')->title = 'English Title';
        $post->save();

        db_translatable()->setCurrentLocale('de');

        // Doit retourner la version anglaise (fallback)
        $this->assertEquals('English Title', $post->title);
    }
}

📄 Licence

Licence MIT. Développé par Boule Luciole.

🤝 Contribution

Les contributions sont les bienvenues ! Merci de :

  1. Forker le projet
  2. Créer une branche (git checkout -b feature/ma-fonctionnalite)
  3. Commiter vos changements (git commit -m 'Ajout de ma fonctionnalité')
  4. Pousser vers la branche (git push origin feature/ma-fonctionnalite)
  5. Ouvrir une Pull Request

📚 Ressources supplémentaires

DB-Translatable - Une solution professionnelle pour gérer les traductions dans Laravel. 🚀