10 d’abril de 2019

slang-ed: editor senzill de fitxers de traduccions [V0.2.0]

Fa un temps vaig parlar de com podem traduir la nostra aplicació Ionic/Angular a diversos idiomes. En concret, vàrem veure que gràcies a utilitzar la llibreria ngx-translate, en teníem prou d'editar uns fitxers JSON amb les traduccions en cada idioma, per a poder utilitzar-les després en el nostre codi.

Si us interessa el seu ús real en una aplicació Ionic/Angular/Electron començant des de zero, podeu seguir la sèrie completa aquí (o descarregar el codi des del seu repositori github).

L'edició "a mà" d'aquests fitxers de traduccions no és difícil, però és pesada i subjecta a errades i oblits. Si heu de fer-ho professionalment, us aconsello la proposta de pagament BabelEdit. Alternativament, podeu utilitzar lliurement l'editor que he anat fent a estones lliures: slang-ed.


slang-ed: Editor senzill de fitxers d'idioma

Es tracta d'una aplicació Electron/Ionic 4 per a editar fitxers i18n de traduccions d'idioma. Està pensat per facilitar la traducció del vostre contingut a diferents idiomes quan s'utilitza ngx-translate com a llibreria d'internacionalització (i18n) per Angular. Permet l'edició dels fitxers JSON de les traduccions de forma senzilla.

Crear un Nou Projecte:
  1. "Inicia des de..." per obrir un directori Angular/Ionic ja preparat:
    • busca una estructura de directoris típica "src/assets/i18n"
    • els fitxers d'idioma han de ser JSON amb un nom que segueixi l'abreviació amb 2 caràcters (per exemple, 'ca.json').
  2. Guardar com Nou Projecte” per a crear un fitxer de projecte slang-ed.
Treballant en un Projecte:
  1. "Obrir Projecte" per carregar les traduccions actuals.
  2. Tot seguit edita els identificadors i les traduccions tant com calgui.
  3. "Guardar Traduccions" per actualitzar els fitxers d'idioma i18n.
  4. "Guardar Projecte" per actualitzar el fitxer de projecte amb les noves traduccions.
Editar Traduccions:
  • "Afegir Arrel" per afegir un node al nivell de l'arrel.
  • "Afegir Node" per afegir un node intermig al nivell actual.
  • "Afegir paraula" per afegir una paraula a traduir en el nivell actual.
  • "Esborrar" per esborrar el node/paraula actual.
  • "Desfés" / "Refés" per recuperar en cas d'errada.

Pendent (To Do):
  • Gestionar els fitxers del projecte: crear, obrir, desar (això permetrà emmagatzemar informació "extra" a les definicions d'idioma). [V0.2.0]
  • Suport per a propietats en les definicions d'idioma: comentari (per a traductors), aprovat (traducció verificada), foundInSrc (clau present en el codi font), conserva (no eliminar la clau, encara que no es trobi en el codi font). 
  • Afegir / eliminar idiomes en el projecte actual. 
  • Desar els fitxers i18n en el format d'objecte (en lloc del format de cadena de clau completa que s'utilitza ara). 
  • Edició de múltiples paraules alhora (per exemple, tots els descendents d’un determinat node). 
  • Indicar les traduccions, aprovacions o comentaris pendents... 
  • Altres: els vostres suggeriments seran sempre benvinguts ;-)
Descàrrega:

Actualment hi ha disponible la V0.2.0. Sense gaire floritures, permet fer la feina. Podeu descarregar-la directament del repositori de github:
Si us animeu a provar slang-ed, ja em comentareu què us sembla !

7 d’abril de 2019

Linux: distribucions lleugeres per a programadors (2019)

 Ja fa temps que vaig posar Ubuntu a un HP dm1-3120es. Però han passat els anys, les distribucions són cada vegada més pesades i, ja per començar, l'ordinador tampoc no era cap meravella. En resum, que és lent.

Per poder continuar treballant amb ell, l'he actualitzat amb un disc dur SSD de 64GB que he "reciclat" d'un antic sistema. I com que bàsicament el vull per poder programar a estones mortes, quan estic lluny del meu equip principal, m'he decidit a mirar quines distribucions linux lleugeres hi ha disponibles en el 2019. Això sí, han de poder suportar NVM (amb Node.js), Visual Studio Code i Gitkraken. I tot plegat amb un entorn gràfic correcte i sense massa dificultats d'instal·lació.

He provat tres distribucions, i tot seguit vull compartir els resultats obtinguts.

Bodhi Linux
La primera distribució que he provat és Bodhi Linux (V5.0.0). Està basada en la darrera versió d'Ubuntu LTS (18.04), però amb el gestor de finestres lleuger Enlightenment.
Jo he instal·lat la versió AppPack, pel que acaba ocupant uns 9GB de disc (la versió més bàsica hauria d'ocupar uns 5GB), mentre que la memòria RAM no arriba als 300MB. Cap problema per instal·lar la xarxa sense fils ni la resta de paquets de programació (el teclat ja era correcte després de la instal·lació). Gràficament, es veu molt polit i és agradable treballar amb ell.

antiX Linux
La segona distribució és antiX Linux (V17.4.1). Està basada en Debian stable i treballa per defecte amb el gestor de finestres icewm.

La versió completa ocupa uns 4GB de disc, mentre que la memòria RAM inicialment es situa entorn als 200MB. El teclat no venia configurat i, en general, ha estat la distribució on més dificultats he trobat per instal·lar tot el necessari. Personalment, l'apartat gràfic no m'acabava de convèncer (tot i que si es canvia pel rox-Flubox millora força), però per la resta, funciona perfectament.

Sparky Linux
La tercera distribució és Sparky Linux (V4.9.2 LTS). També està basada en Debian stable i treballa per defecte amb el gestor de finestres LXDE.

La versió Home amb LXDE ocupa uns 7GB de disc, mentre que no arriba als 300MB de memòria RAM inicial. El teclat no venia configurat, però és senzill de configurar, a l'igual que la xarxa sense fils. En l'apartat gràfic, estaria a mig camí entre les dues anteriors.

Prestacions
Fetes les presentacions, anem a veure les prestacions de cadascuna d'elles (s'ha fet un mínim de tres execucions en cada distribució).
Bodhi antiX Sparky Ubuntu*
Temps de càrrega (de grub a login) 20s 24s 18s 33s
Temps d'arribada a l'escriptori (des del login) 5s 5s 6s 20s
Temps de càrrega d'una aplicació Angular/Electron 138s 110s 108s 140s
Memòria ocupada (amb aplicació Angular/Electron carregada) 1.11GB 1.05GB 1.16GB 1.84GB
*: Ubuntu 18.04 LTS, indicada per referència i comparació.

Tal com es pot comprovar pels resultats de la taula, qualsevol de les tres opcions és vàlida, especialment pel que fa als temps de càrrega i el consum de memòria. Ara bé, pensant en dedicar l'ordinador a programar, el temps de càrrega de l'aplicació seria el factor més important, cosa que deixaria antiX i Sparky com les dues millors alternatives. Considerant també els temps de càrrega i la meva preferència gràfica, llavors la guanyadora és clarament: Sparky Linux.

Finalment, voldria indicar que per a un ús més general, la distribució Bodhi linux pot ser una gran alternativa a un Ubuntu tradicional, especialment en ordinadors antics.

Hi esteu d'acord? Coneixeu alguna alternativa millor? Podeu comentar el que creiu convenient en els comentaris !

30 de març de 2019

Ionic/Angular: Tests unitaris amb ngx-translate

Si treballeu amb Ionic o Angular possiblement utilitzeu la llibreria d'internacionalització ngx-translate.com. I si no ho feu encara, us ho recomano, ja que facilitat la traducció a diversos idiomes i el canvi de llenguatge sense haver de reiniciar l'aplicació. Podeu veure com fer-ho en aquest article, on detallo com afegir un servei de traducció a una aplicació.

El cas és que Ionic v4 facilita la realització de tests unitaris, però no es fàcil comprovar un mòdul que utilitzi els serveis de traducció: pipes, carregadors, dependències... Com gestionar-ho, tot plegat?

La informació no és molt clara i hi ha diverses alternatives, algunes de força complicades. Per això vull compartir aquí la que m'ha semblat més senzilla.

En primer lloc, instal·lem un parell de llibreries que ens serviran de suport:
 npm install ngx-translate-testing --save-dev
 npm install ngx-translate-messageformat-compiler messageformat --save-dev
Llavors ja podem utilitzar la llibreria en els nostres test unitaris:
 ...
 import { TranslateTestingModule } from 'ngx-translate-testing';
 import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';

 describe('ElMeuComponent', () => {
    const ENGLISH_LANGUAGE = 'en';
    const ENGLISH_TRANSLATIONS = require('../../assets/i18n/en.json');
    ...
    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ElMeuComponent],
            schemas: [CUSTOM_ELEMENTS_SCHEMA],
            imports: [
                TranslateTestingModule
                    .withTranslations(ENGLISH_LANGUAGE, ENGLISH_TRANSLATIONS)
                    .withCompiler(new TranslateMessageFormatCompiler())
            ]
        }).compileComponents();
    }));
    ...
 });


I el resultat:
Com es pot veure, és força senzill. Ara ja no hi ha cap excusa per no comprovar els mòduls que utilitzin la pipe translate !!!

10 de febrer de 2019

GitKraken: Inotify Limit Error [Solucionat]

Si desenvolupeu en Ubuntu i treballeu amb git és possible que ja utilitzeu el GitKraken. I si encara no ho feu, proveu-lo ! Segur que us agradarà: us deixo una imatge a sota per fer dentetes...


En tot cas, si treballeu en Angular o en projectes amb molts fitxers, us pot sortir el següent error en obrir el projecte:


Per evitar-lo només cal augmentar el nombre màxim de fitxers a supervisar (per exemple, seguint les indicacions de: https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers).
$ cat /proc/sys/fs/inotify/max_user_watches
$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
$ sudo sysctl -p

I problema resolt !

2 d’abril de 2018

Electron: aplicacions d'escriptori amb tecnologia web

Darrerament, he publicat una sèrie d'articles sobre com fer una aplicació híbrida, per Android, iOS i web utilitzant typescript (javascript), html i CSS3.

En aquest article, veurem com amb aquesta mateixa base de codi podem construir també una aplicació d'escriptori gràcies a Electron. Bàsicament, el que fa és empaquetar l'aplicació Ionic que ja teníem per poder construir una aplicació d'escriptori, que s'executa sobre una pàgina Chrome personalitzada. La part bona és que Electron s'ocupa de tot, així que els canvis a fer són mínims.

Ho mostrarem amb l'aplicació per saber l'hora de sortida i posta del sol que vàrem fer pas a pas:

El codi de l'aplicació Ionic final està disponible en el Github: v1.0.

Per desgràcia, no hi ha encara una plantilla per a un projecte Electron amb Ionic. Hi ha informació diversa, però és difícil extreure'n el gra de la palla. A mi m'han servit molt els articles del bloc de Rob Ferguson. Tot seguit us resumeixo el procés d'adaptar una aplicació Ionic que ja tenim funcionant per afegir-hi Electron i poder generar una aplicació d'escriptori.

En primer lloc, instal·lem electron:
 npm install -g electron

Tot seguit creem un directori electron a l'arrel del projecte hi hi posem la pàgina principal d'electron, main.js, modificada per carregar l'aplicació Ionic:
const electron = require('electron');
// Module to control application life.
const app = electron.app;
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;

const path = require('path');
const url = require('url');

// Module for file manipulations
const fs = require('fs');

// config json
let config_file = path.dirname(process.execPath)+'/config_keys.json';

if(!fs.existsSync(config_file)) {
   config_file = app.getAppPath()+'/config_keys.json';
}

const config = JSON.parse(fs.readFileSync(config_file, 'utf-8'));

process.env.GOOGLE_API_KEY = config.GOOGLE_API_KEY;  //  = "YOUR_API_KEY";

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

function createWindow() {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 1024, height: 768});

  // and load the index.html of the app.
  const startUrl = process.env.ELECTRON_START_URL || url.format({
    pathname: path.join(__dirname, '/../www/index.html'),
    protocol: 'file:',
    slashes: true
  });

  mainWindow.loadURL(startUrl);
  // mainWindow.loadURL("http://localhost:8100");

  // Open the DevTools.
  // mainWindow.webContents.openDevTools();

  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
});

app.on('activate', function () {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

El contingut és bàsicament el d'un projecte inicial d'Electron. Els canvis que s'han fet són:
  • [10-22] Es llegeix un fitxer de configuració config_keys.json que inclou el codi de la clau de Google per a les API de les aplicacions de Maps i Geolocation. Sense aquesta clau, l'aplicació d'escriptori no té permisos per utilitzar geolocalització.
  • [32-37] S'indica a Electron d'on ha de carregar la pàgina inicial. Justament és la de l'aplicació Ionic.
El fitxer de claus config_keys.json es guarda a l'arrel del projecte, i el seu contingut serà com segueix (lògicament, canviant la clau per una de bona):
 {
 "GOOGLE_API_KEY": "You_API_Key_here" 
 }

Al fitxer src/index.html d'Ionic eliminem la referència a cordova.js, que no és necessària en una aplicació d'escriptori:
  <!-- cordova.js required for cordova apps (remove if not needed)
  <script src="cordova.js"></script>
 -->

Finalment, editem el package.json per afegir-hi les noves dependències i instruccions de com treballar amb electron:
 {
  ...
  "scripts": {
    ...
    "start": "ionic serve --no-open",
    "electron": "electron .",
    "dist": "electron-builder",
    ...
  },
  ...
  "devDependencies": {
    "@angular/cli": "^1.5.4",
    "@angular/router": "^5.0.3",
    "@ionic/app-scripts": "3.1.8",
    "@types/node": "^8.0.53",
    "concurrently": "^3.5.0",
    "electron": "^1.7.9",
    "electron-builder": "^19.45.1",
    "ionic-mocks": "^1.0.4",
    "typescript": "2.4.2",
    "wait-on": "^2.0.2"
  },
  "config": {
    "ionic_source_map_type": "source-map"
  },
  "main": "electron/main.js",
  "build": {
    "appId": "com.blogspot.anomenaidesa.sunsetsunrise",
    "files": [
      "electron/main.js",
      "www/**/*"
    ],
    "extraFiles": [
      "config_keys.json"
    ],
    "mac": {
      "category": "productivity"
    },
    "linux": {
      "category": "Utility"
    }
  },
  ...
 }

Ara només ens cal instal·lar les dependències amb:
 npm install

I provar l'aplicació executant primer en un terminal:
 npm start

I, quan acabi de preparar l'aplicació, en un altre terminal:
 npm run electron

Un cop comprovat que obtenim el mateix funcionament que abans amb Ionic, ja podem generar l'aplicació d'escriptori. És tan senzill com fer:
 npm run dist

En Linux, el resultat és una aplicació distribuïble, SunriseSunset-1.1.0-x86_64.AppImage, que encapsula totes les dependències i que es pot executar directament. El resultat és el següent:

 
Tal com es pot veure, té l'aspecte natiu d'una aplicació Ubuntu i el funcionament és idèntic al que teníem en l'aplicació Ionic original.

Noteu com una mateixa base de codi typescript/html/css3 permet generar aplicacions mòbils, web i d'escriptori. Si no necessitem les prestacions addicionals d'una aplicació nativa, la flexibilitat d'aquesta proposta resulta extremadament atractiva, no us sembla?

Com sempre, si voleu practicar, teniu el codi final a la vostre disposició en el meu Github: v1.1.0.

De fet, fins i tot podeu descarregar i provar directament l'aplicació d'escriptori: SunriseSunset-1.1.0-x86_64.AppImage (per Ubuntu, Debian o altres variants compatibles).

Ja em comentareu què us ha semblat !

29 de març de 2018

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

Part VII: Personalització d'icones i tipus de lletra

Aquest article ens mostrarà com canviar el tipus de lletra que utilitza una aplicació i com afegir icones personalitzades.

Fins ara hem fet una aplicació per saber l'hora de sortida i posta del sol:
Ara anem a personalitzar-la una mica, amb icones i tipus de lletra diferents.

Comencem pel tipus de lletra. En tenim moltes de disponibles a Google fonts. N'escollim una que s'ajusti a les necessitats (Poppins en el nostre cas) i descomprimim els fitxer *.ttf a assets/fonts. Tot seguit declarem la nova font i la posem per defecte, editant el fitxer theme/variables.scss per afegir:
 ...
 // Fonts
 // --------------------------------------------------

 //@import "roboto";
 //@import "noto-sans";

 @font-face {
  font-family: "Poppins";
  //font-style: normal;
  //font-weight: 400;
  src:  url($font-path+'/Poppins-Regular.ttf');
 }
 @font-face {
  font-family: "Poppins";
  src:  url($font-path+'/Poppins-Bold.ttf');
  font-weight: bold;
 }
 @font-face {
  font-family: "Poppins";
  src: url($font-path+'/Poppins-Italic.ttf');
  font-style: italic;
 }
 @font-face {
  font-family: "Poppins";
  src: url($font-path+'/Poppins-BoldItalic.ttf');
  font-weight: bold;
  font-style: italic;
 }

 $font-family-base: "Poppins";

 $font-family-md-base: "Poppins";
 $font-family-ios-base: "Poppins";
 $font-family-wp-base: "Poppins";

Pel que fa a les icones, si n'hem d'utilitzar moltes seria convenient declarar un nou tipus de lletra, però si són poques, és molt molt senzill afegir-les directament. Per això, escollim icones lliures, per exemple a Flaticon, procurant que siguin d'un sol color i en format SVG. Llavors les afegim a app/app.scss:
 ...
 // --- Custom icons -------------------------------------------------------------------
 //
 // https://stackoverflow.com/a/44575053/1581368
 // To generate a font : https://yannbraga.com/2017/06/28/how-to-use-custom-icons-on-ionic-3/
 ion-icon {
    &[class*="custom-"] {
        // Instead of using the font-based icons we're applying SVG masks
        mask-size: contain;
        mask-position: 50% 50%;
        mask-repeat: no-repeat;
        background: currentColor;
        width: 1em;
        height: 1em;
    }
    // custom icons
    &[class*="custom-sunrise"] {
        mask-image: url(../assets/icon/sunrise.svg);
    }
    &[class*="custom-sunset"] {
        mask-image: url(../assets/icon/sunset.svg);
    }
 }

I llavors ja els podem utilitzar normalment a home.html:
 ...
    <ion-item-divider color="light">
      {{ "APP.timetable" | translate }}
    </ion-item-divider>
    <ion-item>
      <ion-icon name="custom-sunrise" color="orange" item-left></ion-icon>
      <ion-note item-right>{{ sunrise }}</ion-note>
    </ion-item>
    <ion-item>
      <ion-icon name="custom-sunset" color="blue" item-left></ion-icon>
      <ion-note item-right>{{ sunset }}</ion-note>
    </ion-item>
 ...

El resultat es pot veure a continuació:

Observem en el lateral com s'ha carregat correctament el fitxer de fonts (les versions normal i negreta, que són les que s'utilitzen).

I d'aquesta manera tan senzilla podem donar-li un aspecte més polit a una aplicació. Per acabar-ho d'arrodonir, completem la pàgines de contacte, de crèdits i afegim informació al repositori de Github (en el fitxer README.md).

Com sempre, teniu tot el codi a la vostre disposició en el Github: v1.0.

I amb aquests retocs finals podem donar l'aplicació per completada. Espero que hagi resultat útil. Si teniu alguna idea per millorar-la, traduccions a altres idiomes o qualsevol comentari, no dubteu a comentar/contactar.

24 de març de 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ó: