24 de març del 2018

Ionic: aplicació per saber l'hora de sortida/posta del sol (VI)

Part VI: Traducció a diversos idiomes

Aquest article ens mostrarà com permetre diversos llenguatges en la nostre aplicació.

Fins ara hem fet una aplicació per saber l'hora de sortida i posta del sol:
Per introduir la possibilitat d'utilitzar diversos idiomes, utilitzarem el paquet ngx-translate. En primer lloc l'instal·lem:
 npm install @ngx-translate/core --save
 npm install @ngx-translate/http-loader --save

Seguint les instruccions d'ús, afegim a app.module.ts els mòduls i serveis necessaris:
 ...
 import { SplashScreen } from '@ionic-native/splash-screen';
 
 // ngx-translate
 import {TranslateModule, TranslateLoader} from '@ngx-translate/core';
 import {TranslateHttpLoader} from '@ngx-translate/http-loader';

 import { HttpClientModule, HttpClient } from '@angular/common/http';
 ...
 import { LangService } from '../providers/lang-service';

 // Translations
 export function createTranslateLoader(http: HttpClient) {
   return new TranslateHttpLoader(http, './assets/i18n/', '.json');
 }
 
 @NgModule({
   ...
   imports: [
     ...
     CalendarModule,
     TranslateModule.forRoot({
       loader: {
           provide: TranslateLoader,
           useFactory: (createTranslateLoader),
           deps: [HttpClient]
       }
     }),
     IonicModule.forRoot(MyApp)
   ],
   ...
   providers: [
     ...
     Network,
     LangService
   ]
 })
 ...

Observem com hem utilitzem un servei LangService per gestionar la detecció de l'idioma i el canvi de llenguatge. Per això creem un fitxer providers/lang-service.ts amb el següent contingut:
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
 import { TranslateService } from '@ngx-translate/core';

 @Injectable()
 export class LangService {
  onLang : BehaviorSubject = new BehaviorSubject("");

  lang_sets : Array = [];

  constructor(public translate: TranslateService) {
    console.log('### LangService');
  }

  init(lang_default: string, lang_sets: Array, lang_fallback?: string) {
    // Verify that some sets are present
    if(lang_sets.length==0) {
      console.error("[LangService] error: no lang_sets defined");
      return;
    } else {
      this.lang_sets = lang_sets;
    }

    // Verify default is present in lang_sets
    if(this.lang_sets.indexOf(lang_default) == -1) {
      console.log("[LangService] Warning: trying to use default lang '"+
                   lang_default+"' which is not present in lang_sets");
      console.log("[LangService] Warning: setting default to first" 
                   "language in set");
      lang_default = this.lang_sets[0];
    }

    // this language will be used as a fallback when a translation 
    // isn't found in the current language
    if(!lang_fallback) lang_fallback = lang_default;

    this.translate.setDefaultLang(lang_fallback);

    let langRegex = new RegExp(this.lang_sets.join("|"), 'gi');

    // get browser language by default
    let userLang = navigator.language.split('-')[0];
    userLang = langRegex.test(userLang) ? userLang : lang_default;

    this.setLanguage(userLang);
    // ----------------------------------------------------------------    
  }

  getCurrentLang() {return this.translate.currentLang};

  setLanguage(lang: string) {
    // Verify default is present in lang_sets
    if(this.lang_sets.indexOf(lang) == -1) {
      console.log("[LangService] Warning: trying to use lang '"+lang+
                  "' which is not present in lang_sets");
      return;
    }

    // the lang to use, if the lang isn't available, it will use 
    // the current loader to get them
    this.translate.use(lang);        
    console.log("[LangService] Using language: "+
                 this.translate.currentLang);

    // Inform to all subscriptors
    this.onLang.next(lang);
  }
 }

Observem com el servei:
  • [8] Declara un BehaviorSubject onLang que permetrà notificar un canvi d'idioma a la resta de l'aplicació.
  • [16-48] La funció init permet indicar quin idioma s'utilitza per defecte en l'aplicació (lang_default), el conjunt de llenguatges possibles (lang_sets), i quin és el llenguatge que s'utilitzarà si una traducció no existeix en un idioma (normalment, serà el llenguatge que utilitza com a primari el programador). La funció fa comprovacions d'errors i llavors estableix com a llenguatge per defecte a usar, el que tingui definit l'usuari en el seu dispositiu/navegador.
  • [50] La funció getCurrentLang simplement retorna el llenguatge actualment actiu en l'aplicació.
  • [52-68] La funció setLanguage estableix el llenguatge a utilitzar en l'aplicació, si existeix en el conjunt de disponibles. En cas contrari, agafa el que hi hagi per defecte en l'aplicació.

Aquest servei s'inicialitza a app.component.ts:
 ...
 import { LangService } from '../providers/lang-service';
 ...
  constructor( ..., langService : LangService ) 
  {
     platform.ready().then(() => {
       ...
       langService.init("en", ["en", "ca"]);
     });
   }
 ...

Podem veure com l'aplicació estarà en anglès i català. Per això, cal posar els fitxers d'idioma a assets/i18n/ca.json i assets/i18n/es.json. El nom es correspon al codi d'idioma i el contingut és un JSON senzills amb les traduccions. Per exemple, pel català:
 {
 "APP.title": "Sortida/posta de Sol",
 "APP.place": "Lloc",
 "APP.lat": "Latitud",
        ...
 }

Ara el que ens queda és utilitzar les traduccions en els missatges del programa, i permetre el canvi d'idioma. Modificarem la pàgina home.html:

 <ion-header>
  <ion-navbar>
    <ion-title>{{ "APP.title" | translate }}</ion-title>
  </ion-navbar>
 </ion-header>

 <ion-content padding>
  <ion-list>
    <ion-item-divider color="light">
      {{ "APP.language" | translate }}
    </ion-item-divider>
    <ion-item>
      <ion-label>{{ "APP.lang-current" | translate }}</ion-label>
      <ion-select [(ngModel)]="lang" (ionChange)="doChangeLang()" item-right>
        <ion-option value="ca">Català</ion-option>
        <ion-option value="en">English</ion-option>
      </ion-select>              
    </ion-item>
    ...

Observem que:
  •  [3, 10, 13] S'han canviat els missatges de text pel seu identificador en el fitxer d'idioma i demanant la seva traducció.
  • [14-17] S'ha afegit una opció de canvi d'idioma que crida la funció doChangeLang().

La funció doChangeLang() s'implementa a home.ts, que s'ha modificat de la següent manera:
 ...
 import {TranslateService} from '@ngx-translate/core';
 import { LangService } from '../../providers/lang-service';
 ...
   constructor(  ...,
                 public translate: TranslateService,
                 public langService : LangService,  
               )
   ...
   ionViewDidLoad() {
     ...
     this.dateMsg = "APP.today";

     this.langService.onLang.subscribe(lang=> {
       this.lang = this.translate.currentLang;
       console.log("[HomePage] Current lang: "+this.lang);
       moment.locale(this.lang);  
     });
   }
 
   selectDate() {
     const options: CalendarModalOptions = {
       title: this.translate.instant("APP.date-choose"),
       canBackwardsSelected: true,
       closeLabel: this.translate.instant('APP.Cancel'),
       doneLabel: this.translate.instant('APP.Done'),
     ...
   }
   ...
   doChangeLang() {
     this.langService.setLanguage(this.lang);
   }
 }

On podem veure com:
  • Assignem codis de traducció (p.e. a dateMsg) per mostrar-los traduïts en un html.
  • Utilitzem this.translate.instant() per traduir directament.
  • Es crida al servei langService per canviar l'idioma globalment.
  • Ens subscribim a langService.onLang per canviar el format del dia/hora segons el llenguatge actual.
També cal modificar location-select.html per traduir els missatges, tal com ho hem fet amb home.html.

Però la part més delicada és modificar l'idioma de GoogleMaps. El llenguatge es fixa en crear el mapa, i fins on jo sé, l'API no permet canviar-lo després. Per això, ha calgut modificar el codi de google-maps-service.ts de la següent manera:

 ...
 import { LangService } from '../providers/lang-service';
 ...
  constructor(  ...,
                 public langService : LangService,  
               ) 
  {
     this.langService.onLang.subscribe(lang=> {
       if(this.googleMapScriptElement) {
         console.log("[GoogleMapsService] Current lang: "+lang);
 
         this.updateScriptSrc();
       }
     });
  }
  ... 
  updateScriptSrc() {
    let lang = this.langService.getCurrentLang();

    console.log("[GoogleMapsService].updateScriptSrc with lang: "+lang);
    
    if(this.googleMapScriptElement) {
      let head = document.getElementsByTagName('head')[0];
      let scripts = Array.prototype.slice.call(head.getElementsByTagName('script'));
      let styles = Array.prototype.slice.call(head.getElementsByTagName('style'));

      scripts.forEach(s=> {
        if(s.src.indexOf("://maps.google") != -1) head.removeChild(s);
        else if(!s.src || s.src.indexOf("cordova.js") != -1 || 
                          s.src.indexOf("ion-dev.js") != -1) {}
        else console.log("Skip removing <script src='"+s.src+"'");
      });
      styles.forEach(s=> {
        if(s.textContent.indexOf(".gm-style") != -1) head.removeChild(s);
        else console.log("Skip removing <style>", s);
      });
      document.body.removeChild(this.googleMapScriptElement);
    }

    let script = document.createElement("script");
    script.id = "googleMaps";

    if(this.apiKey){
      script.src = 'http://maps.google.com/maps/api/js?key=' + this.apiKey + 
                   '&callback=mapInit&libraries=places&language='+lang;
    } else {
      script.src = 'http://maps.google.com/maps/api/js?callback=mapInit&'
                            'amp;libraries=places&language='+lang;       
    }    

    this.googleMapScriptElement = document.body.appendChild(script);
  }
  ...

Bàsicament, importem el LangService per subscriure'ns al canvi d'idioma. Quan es produeix, s'actualitza GoogleMaps, eliminant els scripts i estils que afegeix i recreant el mapa amb el nou llenguatge.

Amb tot plegat, podem veure el resultat en la imatge:



Podem observar en la consola els missatges del LangService quan es detecta l'idioma i quan es canvia. També el nou control per canvi d'idioma en la pantalla.

Si ens fixem en la consola, podem veure que la recreació del mapa de GoogleMaps no és del tot perfecte. Tot i eliminar els scripts i estils per evitar duplicats, probablement encara queda alguna cosa que és detectada en carregar el nou mapa. A la llarga, això aniria consumint memòria, però tampoc no canviarem d'idioma constantment, no?

Com sempre, teniu tot el codi a la vostre disposició en el Github: v0.5 (amb traducció a diversos idiomes).

Ara ja tenim una aplicació funcional i en diversos idiomes, però podem fer-hi encara alguns canvis estètics, amb icones personalitzades i tipus de lletra diferents; i finalment, no oblidem completar el projecte donant les dades de contacte i afegint-hi una pàgina per donar crèdit als projectes i persones que l'han fet possible.

Seguiu l'evolució de l'aplicació:

Cap comentari:

Publica un comentari a l'entrada