web-dev-qa-db-fra.com

Base de données multilingue, avec repli par défaut

J'ai une question qui, je le sais, a été largement débattue, mais à mon avis, il y a un aspect qui doit encore être clarifié.

Je crée une application web avec une base de données multilingue, j'ai déjà trouvé des articles de bonnes pratiques (comme this ) et des réponses ici en débordement de pile comme this .

J'ai donc décidé d'utiliser une table principale avec les ID de mes articles et une autre table avec la traduction pour chaque article, disons, par exemple

Content
ContentTranslation

ou

Category
CategoryTranslation

etc.

En ce moment ce que je fais? Je récupère simplement les éléments de la base de données avec toutes les traductions, puis j'itère au-dessus de chacun pour rechercher la traduction correcte en fonction du local de l'utilisateur actuel, et si je trouve le local correct, je mets dans l'objet principal cette traduction pour la page pour rendre, sinon je reçois juste la traduction qui est signalée comme "par défaut".

Avec de grandes quantités d'objets et de traductions, cependant, le temps de réponse du serveur peut augmenter et même si l'utilisateur ne le remarque pas, je ne le veux pas.

Existe-t-il également une bonne pratique pour ce cas d'utilisation? Par exemple, certaines requêtes spécifiques qui disent "choisissez la traduction avec les paramètres régionaux" it "mais si vous ne la trouvez pas, obtenez simplement celle avec l'indicateur" default "défini?

Maintenant, pour la technologie, j'utilise Spring MVC avec Hibernate et JPA (au moyen de JPARepository).

Mes objets étendent tous une classe traduisible de base que j'ai créée de cette façon

@MappedSuperclass
public abstract class Translatable<T extends Translation> extends BaseDTO {

    private static final long serialVersionUID = 562001309781752460L;

    private String title;

    @OneToMany(fetch=FetchType.EAGER, orphanRemoval=true, cascade=CascadeType.ALL)
    private Set<T> translations = new HashSet<T>();

    @Transient private T currentLocale;

    public void addLocale(T translation, boolean edit) {
        if (!edit)
            getTranslations().add(translation);
    }

    public void remLocale(String locale) {
        T tr = null;
        for (T candidate: getTranslations()) {
            if (candidate.getLocale().equals(locale))
                tr = candidate;
        }

        getTranslations().remove(tr);
    }

    public T getLocaleFromString(String locale) {
        if (locale == null)
            return null;
        for (T trans: translations) {
            if (trans.getLocale().equals(locale))
                return trans;
        }
        return null;
    }

    public T getDefaultLocale() {
        for (T tr: translations) {
            if (tr.isDefaultLocale())
                return tr;
        }
        return null;
    }

    public Set<T> getTranslations() {
        return translations;
    }

    public void setTranslations(Set<T> translations) {
        this.translations = translations;
    }

    public T getCurrentLocale() {
        return currentLocale;
    }

    public void setCurrentLocale(T currentLocale) {
        this.currentLocale = currentLocale;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

Donc, dans mon contrôleur, j'itère les traductions, trouve celle qui a les bons paramètres régionaux et remplis la propriété "currentLocale", dans ma page, je prends cela et l'utilisateur obtient la langue correcte comme prévu.

J'espère avoir été clair et pas en désordre, mais si vous avez besoin de plus d'informations, je serai heureux de vous en dire plus.

19
Luca

Quelques notes d'avance:

  • ma réponse est plus un ajout à ma réponse à cette question , où vous avez ajouté un commentaire qui a ensuite conduit à cette question
  • dans ma réponse, j'utilise C # et MS SQL Server (et je laisserai de côté tout code spécifique de mappage OR)

Dans mes applications, j'utilise deux approches différentes pour charger des données multilingues, selon le cas d'utilisation:

Administration/CRUD

Dans le cas où l'utilisateur saisit des données ou modifie des données existantes (par exemple, un produit avec ses traductions), j'utilise la même approche que vous avez montrée ci-dessus dans votre question, par exemple:

public class Product
{
    public int ID {get; set;}
    public string SKU {get; set;}
    public IList<ProductTranslation> Translations {get; set;}
}
public class ProductTranslation
{
    public string Language {get; set;}
    public bool IsDefaultLanguage {get; set;}
    public string Title {get; set;}
    public string Description {get; set;}
}

C'est à dire. Je laisserai le mappeur OR charger les instances de produit avec toutes leurs traductions jointes. J'itère ensuite les traductions et choisis celles dont j'ai besoin.

Front-end/lecture seule

Dans ce cas, qui est principalement du code frontal, où j'affiche généralement des informations à l'utilisateur (de préférence dans la langue de l'utilisateur), j'utilise une approche différente:

Tout d'abord, j'utilise un modèle de données différent qui ne prend pas en charge/ne connaît pas la notion de traductions multiples. Au lieu de cela, il s'agit simplement de la représentation d'un produit dans la "meilleure" langue pour l'utilisateur actuel:

public class Product
{
    public int ID {get; set;}
    public string SKU {get; set;}

    // language-specific properties
    public string Title {get; set;}
    public string Description {get; set;}
}

Pour charger ces données, j'utilise différentes requêtes (ou procédures stockées). Par exemple. pour charger un produit avec l'ID @Id dans la langue @Language, J'utiliserais la requête suivante:

SELECT
    p.ID,
    p.SKU,
    -- get title, description from the requested translation,
    -- or fall back to the default if not found:
    ISNULL(tr.Title, def.Title) Title,
    ISNULL(tr.Description, def.Description) Description
  FROM Products p
  -- join requested translation, if available:
  LEFT OUTER JOIN ProductTranslations tr
    ON p.ID = tr.ProductId AND tr.Language = @Language
  -- join default language of the product:
  LEFT OUTER JOIN ProductTranslations def
    ON p.ID = def.ProductId AND def.IsDefaultLanguage = 1
  WHERE p.ID = @Id

Cela renvoie le titre et la description du produit dans la langue demandée s'il existe une traduction pour cette langue. Si aucune traduction n'existe, le titre et la description de la langue par défaut seront retournés.

33
M4N

tilisation d'une table partagée commune pour tous les champs traduisibles de toutes les tables

Dans l'approche ci-dessus, la table de traduction est une extension de la table parent. Par conséquent, ProductTranslation a tous les champs traduisibles de Product. C'est une approche soignée et rapide et agréable aussi.

Mais il y a un inconvénient (je ne sais pas s'il peut être appelé désavantage). Si plusieurs autres tables nécessitent des champs traduisibles, autant de nouvelles tables sont nécessaires. D'après mon expérience, nous avons adopté une approche différente. Nous avons créé une table générique pour la traduction et une table de liens pour lier les traductions aux champs traduisibles de la table parent.

Je vais donc utiliser l'exemple précédent de Produit qui a deux champs titre et description traduisibles pour expliquer notre approche. Considérez également une autre table ProductCategory avec le nom et la description des champs qui nécessitent également des traductions.

Product
(
   ID: Integer
   SKU: String
   titleID: Integer // ID of LocalizableText record corresponding title
   descriptionID: Integer // ID of LocalizableText record corresponding description
)

ProductCategory
(
   ID: Integer
   nameID: Integer // ID of LocalizableText record corresponding name
   descriptionID: Integer // ID of LocalizableText record corresponding description
)

LocalizableText // This is nothing but a link table
{
    ID: Integer
}

Translations //This is where all translations are stored.
{
    ID: Integer
    localizableTextID: Integer
    language: String
    text: String
}

Pour charger ces données, j'utilise différentes requêtes (modifiées ci-dessus). Par exemple. pour charger un produit avec ID @Id dans la langue @Language, j'utiliserais la requête suivante

SELECT
    p.ID,
    p.SKU,
    -- get title, description from the requested translation,
    -- or fall back to the default if not found:
    Title.text Title,
    description.text Description
  FROM Products p
  -- join requested translation for title, if available:
  LEFT OUTER JOIN Translations title
    ON p.titleID = title.localizableTextID
       AND title.Language = @Language
  -- join requested translation for description, if available:
  LEFT OUTER JOIN Translations description
    ON p.descriptionID = description.localizableTextID
       AND description.Language = @Language
  WHERE p.ID = @Id

Cette requête est basée sur l'hypothèse que les champs individuels du produit n'ont pas de traduction par défaut

Une requête similaire peut être utilisée pour extraire des enregistrements de ProductCategory

4
Francis Mathew