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
Requires
- php: ^8.2
- illuminate/database: ^11.42|^12.0
- illuminate/support: ^11.42|^12.0
Requires (Dev)
- orchestra/testbench: ^9.15
- pestphp/pest: ^4.1
- pestphp/pest-plugin-laravel: ^4.0
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
- Configuration
- Mise en place
- Gestion de la langue
- Manipulation des données
- Suppression de traductions
- Informations et utilitaires
- Query Scopes
- Migration des données existantes
- Commandes Artisan
- Fallbacks et cascade
- Gestion du cache
- Cas d'usage avancés
- Bonnes pratiques
- Tests
🚀 Installation
-
Installer via Composer :
composer require bouleluciole/db-translatable
-
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_translationsavec 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 :
- La commande lit toutes les lignes de la table principale
- Pour chaque ligne, elle crée une traduction dans la locale spécifiée
- Les valeurs sont copiées depuis les colonnes traduisibles
- Utilise
updateOrInsertpour é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 :
- Forker le projet
- Créer une branche (
git checkout -b feature/ma-fonctionnalite) - Commiter vos changements (
git commit -m 'Ajout de ma fonctionnalité') - Pousser vers la branche (
git push origin feature/ma-fonctionnalite) - Ouvrir une Pull Request
📚 Ressources supplémentaires
DB-Translatable - Une solution professionnelle pour gérer les traductions dans Laravel. 🚀