Multi-idioma de los recursos (Parte 2)

Tal como se explicó en la primera parte de la gestión de literales, vamos a comentar cómo aplicar o dar formato a un dato cuando el lugar, tipo y patrón viene dado en un valor del recurso visto previamente aplicando a cada una una estrategia concreta.

Te preguntarás ¿y por qué? bueno, pueden darse circunstancias o casos en los que por necesidades no pueda aplicarse directamente un pipe o donde el lugar del dato a formatear no se sabe o se desconoce el tipo de dato que es. Por ejemplo, se pueden almacenar fórmulas o textos que donde dentro hay partes donde se tienen que sustituir por un importe.

El ejemplo que voy a presentar sería más o menos de la siguiente forma:
“El día X pagué Y en concepto de Z”.

En el caso más fácil, si el texto fuera parte del html del componente, podríamos verlo de la siguiente forma:

<p>El día {{ dia | date }} pagué {{ importe | currency }}  en concepto de {{ concepto }}</p>

No obstante, el texto está en una base de datos con lo que deberemos saber en donde se encuentra cada dato, su tipo y cómo queremos formatearlo quedando de la siguiente forma en nuestro json de recursos:

{ "key": "EXAMPLE", "value": "El día {0:d[f:dd/MM/yyyy]} pagué {1:c} en concepto de {2}" }

Descifrando el texto

Tendrá los siguientes requisitos:

  1. Se enmarcará entre llaves “{“ y “}” para saber que ahí va un valor (una substitución)
  2. Se indicará la posición/índice del array que contiene los valores a colocar seguido de dos puntos “:” (obligatorio si no es tipo string)
  3. Se pondrá el tipo de dato que tiene que ser (en caso de no poner nada, por defecto se selecciona string “s”):
    1. d: Fecha
    2. c: Moneda
    3. n: Número
    4. k: Key de otro recurso
    5. s: Otro string
  4. Formato (opcional) entre corchetes “[“,”]”. Según el tipo de dato puede necesitar un formato específico.

El array de que contiene los valores según el ejemplo visto puede ser el siguiente:

[new Date(2019, 0, 1), 45.5, “inscripción”]

De esta manera, utilizando el pipe visto en la primera parte, en el html quedaría de la siguiente forma:

{{ 'EXAMPLE' | translate: values }}

Y values, definido en un getter del componente:

public get values(): Array<any> {
   return [new Date(2019, 0, 1), 45.5, 'inscripción'];
 }

En una primera aproximación será un array de tipo “any”, porque es una lista de objetos varios. El tipo a dar lo sabe el valor de la key a sustituir.

La construcción de la sustitución se realizará en el pipe utilizando el patrón strategy. Cada estrategia corresponderá a un tipo de sustitución según el tipo (fecha, número, moneda, …).

Este sería el código del pipe:

@Pipe({
 name: 'translate'
})
export class TranslatePipe implements PipeTransform {
 private _strategies: Array<strategies.ITranslateStrategy> = [];
 
 public constructor(
   private _resourcesService: ResourcesService,
   private _stringStrategy: strategies.StringStrategy,
   private _resourceKeyStrategy: strategies.ResourceKeyStrategy,
   private _dateStrategy: strategies.DateStrategy,
   private _currencyStrategy: strategies.CurrencyStrategy,
   private _numberStrategy: strategies.NumberStrategy
 ) {
   this._strategies = [
     this._stringStrategy,
     this._resourceKeyStrategy,
     this._dateStrategy,
     this._currencyStrategy,
     this._numberStrategy
   ];
 }
 
 public transform(value: string, ...args: any[]): string {
   const resource: ITextResource = this._resourcesService.get(value);
   return resource.notFound ? value : this.format(resource.value, args[0]);
 }
 
 private format(value: string, args: any[]) {
   if (value) {
     (args || []).forEach((arg: any, index: number) => {
       const strategyFound: strategies.ITranslateStrategy = this._strategies.find(
         (strategy: strategies.ITranslateStrategy) => {
           return strategy.canApply(index, value, arg);
         }
       );
 
       if (strategyFound) {
         value = strategyFound.apply(index, value, arg);
       }
     });
   }
 
   return value;
 }
}

Se cargan las estrategias, y en el transform, se aplica la estrategia (apply) si es que se puede aplicar (canApply). 

Cada estrategia deberá implementar una interfaz e implementar su lógica.

export interface ITranslateStrategy {
 canApply(index: number, text: string, value: any): boolean;
 apply(index: number, text: string, value: any): string;
}

Por ejemplo, la estrategia de la sustitución de un campo numérico sería:

import { Injectable } from '@angular/core';
import { ITranslateStrategy } from './iTranslateStrategy';
import { DecimalPipe } from '@angular/common';
 
@Injectable()
export class NumberStrategy implements ITranslateStrategy {
 public constructor(private _decimalPipe: DecimalPipe) {}
 
 private getRegExp = (index: number) => new RegExp(`{${index}:n}`, 'gm');
 private isNumber = (value: any) => typeof value === 'number';
 
 public canApply(index: number, text: string, value: any): boolean {
   const regNumber = this.getRegExp(index);
   return regNumber.test(text);
 }
 
 public apply(index: number, text: string, value: any): string {
   if (!this.isNumber(value)) {
     console.log(`TranslatePipe: value at position ${index} is not a number`);
     return text;
   }
   const regNumber = this.getRegExp(index);
   return text.replace(regNumber, this._decimalPipe.transform(value));
 }
}

Puedes descargar el código del artículo completo (dos partes) en GitHub. Verás la implementación de las siguientes estrategias: Currency, Date, Number, ResourceKey y String.

Se pueden ir añadiendo tantas estrategias como desees. Por ejemplo, se puede crear una estrategia donde se tenga que rellenar un texto con tantos ‘0’ hasta una longitud dada o bien realizar el cálculo de alguna fórmula, etc.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *