Guide : Traduction du contenu en base de données
Version : 1.0 Date : 2025-01-27 Objectif : Documenter l'implémentation de la traduction du contenu stocké en base de données
Vue d'ensemble
Qu'est-ce que la traduction du contenu en base de données ?
La traduction du contenu en base de données permet de stocker plusieurs versions linguistiques d'un même contenu directement dans les colonnes de la base de données, au format JSON. Contrairement aux solutions qui créent des tables de traduction séparées, cette approche stocke toutes les traductions dans une seule colonne JSON.
Modèles traduisibles
Les modèles suivants ont été migrés vers le système de traduction :
- Post (Blog) :
title,slug,excerpt,content - Product (Shop) :
name,slug,description - Page :
title,slug,content - Category (Shares) :
name,slug,description - Tag (Shares) :
name,slug - Feedback (FeedbackHub) :
title,description - RoadmapItem (FeedbackHub) :
title,description - Poll (FeedbackHub) :
title
Architecture du stockage
Les traductions sont stockées au format JSON dans les colonnes de la base de données :
{
"fr": "Titre en français",
"es": "Título en español"
}
Cette approche permet de :
Utilisation dans les modèles
Trait ConditionallyTranslatable
Tous les modèles traduisibles utilisent le trait ConditionallyTranslatable qui étend HasTranslations de Spatie et permet d'activer/désactiver les traductions par modèle via la configuration.
Exemple : Modèle Post
<?phpnamespace App\Specifics\Blog\Models;
use App\Specifics\Shares\Models\Concerns\ConditionallyTranslatable;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use ConditionallyTranslatable;
protected function casts(): array
{
return [
'title' => 'array', // Cast to JSON
'slug' => 'array', // Cast to JSON
'excerpt' => 'array', // Cast to JSON
'content' => 'array', // Cast to JSON
];
}
protected function getTranslatableFields(): array
{
return ['title', 'slug', 'excerpt', 'content'];
}
}
Méthodes disponibles
Récupérer une traduction
// Récupérer la traduction pour la locale courante
$post->title; // Retourne "Titre en français" si locale = 'fr'// Récupérer une traduction spécifique
$post->getTranslation('title', 'es'); // Retourne "Título en español"
// Récupérer toutes les traductions
$post->getTranslations('title'); // Retourne ['fr' => '...', 'es' => '...']
Définir une traduction
// Définir une traduction
$post->setTranslation('title', 'es', 'Nouveau titre en espagnol');
$post->save();// Définir plusieurs traductions
$post->setTranslations('title', [
'fr' => 'Titre français',
'es' => 'Título español',
]);
$post->save();
Supprimer une traduction
// Supprimer une traduction pour une locale spécifique
$post->forgetTranslation('title', 'es');
$post->save();
Vérifier l'existence d'une traduction
// Vérifier si une traduction existe
$post->hasTranslation('title', 'es'); // Retourne true/false
Fallback automatique
Si une traduction n'existe pas pour la locale courante, le système utilise automatiquement la locale de fallback (configurée dans config/translatable.php).
// Si la traduction ES n'existe pas, retourne la traduction FR
app()->setLocale('es');
$post->title; // Retourne la traduction FR si ES n'existe pas
Migrations
Structure des migrations
Chaque modèle traduisible a une migration qui convertit les colonnes de type string/text en json.
Exemple : Migration pour Post
<?phpuse Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$defaultLocale = config('translations.default_locale', 'fr');
if (Schema::hasTable('posts')) {
// Migrer les données existantes
$posts = DB::table('posts')->get();
foreach ($posts as $post) {
$titleData = json_decode($post->title, true);
if (! is_array($titleData)) {
DB::table('posts')
->where('id', $post->id)
->update([
'title' => json_encode([$defaultLocale => $post->title ?? '']),
]);
}
}
// Modifier le type de colonne
Schema::table('posts', function (Blueprint $table) {
$table->json('title')->change();
$table->json('slug')->change();
$table->json('excerpt')->nullable()->change();
$table->json('content')->nullable()->change();
});
}
}
public function down(): void
{
// Conversion inverse (JSON → string)
// ...
}
};
Exécuter les migrations
Exécuter toutes les migrations
php artisan migrateExécuter une migration spécifique
php artisan migrate --path=database/migrations/2026_01_20_110118_add_translations_to_posts_table.php
Migration des données existantes
Commande Artisan
Une commande Artisan est disponible pour migrer les données existantes vers le format JSON :
Migrer tous les modèles
php artisan translate:migrate-dataMigrer un modèle spécifique
php artisan translate:migrate-data --model=PostMode dry-run (simulation sans modification)
php artisan translate:migrate-data --dry-run
Options disponibles
--dry-run : Simule la migration sans modifier les données--model={ModelName} : Migre uniquement un modèle spécifique (Post, Product, Page, Category, Tag, Feedback, RoadmapItem, Poll)Rapport de migration
La commande génère un rapport détaillé avec :
Exemple de sortie :
📊 Rapport de migration+------------------+--------+
| Statistique | Valeur |
+------------------+--------+
| Modèles traités | 8 |
| Enregistrements | 150 |
| Migrés | 120 |
| Ignorés | 30 |
| Erreurs | 0 |
+------------------+--------+
Actions de migration
Chaque modèle a une Action de migration dédiée dans app/Specifics/{Module}/Actions/Migrate{Entity}TranslationsAction.php :
MigratePostTranslationsActionMigrateProductTranslationsActionMigratePageTranslationsActionMigrateCategoryTranslationsActionMigrateTagTranslationsActionMigrateFeedbackTranslationsActionMigrateRoadmapItemTranslationsActionMigratePollTranslationsActionCes Actions implémentent l'interface MigrateTranslationsActionInterface et sont orchestrées par le TranslationMigrationService.
Factories et Seeders
Factories
Les factories génèrent automatiquement des données traduites si les traductions sont activées :
// Factory génère automatiquement des traductions JSON
$post = Post::factory()->create();
// $post->title = ['fr' => '...', 'es' => '...']// Désactiver les traductions pour générer des données simples
Config::set('translations.models.post', false);
$post = Post::factory()->create();
// $post->title = 'Simple title'
Seeders
Les seeders doivent être adaptés pour créer des données traduites :
use App\Specifics\Blog\Models\Post;Post::create([
'title' => [
'fr' => 'Titre en français',
'es' => 'Título en español',
],
'slug' => [
'fr' => 'titre-en-francais',
'es' => 'titulo-en-espanol',
],
// ...
]);
Intégration avec Filament
TranslatableField
Le helper TranslatableField permet de créer des champs traduisibles dans les formulaires Filament :
use App\Filament\Support\TranslatableField;TranslatableField::makeTextInput('title', 'Titre', fn ($field) =>
$field->required()->maxLength(255)
)
LocaleAction
Le helper LocaleAction ajoute un sélecteur de langue dans l'en-tête des pages Filament :
use App\Filament\Support\LocaleAction;protected function getHeaderActions(): array
{
return [
...(LocaleAction::make() ?? []),
];
}
Pages Create et Edit
Les pages Create et Edit doivent implémenter les méthodes suivantes :
mutateFormDataBeforeFill() : Hydrate le formulaire avec les traductionsmutateFormDataBeforeSave() / mutateFormDataBeforeCreate() : Prépare les données pour la sauvegardeafterSave() / afterCreate() : Sauvegarde explicite des traductionsExemple : EditPost
protected function mutateFormDataBeforeFill(array $data): array
{
if (translation_model_enabled('post')) {
$translatableFields = ['title', 'slug', 'excerpt', 'content'];
$locales = available_locales(); foreach ($translatableFields as $field) {
unset($data[$field]);
$translations = $this->record->getTranslations($field);
foreach ($locales as $locale) {
$key = "{$field}_{$locale}";
$data[$key] = $translations[$locale] ?? null;
}
}
}
return $data;
}
Tests
Tests de traduction
Chaque modèle traduisible a un fichier de test dédié dans tests/Feature/ :
tests/Feature/Specifics/Blog/Models/PostTranslationTest.phptests/Feature/Specifics/Shop/Models/ProductTranslationTest.phptests/Feature/Models/PageTranslationTest.phptests/Feature/Specifics/Shares/Models/CategoryTranslationTest.phptests/Feature/Specifics/Shares/Models/TagTranslationTest.phptests/Feature/Specifics/FeedbackHub/Models/FeedbackTranslationTest.phptests/Feature/Specifics/FeedbackHub/Models/RoadmapItemTranslationTest.phptests/Feature/Specifics/FeedbackHub/Models/PollTranslationTest.phpTests de la commande de migration
Le fichier tests/Feature/Console/Commands/TranslateMigrateDataCommandTest.php teste la commande de migration avec :
Exécuter les tests
Tous les tests de traduction
php artisan test --filter=TranslationTestTests d'un modèle spécifique
php artisan test --filter=PostTranslationTestTests de la commande de migration
php artisan test --filter=TranslateMigrateDataCommandTest
Activation/Désactivation
Par modèle
Les traductions peuvent être activées/désactivées par modèle via la configuration :
Fichier : config/translations.php
'models' => [
'post' => true, // Activé
'product' => true, // Activé
'page' => false, // Désactivé
],
Fichier : .env
TRANSLATIONS_MODELS_POST=true
TRANSLATIONS_MODELS_PRODUCT=true
TRANSLATIONS_MODELS_PAGE=false
Vérifier l'état
// Vérifier si les traductions sont activées pour un modèle
translation_model_enabled('post'); // Retourne true/false// Vérifier si les traductions sont activées globalement
translations_enabled(); // Retourne true/false
Bonnes pratiques
1. Toujours utiliser les accesseurs
// ✅ Bon
$post->title; // Retourne automatiquement la traduction de la locale courante// ❌ Éviter
$post->getRawOriginal('title'); // Retourne le JSON brut
2. Vérifier l'existence avant d'accéder
// ✅ Bon
if ($post->hasTranslation('title', 'es')) {
$title = $post->getTranslation('title', 'es');
}// ❌ Éviter
$title = $post->getTranslation('title', 'es'); // Peut retourner null
3. Utiliser le fallback
// ✅ Bon - Le fallback est automatique
$post->title; // Retourne FR si ES n'existe pas// ✅ Bon - Fallback explicite
$post->getTranslation('title', 'es', 'fr'); // Retourne FR si ES n'existe pas
4. Sauvegarder après modification
// ✅ Bon
$post->setTranslation('title', 'es', 'Nouveau titre');
$post->save(); // Important !// ❌ Oubli fréquent
$post->setTranslation('title', 'es', 'Nouveau titre');
// Oubli de save() → modifications perdues
5. Utiliser les factories pour les tests
// ✅ Bon
$post = Post::factory()->create(); // Génère automatiquement des traductions// ❌ Éviter
$post = Post::create([
'title' => ['fr' => '...', 'es' => '...'], // Répétitif
]);
Dépannage
Problème : Les traductions ne s'affichent pas
Solution :
translations_enabled()translation_model_enabled('post')app()->getLocale()Problème : Erreur lors de la migration
Solution :
php artisan translate:migrate-data --dry-runstorage/logs/laravel.logProblème : Les données ne sont pas migrées
Solution :
--model=PostProblème : Fallback ne fonctionne pas
Solution :
config('translatable.use_fallback')config('translatable.fallback_locale')Références
Fichiers du projet
app/Specifics/Shares/Models/Concerns/ConditionallyTranslatable.php - Trait conditionnelapp/Services/TranslationMigrationService.php - Service de migrationapp/Console/Commands/TranslateMigrateDataCommand.php - Commande de migrationapp/Contracts/Actions/MigrateTranslationsActionInterface.php - Interface des Actionsapp/Specifics/{Module}/Actions/Migrate{Entity}TranslationsAction.php - Actions de migrationDocumentation externe
Guides connexes
docs/guides/spatie-translatable-configuration.md - Configuration de basedocs/guides/filament-translatable-v5.md - Intégration Filamentdocs/prd/epic-2-database-content-translation.md - PRD de l'Epic 2