Aplicación web multi-idioma con Angular y NGX-Translate
Introducción
Esta ocasión vamos a aprender a desarrollar e implementar una aplicación web multi-idioma, usando Angular 6 y NGX-Translate. Se dará por hecho que se tienen unas nociones básicas del funcionamiento de Angular, y se omitirán los pasos correspondientes a la instalación de los requerimientos.
Requerimientos
A continuación se detalla el software que deberemos tener instalado en nuestro equipo antes de iniciar el tutorial:
- NodeJS
- Angular CLI
- VSCode (O cualquier entorno de desarrollo similar)
Angular
Creación del proyecto
Lo primero que deberemos hacer será crear un nuevo proyecto, en el que posteriormente implementaremos NGX-Translate para así desarrollar una aplicación web multi-idioma.
Abrimos una ventana de terminal y nos movemos a la carpeta en la que deseemos crear el nuevo proyecto, una vez en ella ejecutaremos el siguiente comando:
ng new angular-multilanguage
Ésto nos creará un proyecto nuevo llamado angular-multilanguage dentro de la carpeta en la que estábamos situados. Sin cerrar la ventana de terminal, nos movemos a la carpeta del proyecto y lo arrancamos mediante los siguientes comandos:
cd angular-multilanguage
ng serve
Una vez haya terminado la compilación del proyecto, si abrimos nuestro navegador e introducimos en la barra de dirección localhost:4200, de inmediato nos cargará la página web.
Instalación de dependencias
Para simplificar la gestión de estilos procederemos a instalar Bootstrap, y NGBootstrap para el uso de componentes optimizados en Angular. También instalaremos NGXTranslate para poder gestionar los diferentes idiomas de la web. Si el proceso de ng serve aún sigue ejecutándose, debemos pararlo antes de proceder con la instalación. Una vez parado, ejecutaremos el siguiente comando:
npm install bootstrap
Seguidamente instalaremos NGBootstrap:
npm install --save @ng-bootstrap/ng-bootstrap
Y por último, NGXTranslate:
npm install --save @ngx-translate/core@10
npm install --save @ngx-translate/http-loader@3
Configuración de dependencias
Para poder usar los estilos de Bootstrap necesitaremos importar los estilos css en nuestro archivo de configuración angular.json de la siguiente manera:
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"styles.scss"
]
Ahora configuraremos el archivo app.module.ts con todo lo necesario para que la app funcione, y nos quedará de la siguiente manera:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AppComponent } from './app.component';
import { MainComponent } from './components/main/main.component';
@NgModule({
declarations: [
AppComponent,
MainComponent
],
imports: [
BrowserModule,
NgbModule,
TranslateModule.forRoot(),
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [ HttpClient ]
}
})
],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule { }
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
}
Desarrollo
Desde la ventana de terminal, con el ng serve detenido, procedemos a crear un nuevo componente mediante el siguiente comando:
ng generate component components/main
Con esto lograremos generar un nuevo componente llamado main dentro de la carpeta components (la cual si no tenemos se creará automáticamente).
Una vez hecho esto nos dirigiremos al archivo app.component.html y reemplazaremos el contenido por lo siguiente:
<!-- Componente Main -->
<app-main></app-main>
Ahora iremos al archivo main.component.html y sustituiremos el contenido por el siguiente:
<div class="container pt-5">
<div class="row">
<div class="col-4 offset-4 text-center">
<!-- Dropdown -->
<div ngbDropdown class="d-inline-block">
<input ngbDropdownToggle type="button" class="btn btn-outline-primary" id="dropdownIdioma" value="Idioma" />
<div ngbDropdownMenu aria-labelledby="dropdownIdioma">
<input type="button" class="dropdown-item" value="Español" (click)="changeLanguageToSpanish()" />
<input type="button" class="dropdown-item" value="Inglés" (click)="changeLanguageToEnglish()" />
</div>
</div>
<!-- /Dropdown -->
<!-- Text Bind -->
<input type="text" class="form-control mt-5" [(ngModel)]="text" placeholder="Introduce texto aquí" />
<p class="mt-2">El texto introducido es: {{ text }}</p>
<!-- /Text Bind -->
</div>
</div>
</div>
Y el archivo main.component.ts por el momento quedará de la siguiente manera:
import { Component, OnInit } from '@angular/core';
@Component({...})
export class MainComponent implements OnInit {
text: string;
constructor() { }
ngOnInit() { }
// Se cambia el idioma a Español
changeLanguageToSpanish(): void {
console.log('Idioma cambiado al Español');
}
// Se cambia el idioma a Inglés
changeLanguageToEnglish(): void {
console.log('Idioma cambiado al Inglés');
}
}
A la hora de compilar la web, nos debe dar un error que diga: Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’. Esto es porque no hemos importado el módulo de formularios en el app.module.ts:
import { FormsModule } from '@angular/forms';
@NgModule({
...
imports: [ FormsModule ],
...
})
export class AppModule { }
Una vez hecho esto, ya compilará correctamente.
Hasta ahora hemos conseguido crear una página web sencilla, que dispone de un desplegable para cambiar de idioma, una caja de texto para introducir lo que se nos ocurra, y un párrafo en el que se muestra un texto fijo seguido de lo que introducimos en la caja de texto. La única funcionalidad que tiene el desplegable es mostrar por consola un mensaje indicando a qué idioma se ha cambiado, por lo que a continuación se detallarán los pasos necesarios para implementar la funcionalidad de multi-idioma.
NGXTranslate
Para este ejemplo básico sólo haremos uso de dos idiomas (Español e Inglés), pero se pueden incluir todos los que sean necesarios. También es importante recalcar, que la dificultad de iniciar una aplicación web multi-idioma de cero es muchísimo menor a convertir una ya existente que se inició sin contemplar la posibilidad de esta necesidad en un futuro, pero no es imposible.
Lo primero que haremos será crear una carpeta llamada i18n dentro de src/app/assets. En ella se incluirán los ficheros JSON con los literales que requiera la aplicación traducidos en los diferentes idiomas. El nombre de estos ficheros debería ser {idioma}.json, así que en nuestro caso crearemos uno llamado es.json y otro llamado en.json.
{
"main": {
"language": "Idioma",
"spanish": "Español",
"english": "Inglés",
"input_placeholder": "Introduce texto aquí",
"introduced_text": "El texto introducido es: {{ text }}"
}
}
{
"main": {
"language": "Language",
"spanish": "Spanish",
"english": "English",
"input_placeholder": "Introduce text here",
"introduced_text": "Introduced text is: {{ text }}"
}
}
<div class="container pt-5">
<div class="row">
<div class="col-4 offset-4 text-center">
<!-- Dropdown -->
<div ngbDropdown class="d-inline-block">
<!-- Usando el pipe translate -->
<input ngbDropdownToggle type="button" class="btn btn-outline-primary" id="dropdownIdioma" value="{{ 'main.language' | translate }}" />
<div ngbDropdownMenu aria-labelledby="dropdownIdioma">
<!-- Usando el pipe translate -->
<input type="button" class="dropdown-item" value="{{ 'main.spanish' | translate }}" (click)="changeLanguageToSpanish()" />
<!-- Usando la directiva translate -->
<button type="button" [translate]="'main.english'" class="dropdown-item" (click)="changeLanguageToEnglish()"></button>
</div>
</div>
<!-- /Dropdown -->
<!-- Text Bind -->
<input type="text" class="form-control mt-5" [(ngModel)]="text" placeholder="{{ 'main.input_placeholder' | translate }}" />
<!-- Usando el pipe translate con parámetros -->
<p class="mt-2">{{ 'main.introduced_text' | translate: { text: (text || '') } }}</p>
<!-- /Text Bind -->
</div>
</div>
</div>
import { Component, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Component({...})
export class MainComponent implements OnInit {
text: string;
constructor(private translate: TranslateService) { }
ngOnInit() { }
// Se cambia el idioma a Español
changeLanguageToSpanish(): void {
this.translate.use('es');
console.log('Idioma cambiado al Español');
}
// Se cambia el idioma a Inglés
changeLanguageToEnglish(): void {
this.translate.use('en');
console.log('Idioma cambiado al Inglés');
}
}
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Component({...})
export class AppComponent {
constructor(translate: TranslateService) {
translate.setDefaultLang('es');
translate.use('es');
}
}
{
"app": {
"title": "Angular Multi-Idioma"
},
"main": {
"language": "Idioma",
"spanish": "Español",
"english": "Inglés",
"input_placeholder": "Introduce texto aquí",
"introduced_text": "El texto introducido es: {{ text }}"
}
}
{
"app": {
"title": "Angular Multilanguage"
},
"main": {
"language": "Language",
"spanish": "Spanish",
"english": "English",
"input_placeholder": "Introduce text here",
"introduced_text": "Introduced text is: {{ text }}"
}
}
import { Component } from '@angular/core';
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
import { Title } from '@angular/platform-browser';
@Component({...})
export class AppComponent {
constructor(private translate: TranslateService, private titleService: Title) {
this.translate.setDefaultLang('es');
this.translate.use('es');
this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
this.translate.get('app.title').subscribe((res: string) => {
this.titleService.setTitle(res);
});
});
}
}
Bonus
Como extra, ya que cada vez es más esencial, vamos a elaborar test unitarios básicos para testear nuestros componentes. El archivo app.component.spec.ts lo dejaremos preparado únicamente para que no ‘explote‘, pero sin testear funcionalidad:
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { MainComponent } from './components/main/main.component';
import { TranslateTestingModule } from 'ngx-translate-testing';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { HttpLoaderFactory } from './app.module';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
MainComponent
],
imports: [
TranslateTestingModule,
FormsModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [ HttpClient ]
}
})
]
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
});
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MainComponent } from './main.component';
import { TranslateTestingModule } from 'ngx-translate-testing';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { HttpLoaderFactory } from '../../app.module';
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { AppComponent } from '../../app.component';
const TRANSLATIONS_ES = require('../../../assets/i18n/es.json');
const TRANSLATIONS_EN = require('../../../assets/i18n/en.json');
describe('MainComponent', () => {
let component: MainComponent;
let fixture: ComponentFixture<MainComponent>;
let translate: TranslateService;
let http: HttpTestingController;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
MainComponent
],
imports: [
TranslateTestingModule,
FormsModule,
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [ HttpClient ]
}
})
],
providers: [ TranslateService ]
}).compileComponents();
translate = TestBed.get(TranslateService);
http = TestBed.get(HttpTestingController);
}));
beforeEach(() => {
fixture = TestBed.createComponent(MainComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load translations', () => {
TranslateTestingModule
.withTranslations('es', TRANSLATIONS_ES)
.withTranslations('en', TRANSLATIONS_EN)
.withDefaultLanguage('es');
});
it('should load and change between translations', async(() => {
spyOn(translate, 'getBrowserLang').and.returnValue('es');
const fixture = TestBed.createComponent(AppComponent);
const compiled = fixture.debugElement.nativeElement;
// El DOM debería estar vacío ya que no se han renderizado las traducciones aún
expect(compiled.querySelector('#dropdownIdioma').value).toEqual('');
http.expectOne('/assets/i18n/es.json').flush(TRANSLATIONS_ES);
http.expectNone('/assets/i18n/en.json');
// Se comprueba que no hay peticiones pendientes antes de seguir
http.verify();
fixture.detectChanges();
// El contenido debería estar traducido en español ya
expect(compiled.querySelector('#dropdownIdioma').value).toEqual(TRANSLATIONS_ES.main.language);
// Se cambia el idioma a Inglés
translate.use('en');
http.expectOne('/assets/i18n/en.json').flush(TRANSLATIONS_EN);
// Se comprueba que no hay peticiones pendientes antes de seguir de nuevo
http.verify();
// El DOM no debería haber renderizado aún las traducciones
expect(compiled.querySelector('#dropdownIdioma').value).toEqual(TRANSLATIONS_ES.main.language);
fixture.detectChanges();
// Las traducciones en inglés ya deberían haberse renderizado
expect(compiled.querySelector('#dropdownIdioma').value).toEqual(TRANSLATIONS_EN.main.language);
}));
});
ng test
Automáticamente, se abrirá una ventana de nuestro navegador predeterminado con el resultado. Si hemos seguido todos los pasos correctamente todos los test deberían pasar sin errores.
Si durante la realización del tutorial nos diera el siguiente error: ERROR in node_modules/@types/jasmine/index.d.ts(138,47): error TS1005: ‘;’ expected es muy probable que sea debido a las versiones que tenemos configuradas de Jasmine y Typescript, podemos ver una tabla de compatibilidades en el siguiente issue, y deberemos configurar las versiones que deseemos en nuestro archivo package.json:
"devDependencies": {
...
"@types/jasmine": "~2.8.8",
"typescript": "^2.7.2"
...
}
npm install
Resultado
A continuación podréis ver una captura del proyecto recién creado y otra de como ha quedado al final del tutorial.
Recursos
El código fuente del proyecto está subido a GitHub, desde el siguiente enlace podréis acceder a él.
Analyst Frontend Developer en Comunytek (BBVA CIB – NOVA).
Autodidacta, apasionado de las nuevas tecnologías y de los proyectos DIY.
Muy buen trabajo Iván Gabriel, excelente articulo, me puedes ayudar, necesito traducir los tooltips, estoy usando angular-material y te pongo un ejemplo:
save
Entonces traduzco matTooltip=»Save changes» así:
save
Cabe indicar que todo lo demas he podido traducir y funciona, solo los tooltips no he logrado hacer funcionar en lugar de la traduccion me sale la definicion es decir en lugar de salir «Salva cambi» (estoy traduciendo al italiano) me sale «contact.view.grabarcambios»
Espero me puedas ayudar