ngx-translate : écrire une pipe synchrone pour calculer la clef à utiliser
Dans un précédent article, nous avions vu comment utiliser la librairie ngx-translate pour internationaliser une application Angular. Dans ce présent article nous verrons comment créer une pipe qui calcule la clef de traduction à utiliser, problème qui paraît bête au premier abord mais est plus complexe qu'il n'y paraît.
Une pipe pour calculer la clef de traduction ?
Pourquoi faire cela ? Pour de bonnes raisons bien sûr : si par exemple vous devez afficher l'état d'un objet en se basant sur un enum, vous ne pouvez pas mettre en dur la clef de traduction car vous ne la connaissez pas à l'avance !
Implémentation utilisant la pipe Async
Il est possible de faire en première implémentation une Pipe qui retourne un Observable<string> :
@Pipe({ name: 'maPipeDeTraduction' }) export class MaPipeDeTraductionPipe implements PipeTransform { constructor(private translateService: TranslateService) { } transform(value: string, args?: string): Observable<string> { switch (value) { case 'pipe': return this.translateService.get('clef.pipe' ); case 'angular': return this.translateService.get('clef.angular'); default: return ''; } } }
L'utilisation au sein d'un template est alors faite en chaînant la pipe créée avec la pipe Async :
<div>{{ unStatut | maPipeDeTraduction | async }}</div>
Cela fonctionne bien mais nous oblige à chaque utilisation à mettre la pipe async en bout de chaîne. Je trouve cela vraiment dommage et fastidieux lorsque l'on est habitué à la pipe translate d'ngx-translate qui ne nécessite pas cette plomberie.
Et si on se passait d'async ?
D'ailleurs pourquoi on a besoin d'async ? Tout simplement parce qu'ngx-translate ne retourne jamais directement la valeur traduite mais une Observable<string> qui la donne une fois qu'il a eu le temps de charger les différentes traductions (depuis un fichier sur le serveur par exemple). Cela est donc un processus asynchrone et on doit passer par un Observable hors les pipes, elles, retournent une valeur de manière synchrone.
La solution consiste donc à se replonger dans l'article de William Petit sur les Pipes Angular et de marquer la Pipe comme impure. Cela nous permettra d'utiliser ensuite le ChangeDetectorRef pour indiquer qu'il faut recalculer la valeur retournée par la Pipe. On aura ainsi ce processus technique :
- Premier appel à la méthode transform de la pipe : on essaye de lire la valeur traduite à l'aide de la méthode instant de TranslateService. Si une valeur existe (= ngx-translate a déjà chargé les traductions) alors on la retourne directement.
- Si aucune valeur existe, on demande la traduction en utilisant le TranslateService : on s'abonne à l'Observable et une fois la traduction reçue ( = ngx-translate a chargé les traductions) alors on demande à Angular de reprocesser la pipe en utilisant le ChangeDetectorRef. Il repasse alors dans la méthode transform mais on obtient la traduction via la méthode instant.
- On pense bien à se désabonner des souscriptions à l'Observable.
Voici ce que cela donne en code :
@Pipe({ name: 'maPipeDeTraduction', pure: false }) export class MaPipeDeTraductionPipe implements PipeTransform, OnDestroy { constructor(private translateService: TranslateService, private changeDetectorRef : ChangeDetectorRef) { } serviceSubscription: Subscription; transform(value: string, args?: string): string { this.dispose(); let key =''; switch (value) { case 'pipe': key= 'clef.pipe'; case 'angular': key= 'clef.angular'; default: return ''; } const trad=this.translateService.instant(key); if(trad){ return trad; } this.serviceSubscription = this.translateService .get(key) .subscribe(k=>{ this.changeDetectorRef.markForCheck(); }); return ''; } ngOnDestroy(): void { this.dispose(); } private dispose(){ if (typeof this.serviceSubscription !== 'undefined') { this.serviceSubscription.unsubscribe(); this.serviceSubscription = undefined; } } }
On pourrait avoir comme remarque que marquer une pipe comme impure n'est pas bon pour les performances : hors c'est déjà le cas sur la pipe async et aussi sur la pipe translate d'ngx-translate. Il ne faut donc pas trop se faire de noeud au cerveau de ce côté !
Cela fonctionne donc très bien en l'état mais il nous reste à gérer les changements de langue d'ngx-translate. Pourquoi ne pas ré-écrire cela à la main mais dériver directement de la pipe translate ?
Soyons feignants : dérivons de la pipe translate !
j'ai toujours considéré qu'être feignant était une qualité en tant que développeur et nous allons donc le mettre à l'oeuvre ici en récrivant le code ci-dessus dérivant de la pipe Translate. Il faudra faire attention à deux choses : appeler le constructeur de base (il prends un ChangeDetectorRef en paramètre, on sait maintenant pourquoi) et bien configurer la pipe comme inpure.
@Pipe({ name: 'maPipeDeTraduction', pure: false }) export class MaPipeDeTraductionPipe extends TranslatePipe implements PipeTransform { constructor(private translateService: TranslateService, private changeDetectorRef : ChangeDetectorRef) { super(translateService, changeDetectorRef); } transform(value: string, args?: string): string { let key =''; switch (value) { case 'pipe': key= 'clef.pipe'; case 'angular': key= 'clef.angular'; default: return ''; } return super.transform(key, args); } }
Et voilà, on a libéré la bête !
Happy coding :)
Commentaires