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:
- Se enmarcará entre llaves “{“ y “}” para saber que ahí va un valor (una substitución)
- Se indicará la posición/índice del array que contiene los valores a colocar seguido de dos puntos “:” (obligatorio si no es tipo string)
- Se pondrá el tipo de dato que tiene que ser (en caso de no poner nada, por defecto se selecciona string “s”):
- d: Fecha
- c: Moneda
- n: Número
- k: Key de otro recurso
- s: Otro string
- 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.