[SSAS] Automatiser l’import export de traductions (Translations)

Il vous est déjà sûrement arrivé d’avoir à localiser un cube, c’est à dire de traduire sa structure de manière à ce qu’une dimension Produit s’appelle « Produkt » lorsque le cube est consulté depuis un poste germanique, « Product » depuis un anglophone… etc…

Le mécanisme utilisé est celui des traductions (ou Translations en VO) affectable depuis BIDS dans l’onglet du même nom pour les cubes et les dimensions.


Le fait que l’on doive l’éditer dans BIDS est intéressant mais problématique lorsque l’on utilise un prestataire pour traduire le cube: il faut en effet soit fournir un accès en remote au cube, soit forger soit même un fichier Excel par une multitude de copier/coller.
Etant assez peu fan de ce genre de procédé je me suis mis en tête de voir comment ce genre d’application pouvait s’automatiser.

Tout d’abord BIDS communique avec SSAS par AMO, il est donc intéressant de se plonger dans technet pour voir comment ces Translations sont représentées dans le modèle. Dans ce très petit paragraphe de l’article « Programming AMO Advanced Objects » on remarque que Translations est une collection qui est attachée à tout objet pouvant être traduit, et qui comprend des objets Translation.
Cet objet comprend – toujours en se référant à la doc – trois propriétés texte, Caption Description et DisplayFolder ainsi qu’un identifiant de langue entier (qu’on imagine être 1033 pour l’anglais, 1036 pour le français…).
1ère constatation: il n’y a pas de repository central des translations, il faudra parcourir l’arbre des composants d’une base pour extraire ces objets.

Ensuite qu’est ce qui peut être traduit sur un composant et comment est-ce associé à une propriété d’un objet Translation? Là pour le coup rien n’est défini dans le modèle, c’est là encore du côté de la doc et de cet article que la réponse est venue.
2ème constation: ce qui peut être traduit n’est pas déductible du modèle, il va falloir le modéliser

Enfin en creusant un peu j’ai remarqué quelques incongruités dans le modèle: les Attributes utilisent une spécialisation de Translation (AttributeTranslation), de même les collections de traductions ne s’appellent pas toujours Translations.
3ème constatation: le modèle n’est pas très facile et cohérent, ce qui rend difficile son traitement automatisé.

La modélisation

La première étape pour se débarasser de ces problèmes est donc arriver à modéliser l’arbre des propriétés traductibles des objets de la base, ce en gérant les potentielles incongruités. Un arbre étant assez facile à modéliser en xml, je m’y suis attelé, ce qui donne le fichier XML suivant:


Bon, maintenant il est possible de parcourir cet arbre et d’extraire les champs du cube devant être traduits en les documentant suffisamment pour faciliter leur import ultérieur. Le code d’export Bon ça n’est pas le code le plus propre de l’année – il s’agit d’un prototype🙂 – mais bon ça marche. L’idée est simplement de partir du noeud Database (le paramètre ModelComponent) et de son élément XML associé pour faire un parcours en profondeur et retrouver par réflexion les collections d’enfants et leurs membres.

 

public IEnumerable ExtractTranslations(ModelComponent c, XElement e)

{

Type t = c.GetType(); string ID = (e.Attribute(« id ») != null) ? e.Attribute(« id »).Value : « ID »; string path = e.Attribute(« name »).Value + « [ » + t.GetProperty(ID).GetValue(c, null).ToString() + « ] »; //Pour chaque propriété du noeud courant on retourne une ligne foreach (XElement p in e.Elements(« Property »)) { object propertyValue = t.GetProperty(p.Attribute(« name »).Value) .GetValue(c, null); if (propertyValue != null) { yield return new OutputLine() { Path = path, Value = t.GetProperty(p.Attribute(« name »).Value) .GetValue(c, null).ToString(), Property = p.Attribute(« translation »).Value, CustomCollection = (p.Attribute(« collection ») != null) ? p.Attribute(« collection »).Value : « Translations » }; } } //Puis on fait un appel récursif par enfant de chaque collection d’enfants foreach (XElement o in e.Elements(« Object »)) { var childrenCollection = t.GetProperty(o.Attribute(« name »).Value) .GetValue(c, null) as IEnumerable; foreach (ModelComponent child in childrenCollection) { foreach (OutputLine ol in ExtractTranslations(child, o)) { ol.Path = path + « . » + ol.Path; yield return ol; } } } }

Quel est le résultat en sortie? Un fichier fait un peu comme cela (j’ai laissé une seule ligne en exemple)ça:

Databases[Adventure Works DW 2008] .Cubes[Adventure Works] .MeasureGroups[Fact Internet Sales 1] ;Translations ;1033 ;Caption ;Internet Sales

Et cette chose là peut s’importer facilement: si si la preuve! Code d’import La on va descendre par réflexion le path , pour affecter à la collection de traductions (au nom fourni en paramètre) un membre qui comprendra pour le champ fourni la valeurrécupérée.

public void PutTranslation(Server root, InputLine l){ //D'abord on descend le path jusqu'a l'objet a traduire MajorObject closerMajorObject = root; ModelComponent current = root; while (l.Path.Length > 0) { string currentPath = l.Path.Split('.')[0]; object childrenCollection = current.GetType() .GetProperty(currentPath.Split('[')[0]) .GetValue(current, null); //On positionne le courant sur la collection fille, a l'index specifie PropertyInfo accessor = childrenCollection.GetType() .GetProperty("Item", new Type[] { typeof(string) }); current = accessor .GetValue(childrenCollection , new object[] { currentPath.Split('[')[1].TrimEnd(']') }) as ModelComponent; //Si on est sur le dernier noeud on vide la chaine pour sortir if (l.Path.Contains('.')) l.Path = l.Path.Substring(currentPath.Length + 1); else l.Path = string.Empty; } //On recupere la collection a modifier object translationCollection = current.GetType() .GetProperty(l.CustomCollection) .GetValue(current, null); //On recherche le type de traduction attendue //(les DimensionAttribute utilisent un type particulier) string type = (current.GetType().Name == "DimensionAttribute") ? "AttributeTranslation" : "Translation"; //On recherche la methode add sur la collection MethodInfo translationAdder = translationCollection.GetType() .GetMethods() .Where(m => m.Name == "Add" && m.GetParameters()[0].ParameterType.Name == type) .First(); //On recherche un objet de traduction pour ce langage si il existe object translation = translationCollection.GetType() .GetMethod("FindByLanguage") .Invoke(translationCollection, new object[] { l.Language }); //Sinon on cree un nouvel objet en invoquant le constructeur (int32) if (translation == null) { translation = translationAdder.GetParameters()[0].ParameterType .GetConstructor(new Type[] { typeof(Int32) }) .Invoke(new object[] { l.Language }); translationAdder.Invoke(translationCollection, new object[] { translation }); } //On affecte la valeur translation.GetType() .GetProperty(l.Property) .SetValue(translation, l.Value, null);}

Voilà avec ça ça suffit pour tout importer, et vous pouvez même le tester: j’ai intégré cela dans un petit lot SSIS sur mon SkyDrive. N’hésitez pas si vous avez des questions!

A bientôt!

Une réflexion sur “[SSAS] Automatiser l’import export de traductions (Translations)

  1. Excellent article, François, j’ai été à la recherche d’un système d’automatisation pour les étiquettes des cubes, je vais utiliser votre code, merci pour le partage.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s