Es mostren els missatges amb l'etiqueta de comentaris traducció. Mostrar tots els missatges
Es mostren els missatges amb l'etiqueta de comentaris traducció. Mostrar tots els missatges

10 d’abril del 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 !

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ó: