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 :

  • Stocker toutes les traductions dans une seule colonne
  • Ajouter de nouvelles langues sans migration
  • Conserver la structure simple de la base de données
  • Utiliser les index JSON natifs de MySQL/MariaDB (si nécessaire)

  • 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

    <?php

    namespace 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

    <?php

    use 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 migrate

    Exé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-data

    Migrer un modèle spécifique

    php artisan translate:migrate-data --model=Post

    Mode 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 :

  • Nombre total d'enregistrements traités
  • Nombre d'enregistrements migrés
  • Nombre d'enregistrements ignorés (déjà migrés)
  • Nombre d'erreurs
  • Détails par modèle
  • 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 :

  • MigratePostTranslationsAction
  • MigrateProductTranslationsAction
  • MigratePageTranslationsAction
  • MigrateCategoryTranslationsAction
  • MigrateTagTranslationsAction
  • MigrateFeedbackTranslationsAction
  • MigrateRoadmapItemTranslationsAction
  • MigratePollTranslationsAction
  • Ces 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 traductions
  • mutateFormDataBeforeSave() / mutateFormDataBeforeCreate() : Prépare les données pour la sauvegarde
  • afterSave() / afterCreate() : Sauvegarde explicite des traductions
  • Exemple : 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.php
  • tests/Feature/Specifics/Shop/Models/ProductTranslationTest.php
  • tests/Feature/Models/PageTranslationTest.php
  • tests/Feature/Specifics/Shares/Models/CategoryTranslationTest.php
  • tests/Feature/Specifics/Shares/Models/TagTranslationTest.php
  • tests/Feature/Specifics/FeedbackHub/Models/FeedbackTranslationTest.php
  • tests/Feature/Specifics/FeedbackHub/Models/RoadmapItemTranslationTest.php
  • tests/Feature/Specifics/FeedbackHub/Models/PollTranslationTest.php
  • Tests de la commande de migration

    Le fichier tests/Feature/Console/Commands/TranslateMigrateDataCommandTest.php teste la commande de migration avec :

  • Mode dry-run
  • Migration d'un modèle spécifique
  • Migration de tous les modèles
  • Gestion des erreurs
  • Rapport de migration
  • Exécuter les tests

    Tous les tests de traduction

    php artisan test --filter=TranslationTest

    Tests d'un modèle spécifique

    php artisan test --filter=PostTranslationTest

    Tests 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 :

  • Vérifier que les traductions sont activées : translations_enabled()
  • Vérifier que le modèle est activé : translation_model_enabled('post')
  • Vérifier la locale courante : app()->getLocale()
  • Vérifier que les données sont bien en JSON dans la base de données
  • Problème : Erreur lors de la migration

    Solution :

  • Exécuter en mode dry-run : php artisan translate:migrate-data --dry-run
  • Vérifier les logs : storage/logs/laravel.log
  • Vérifier que les colonnes existent dans la base de données
  • Vérifier que les migrations ont été exécutées
  • Problème : Les données ne sont pas migrées

    Solution :

  • Vérifier que les données existent dans la base de données
  • Vérifier que les données ne sont pas déjà en JSON
  • Vérifier les erreurs dans le rapport de migration
  • Exécuter la migration pour un modèle spécifique : --model=Post
  • Problème : Fallback ne fonctionne pas

    Solution :

  • Vérifier la configuration : config('translatable.use_fallback')
  • Vérifier la locale de fallback : config('translatable.fallback_locale')
  • Vérifier que la traduction de fallback existe

  • Références

    Fichiers du projet

  • app/Specifics/Shares/Models/Concerns/ConditionallyTranslatable.php - Trait conditionnel
  • app/Services/TranslationMigrationService.php - Service de migration
  • app/Console/Commands/TranslateMigrateDataCommand.php - Commande de migration
  • app/Contracts/Actions/MigrateTranslationsActionInterface.php - Interface des Actions
  • app/Specifics/{Module}/Actions/Migrate{Entity}TranslationsAction.php - Actions de migration
  • Documentation externe

  • spatie/laravel-translatable
  • Documentation Spatie Translatable
  • Guides connexes

  • docs/guides/spatie-translatable-configuration.md - Configuration de base
  • docs/guides/filament-translatable-v5.md - Intégration Filament
  • docs/prd/epic-2-database-content-translation.md - PRD de l'Epic 2

  • Changelog

    Version 1.0 (2025-01-27)

  • ✅ Migration de 8 modèles vers le système de traduction
  • ✅ Commande de migration globale créée
  • ✅ Tests complets pour tous les modèles
  • ✅ Documentation complète

Prendre rendez-vous