Multi-idioma de los recursos (Parte 1)

Cuando desarrollamos una aplicación para ser utilizada por personas de varias culturas pensamos siempre en que el software soporte el multi-idioma de los recursos con las siguientes premisas:

  1. Mostrar textos en el idioma del usuario. Aquí solamente cubriremos los recursos que se necesitan para los literales fijos como: etiquetas, bloques de texto, u otros recursos que no pertenecen a información de negocio almacenada en una base de datos.
  2. Formatear los datos de tipo fecha, moneda, números, … según la cultura.
  3. Los datos en la base de datos están en varios lenguajes.

Y obviamente, las traducciones se hacen en el lado del cliente (frontend) mientras que los recursos están en el servidor. Todo ello sin usar i18n aunque al menos importaremos los locales necesarios (por ejemplo es-ES, en-US).

Por la magnitud de este tema, este artículo se dividirá en dos partes abarcando cada artículo cada premisa anterior. La tercera premisa no la abarcaremos dado que escapa al ámbito del frontend.

¡¡¡Empecemos!!!

Estructura de datos de los recursos

Lo primero a realizar será crear tantos archivos json como culturas diferentes tenga la aplicación, en nuestro caso:

  1. literals.es-ES.json
  2. literals.en-US.json

La estructura del archivo será: clave/valor donde la clave será la misma por cada uno de las culturas.

literals.es-ES.json:

{
   "resources": [
     { "key": "TITLE", "value": "¡Bienvenido a Literales!" },
     { "key": "ANGULARFRONTENDERS", "value": "Angular Frontenders" },
     { "key": "HELLO.WORLD",  "value": "¡Hola mundo!" }
   ]
 } 

   literals.en-US.json:

{
   "resources": [
     { "key": "TITLE", "value": "Welcome to Literals!" },
     { "key": "ANGULARFRONTENDERS", "value": "Angular Frontenders" },
     { "key": "HELLO.WORLD",  "value": "Hello world!" }
   ]
 }

Naturalmente, se pueden importar estos json a una base de datos y que se queden ahí almacenados para su posterior gestión.

A efectos de emular con un servidor «json-server», estará todo en un solo archivo «db.json». Accediendo de la siguiente forma para recuperar los recursos:

http://localhost:3000/resources/es-ES
http://localhost:3000/resources/en-US

  Y ejecutando desde donde está ese «db.json» el siguiente comando:

    json-server --watch db.json

Lectura de los recursos

Para obtener los recursos llamando a el servidor json, crearemos un servicio para tal efecto:

import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LOCALE_ID } from '@angular/core';

@Injectable()
export class DataService {
  private readonly _baseUrl: string = 'http://localhost:3000'; 
  
  public constructor(
    @Inject(LOCALE_ID) private _locale: string,
    private _httpClient: HttpClient) {
  } 

  public getResources() {
    return this._httpClient.get(${this._baseUrl}/${this._locale});
  }
} 

Como se puede ver, la url de acceso será por donde escucha el servidor json (http://localhost:3000), concatenando el locale actual de la aplicación (es-ES, en-US, …).

Estos datos obtenidos (lista de pareja key/value), se almacenarán en un servicio para su posterior uso dentro de la aplicación:

export interface ITextResource {
  key: string;
  value: string;
  notFound?: boolean;
 }

@Injectable()
export class ResourcesService {
  private _dictionary: Array = []; 

  public constructor(private _dataService: DataService) {
  } 
 
  public load() {
    this._dictionary = [];
    return new Promise((resolve, reject) => {
      this._dataService.getResources().subscribe(
        (resources: Array) => {
          this._dictionary = resources;
          resolve(true);
        },
        err => {
          console.log('not server found.');
          resolve(true);
        }
      );
    });
  } 
 
  public get(key: string): ITextResource {
    const resourceFound = this._dictionary.find((resource: ITextResource) => resource.key === key); 
    return (
      resourceFound || {
        key: key,
        notFound: true,
        value: [[resource: "${key}" not found!]]
      }
    );
  }

} 

Con el método load, podemos recuperar todos los recursos y almacenarlos en una variable interna. Este método load se debería ejecutar como mínimo al ejecutarse la aplicación inicialmente.

Esta variable interna está definida en este caso por un Array, no obstante, otra alternativa podría ser usando un diccionario (Map).

El método get, devolvería un objeto con los datos necesarios en base a la key del idioma.

Hemos comentado que la carga de los recursos debería ser al iniciarse la aplicación, para ese caso, en el módulo de la app, ejecutamos el load del servicio de la siguiente manera:

import { ResourcesService } from './resources/resources.service';
import localeES from '@angular/common/locales/es';

registerLocaleData(localeES, 'es-ES');

export function resourcesProviderFactory(provider: ResourcesService) {
  return () => provider.load();
}
 
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, ResourcesModule, TranslateModule],
  providers: [
    { provide: LOCALE_ID, useValue: 'es-ES' },
    {
      provide: APP_INITIALIZER,
      useFactory: resourcesProviderFactory,
      deps: [ResourcesService],
      multi: true
    }
  ],
  bootstrap: [AppComponent]
 })
 export class AppModule {} 

Definimos primero en qué locale deseamos que trabaje mediante el comando registerLocaleData.
Posteriormente definimos una función resourcesProviderFactory que ejecuta el load del servicio de recursos.

Toda esta configuración la incorporamos en el apartado providers del módulo.

Uso de los recursos

Una vez tenemos ya los recursos cargados en un servicio de la aplicación, se puede inyectar dicho servicio para su uso (para otro servicio, componente, etc.) usando el método get para ir obteniendo los valores.

En nuestro caso hemos creado un pipe:

import { Pipe, PipeTransform } from '@angular/core';
 
import {
  ResourcesService,
  ITextResource
} from '../resources/resources.service';
 
@Pipe({
  name: 'translate'
})
export class TranslatePipe implements PipeTransform {
 
  public constructor(private _resourcesService: ResourcesService) { 
  }
 
  public transform(value: string, ...args: any[]): string {
    const resource: ITextResource = this._resourcesService.get(value);
    return resource.notFound ? value : resource.value;
  }

}

Usándolo de la siguiente forma:

<h2> {{ 'HELLO.WORLD' | translate }}</h2>

Puedes descargar el código del artículo completo (dos partes) en GitHub

Deja un comentario

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