angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Affiner le multilinguisme : ajout d'un repli linguistique en option

Lorsque la langue de repli est désactivée, l'élément n'est pas affiché si la traduction n'est pas disponible.

11 septembre 2019
post main image
unsplash.com/@miabaker

Dans un article précédent, j'ai décrit la première version de la base de données multilingue utilisée sur ce site. Pour chaque table qui a des champs qui doivent être traduits, nous ajoutons une table'traduction' avec ces champs. J'ai aussi implémenté un repli de langue : si un élément, comme un billet de blog, n'existe pas dans la langue sélectionnée, alors l'élément de la langue par défaut (pour tout le système) est affiché. Cela fonctionne très bien, mais maintenant je veux ajouter une option de repli permettant à certains messages d'apparaître uniquement en espagnol, d'autres uniquement en français, etc.

Nous pourrions facilement implémenter ceci en changeant le langage de repli vers un langage inconnu / inexistant. Maintenant, si nous ajoutons un poste en espagnol, le poste dans d'autres langues n'est pas disponible, juste ce que nous voulons. Le seul problème, c'est que nous n'avons plus maintenant de langue de repli. Est-ce un problème ?

Considérons le cas suivant. Nous avons un poste en espagnol, et nous voulons que le poste inexistant en français revienne à la langue par défaut, l'anglais. Mais nous ne voulons pas que le message inexistant en allemand revienne à la langue par défaut. Cela n'est pas possible avec l'implémentation actuelle. À quelle fréquence une telle condition se manifestera-t-elle ? Pas souvent, mais je peux imaginer que cela peut arriver, et je veux me préparer à cela. Alors, comment pouvons-nous mettre en œuvre cela ?

Quelques options de repli

Il existe de nombreuses options de repli possibles pour une base de données multilingue. On a toujours besoin d'un interrupteur pour dire :

  • retour à l'élément de langue par défaut si aucune traduction n'est présente pour la langue sélectionnée
  • ne pas revenir en arrière, ce qui signifie que cet élément n'est pas disponible dans la langue sélectionnée

Pour le repli, nous pouvons avoir des options comme :

  • repli du niveau d'enregistrement, p. ex. titre, description
  • repli au niveau du champ, p. ex. titre

Nous pouvons aussi l'avoir :

  • une seule solution de repli, p. ex. il n'y a qu'une seule langue de repli
  • Remplacement récursif, par exemple, s'il n'existe pas en portugais utiliser l'espagnol, s'il n'existe pas en espagnol utiliser l'anglais

Le langage de repli peut l'être :

  • l'échelle du système
  • à un niveau record
  • sur le terrain

Sans entrer dans les détails, vous pouvez imaginer qu'en implémentant un maximum de flexibilité, toutes options confondues, nous allons créer un système très complexe qui nécessitera des requêtes complexes, en fait, il peut être nécessaire de construire un préprocesseur complexe pour le front-end pour éviter des vues lentes des pages en raison de toutes les options de traitement du langage.

Faire des choix

Retour à l'essentiel. Je veux pour ce site web :

  • Prise en charge de plusieurs langues
  • Il existe une langue par défaut à l'échelle du système
  • Il existe un langage de repli à l'échelle du système
  • L'action par défaut doit être un repli de la langue
  • Par élément de langue, il doit être possible de désactiver le repli de langue, rendant l'élément indisponible s'il n'est pas publié.

J'ai défini les langues suivantes :

  • language_selected : langue utilisée
  • language_default : la langue par défaut, un réglage statique à l'échelle du système
  • language_fallback : la langue de repli, un paramètre dynamique à l'échelle du système basé sur la langue sélectionnée.

La plupart du temps, la langue de repli sera la langue par défaut, mais à l'avenir, nous pourrions également la changer pour une autre langue :

Exemple n° 1 : repli de fr-Be vers fr-FR

  • langue par défaut : en-US
  • langue sélectionnée : fr-BE
  • langue de repli : fr-FR

Exemple #2 : es-ES avec repli sur en-GB

  • langue par défaut : fr-FR
  • langue sélectionnée : es-ES
  • langue de repli : fr-FR

Mise en œuvre

Je veux que les requêtes ne ralentissent pas le système, mais si c'est le cas, nous pouvons toujours ajouter'mise en cache des résultats de requête' et/ou prétraitement des requêtes.

Pour l'instant, il y a deux tables des matières :

  • ContenuItem
  • ContentItemTranslation

Nous commençons toujours par sélectionner un ou plusieurs enregistrements dans ContentItem. Si l'enregistrement ContentItemTranslation n'est pas présent (ou publié) pour la langue sélectionnée, nous devons effectuer un repli, mais seulement si ce repli est activé pour cet élément. Il est temps de regarder la requête utilisée pour récupérer les éléments. Nous distinguons deux possibilités :

  • ContentItemTranslation record n'existe pas
  • ContentItemTranslation record existe

Dans les deux cas nous pouvons faire une jointure d'union, la première partie obtient l'enregistrement pour la langue sélectionnée et la deuxième partie obtient la langue pour la langue par défaut. La difficulté est de savoir comment désactiver le repli. Lorsque l'enregistrement ContentItemTranslation existe, nous pouvons mettre un drapeau'do_not_fallback' ici mais que faire si l'enregistrement ContentItemTranslation n'existe pas ? Ensuite, nous avons besoin d'un autre drapeau spécifique à une langue quelque part et ce drapeau peut ou non exister.

Une façon de le faire est d'ajouter une autre table ContentItemTranslationDoNotFallback contenant un seul drapeau = do_not_fallback. Comme avec ContentItemTranslation, les enregistrements ContentItemTranslationDoNotFallback peuvent être présents ou non. Ensuite, nous avons trois tables :

  • ContenuItem
  • ContentItemTranslation
  • ContentItemTranslationDoNoNotFallback

Nous voulons que l'action par défaut soit de revenir à la langue de repli si l'enregistrement de traduction n'existe pas. Tout cela semble assez simple. Mais cela introduit aussi un enregistrement supplémentaire d'une nouvelle table qui peut être présente ou non.

Une autre façon d'obtenir ce que nous voulons est d'ajouter l'option'do_not_fallback' à l'enregistrement de traduction. Je veux dire, si nous devons ajouter un enregistrement à une nouvelle table ContentItemTranslationDoNotFallback pour indiquer'do_not_fallback' alors pourquoi ne pas utiliser l'enregistrement de table ContentItemTranslation pour ce but ? S'il n'existe pas, nous l'ajoutons, sinon nous l'utilisons simplement.

Dans ce cas, l'action par défaut est toujours de revenir à la langue de repli si l'enregistrement'traduction' n'existe pas. Nous insérons maintenant un enregistrement de traduction non seulement lorsque nous avons une valeur de traduction, mais aussi lorsque nous voulons un repli pour cet enregistrement. Dans l'enregistrement de traduction, nous utilisons l'indicateur publié / actif pour indiquer si la traduction existe. Le tableau ci-dessous montre les actions pour la langue sélectionnée.

Table
des matièresTraduction d'éléments
Publication de ContentItemTranslation
ContentItemTranslation
do_not_fallback_fallback
Action
1 N'existe pas - - Pas de valeur traduite, repli
2 Existe Vrai Vrai ou faux Utiliser la valeur convertie
3

Existe

Faux Faux Pas de valeur traduite, repli
4 Existe Faux Vrai Pas de valeur traduite, pas de repli, article non disponible

SQL question

Ci-dessous se trouve une implémentation possible SQL d'une requête. Notez que j'utilise SELECTles's' et UNION ici qui permettent une sélection facile des objets lors de l'utilisation d'une SQLAlchemy requête. Dans l'implémentation actuelle, j'ai deux drapeaux, supprimé et statut, qui doivent tous deux être Faux. content_item_parent_id = 0 signifie l'enregistrement de traduction principal.

SET @language_fallback_id = 1;
SET @language_selected_id = 2;

### [1] = fallback: translation record does not exist

(
SELECT 
  ci.id ci_id, cit.id cit_id, cit.title cit_title
FROM content_item ci, content_item_translation cit
WHERE 
  ci.content_item_type = 1
  AND ci.content_item_parent_id = 0
  AND ci.published = 1
  AND cit.content_item_id = ci.id
  AND cit.content_item_parent_id = 0
  AND cit.published = 1
  AND cit.language_id = @language_fallback_id
  AND NOT EXISTS
    (
    # return 1 when exists
    SELECT 1 
    FROM content_item_translation cit2
    WHERE 
          cit2.content_item_id = ci.id
      AND cit2.content_item_parent_id = 0
      AND cit2.language_id = @language_selected_id
      )
)

UNION ALL

### [2] = use translation: translation record exists and published = True

(
SELECT 
  ci.id ci_id, cit.id cit_id, cit.title cit_title
FROM content_item ci, content_item_translation cit
WHERE 
  ci.content_item_type = 1
  AND ci.content_item_parent_id = 0
  AND ci.published = 1
  AND cit.content_item_id = ci.id
  AND cit.content_item_parent_id = 0
  AND cit.published = 1
  AND cit.language_id = @language_selected_id
)

UNION ALL

### [3] = fallback: translation record exists and published = False & do_not_fallback = False

(
SELECT 
  ci.id ci_id, cit.id cit_id, cit.title cit_title
FROM content_item ci, content_item_translation cit
WHERE 
  ci.content_item_type = 1
  AND ci.content_item_parent_id = 0
  AND ci.published = 1
  AND cit.content_item_id = ci.id
  AND cit.content_item_parent_id = 0
  AND cit.published = 1
  AND cit.language_id = @language_fallback_id
  AND EXISTS
    (
    # return 1 when published = False & do_not_fallback = False
    SELECT 1 
    FROM content_item ci2, content_item_translation cit2
    WHERE 
          ci2.id = ci.id
      AND ci2.content_item_type = 1
      AND ci2.content_item_parent_id = 0
      AND ci2.published = 1
      AND cit2.content_item_id = ci2.id
      AND cit2.content_item_parent_id = 0
      AND cit2.published = 0
      AND cit2.do_not_fallback = 0
      AND cit2.language_id = @language_selected_id
      )
)

ORDER BY ci_id DESC;

Pour SQLAlchemy le moment, j'ai décidé d'utiliser raw sql pour obtenir le nombre et les ids pour la pagination et ensuite utiliser ces ids pour sélectionner les objets en utilisant une deuxième requête. Oui, je suis un peu fatigué de traduire SQL tout SQLAlchemy le temps, cela prend beaucoup de temps et pour quoi faire ? Néanmoins, je crois que l'utilisation ORM peut aider beaucoup à réduire le code, mais pas tout le temps. Il s'agit donc de l'utiliser de la bonne façon.

Administrateur

Bien sûr, l'administrateur doit être en mesure de voir le statut des éléments de contenu. L'état du contenu de la traduction est indiqué par langue comme suit :

Publié

Non publié, repli

Non publié, pas de repli, pas disponible

Résumé

Nous avons ajouté l'option'do_not_fallback' au contenu de notre base de données. Cela a introduit plus de complexité, mais les frais généraux supplémentaires ne semblent pas vraiment énormes. Par rapport à la situation où nous avons toujours eu recours à la langue par défaut, les changements sont les suivants :

  • Le nombre d'enregistrements (items) n'est plus le nombre d'enregistrements pour la langue par défaut.
  • Lorsque nous affichons un élément et que nous changeons la langue d'un élément qui n'a pas de traduction, nous devons alors montrer que l'élément n'est pas disponible dans la langue sélectionnée.
  • La fonction'les plus consultés' (billets de blog), la fonction'recherche' (billets de blog), etc. doivent maintenant aussi filtrer les articles non disponibles.

Encore une fois beaucoup de travail, mais je pense que c'est maintenant la fonctionnalité multilingue que je peux utiliser pour le reste de ce projet.

Liens / crédits

Fallback
https://www.i18next.com/principles/fallback

Language Fallback
http://sitecore-masters.com/en/language-fallback/

Multi Language i18n & Localization in Pimcore
https://pimcore.com/docs/5.x/Development_Documentation/Multi_Language_i18n/index.html

En savoir plus...

Multilanguage

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.