# Indexer une table de traductions maison par locale et identifiant d’objet

> Concevoir une table de traductions faite maison qui reste rapide à grande échelle, sans extension du marché, en misant sur les bons index.

- Auteur : Clément Hadrot
- Publié le : 2024-01-17
- Mis à jour le : 2024-01-17
- Catégorie : Multilingue
- URL : https://wpmoderne.dev.wordpress-developpement.fr/multilingue/indexer-table-traductions-locale-object-id/

## L’essentiel

- Une clé composite (object_id, locale) évite les scans de table
- Un index couvrant élimine les lookups supplémentaires
- La normalisation coûte cher en jointures si mal pensée

Comparons deux approches, avant d'entrer dans le détail : stocker les traductions dans une table dédiée avec des clés étrangères vers `wp_posts`, ou dupliquer chaque contenu comme un article séparé relié par une métadonnée « groupe de traduction ». La deuxième option, c'est ce que font la plupart des extensions du marché. La première, plus légère, convient bien à un projet qui n'a pas besoin d'un éditeur de traduction complet, seulement d'une correspondance fiable entre un contenu et ses variantes linguistiques.

Ce choix architectural devient déterminant dès que le volume grossit. Une table mal indexée reste invisible sur un site de démonstration à cinquante articles, puis se met à ralentir chaque page dès que le catalogue dépasse quelques milliers d'entrées. Voici comment construire cette table pour qu'elle tienne la charge sans jamais devenir le goulot d'étranglement du site.

## La structure minimale

Une table de traduction maison a rarement besoin de plus de quatre colonnes : un identifiant de groupe (pour relier les variantes entre elles), l'identifiant de l'objet WordPress concerné, sa locale, et éventuellement un type d'objet si le projet mélange articles, pages et taxonomies dans la même logique.

```
CREATE TABLE wp_translation_map (
    group_id   BIGINT UNSIGNED NOT NULL,
    object_id  BIGINT UNSIGNED NOT NULL,
    object_type VARCHAR(20) NOT NULL DEFAULT 'post',
    locale     VARCHAR(10) NOT NULL,
    PRIMARY KEY (object_type, object_id),
    KEY idx_group_locale (group_id, locale)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

La clé primaire composite sur `(object_type, object_id)` garantit qu'un même objet n'appartient jamais à deux groupes de traduction en même temps — une contrainte qu'on regrette amèrement de ne pas avoir posée dès le départ, le jour où un script de migration crée des doublons silencieux.

## L'index qui change tout : idx_group_locale

La requête la plus fréquente dans ce genre de système ressemble à : « donne-moi la version allemande de l'article dont le groupe de traduction est 42 ». Sans index sur `(group_id, locale)`, MySQL doit parcourir toutes les lignes du groupe pour trouver la bonne locale, ce qui reste rapide sur un groupe de trois langues mais devient coûteux si l'on multiplie les scans sur une page qui affiche vingt liens de traduction (un sélecteur de langue complet, par exemple).

```
SELECT object_id
FROM wp_translation_map
WHERE group_id = 42 AND locale = 'de';
```

> L'essentiel à retenir : Une clé composite (object_id, locale) évite les scans de table ; Un index couvrant élimine les lookups supplémentaires ; La normalisation coûte cher en jointures si mal pensée

Avec l'index couvrant, MySQL peut répondre directement depuis l'index sans toucher la table de données (un « index-only scan »), ce qui divise le temps de réponse par un facteur observable dès que la table dépasse la centaine de milliers de lignes.

## Arborescence logique du système

Sur un projet récent (un configurateur de formations disponible en six langues), voici comment les pièces s'articulent entre la table maison et les tables natives de WordPress :

```
wp_posts
 └─ post_id 501 (fr, publié)
 └─ post_id 502 (en, publié)
 └─ post_id 503 (de, brouillon)

wp_translation_map
 └─ group_id 42
     ├─ object_id 501, locale fr
     ├─ object_id 502, locale en
     └─ object_id 503, locale de
```

Le champ `group_id` n'a pas besoin d'exister comme entité à part entière : c'est simplement le plus petit `object_id` du groupe, ou un identifiant généré une seule fois à la création de la première version d'un contenu.

## Éviter la jointure systématique

Un piège classique consiste à vouloir résoudre la traduction directement dans la requête principale d'affichage d'une liste d'articles, via une jointure sur `wp_translation_map`. Cela fonctionne, mais alourdit chaque `WP_Query` du site, y compris celles qui n'ont jamais besoin de connaître les traductions (un flux RSS, une recherche interne). Mieux vaut résoudre la traduction à la demande, uniquement quand le sélecteur de langue ou un lien croisé en a réellement besoin.

- Requête de lecture isolée, appelée uniquement pour le sélecteur de langue
- Résultat mis en cache via `wp_cache_set()` avec le `group_id` comme clé
- Invalidation du cache sur les hooks `save_post` et `before_delete_post`

## Nettoyage et intégrité référentielle

MySQL ne connaît pas de contrainte de clé étrangère native vers `wp_posts` sans configuration supplémentaire (et beaucoup d'hébergeurs mutualisés désactivent InnoDB strict pour ce genre de contrainte). Il faut donc prévoir un nettoyage explicite : un hook sur `before_delete_post` qui supprime la ligne correspondante dans `wp_translation_map`, et idéalement une commande WP-CLI personnalisée pour auditer les lignes orphelines de temps en temps.

> Une table de traduction sans nettoyage automatique fonctionne parfaitement pendant un an, puis accumule des références vers des articles supprimés jusqu'au jour où le sélecteur de langue affiche un lien mort.

## Pour aller plus loin

Cette architecture tient sa promesse tant que le nombre de locales par groupe reste raisonnable (moins d'une dizaine). Au-delà, ou si le projet a besoin d'un vrai flux de relecture éditoriale par langue, il devient plus rationnel d'évaluer une extension établie plutôt que de continuer à enrichir un système maison. La table à quatre colonnes reste un excellent point de départ : simple à auditer, facile à migrer, et suffisamment rapide pour ne jamais devenir le sujet de la prochaine réunion de debug.
