Localización en Polymer

Tras una batería de propuestas con mis dos grandes compañeros @ismaelfaro y @javiervelezreye para hacer localizable una aplicación en Polymer, podéis imaginar la cantidad de sugerencias que pudimos proponer cada una de las partes. Vamos a eso que los desarrolladores acabamos llamando “sesión dedicada a hablar del sexo de los ángeles Guiño”. Con lo cual tras algunas vueltas y no pocas horas de meditación, voy con mi propuesta.

Para ello os voy a mostrar el resultado final y después pasare a desglosar cada una de las partes.

image

1. Estructura del proyecto

image

La carpeta componentes contiene dos subcarpetas polymer y plarform que son los dos requerimientos básicos para trabajar con Polymer, junto con core-ajax que es un componente desarrollado por el equipo de polymer para hacer llamadas ajax al servidor.

La carpeta customelements contiene un componente global para localizar y el json necesario para localizar nuestro Hello World, tanto en Español,como en Ingles.

Por último un archivo index.html donde vamos a utilizar nuestro componente.

En primer lugar vamos a ver la estructura del archivo translate.json.

image

Si observas en este archivo tanto el idioma Ingles como Español están en el mismo archivo, cosa que no me parece una buena practica pero para el ejemplo nos puede servir. Mis recomendaciones serían separar los idiomas en diferentes archivos, así como hacer un archivo para cada idioma y por componente, entidad de dominio,etc.. al igual como un archivo global para cada idioma donde vamos a introducir textos como cancel,yes,no, etc,etc.

Y porque esta separación, pues sencillo, principalmente por temas de responsabilidad única así como la capacidad de hacer tu app más escalable.

Puedes utilizar dos estrategias dependiendo del volumen de tu app, si esta es pequeña podrías concatenar todo los archivos con herramientas como Grunt o Gulp y si tu app es muy grande podrías cargar los archivos de idiomas por separado para cada componente, como podéis imaginar todo tiene sus ventajas e inconvenientes, la primera tiene una carga grande y menos round-trip al servidor y la segunda cargas pequeñas y muchos round-trip, con lo cual te dejo que seas tu el que decidas que hacer en función del tamaño de tu app, pero eso sí separa los archivos por idioma y por alguno de los criterios que ya he comentado, o por cualquiera que te parezca apropiado, piensa que esto no es una doctrina.

Una vez descrito como deberías localizar tus resources vamos con nuestro componente responsable de la carga.

   1: <link rel="import" href="../components/polymer/polymer.html">

   2: <link rel="import" href="../components/core-ajax/core-ajax.html">

   3:  

   4: <polymer-element name="my-translate" attributes="culture url">

   5:     

   6:     <template>

   7:         <style>

   8:             :host{

   9:                 display: none;

  10:             }

  11:         </style>

  12:         <core-ajax id="resources" auto url="{{url}}" handleAs="json" on-core-response="{{handleResponse}}"></core-ajax>        

  13:     </template>

  14:     

  15:     <script>

  16:             Polymer("my-translate",{

  17:               culture:'en',

  18:               url:'../customelements/translate.json',              

  19:               handleResponse:function(event,response){

  20:                  this.data = response.response;

  21:                  this.setModel();                    

  22:               },

  23:               cultureChanged:function(oldValue, newValue){

  24:                   if(oldValue!==newValue){

  25:                     this.setModel();

  26:                   }

  27:               },

  28:               urlChanged:function(oldValue,newValue){

  29:                   if(oldValue!==newValue){

  30:                     this.$.resources.go();

  31:                   }

  32:               },

  33:               setModel:function(){

  34:                   var culture = this.culture || "en",templates,i;

  35:                   if(this.data){                      

  36:                       templates = document.querySelectorAll("[is*='auto-binding']");

  37:                       if (templates){

  38:                           for(i=0;i<templates.length;i++){

  39:                               templates[i].model.res=this.data[culture];

  40:                           }

  41:                       }

  42:                   }

  43:               }

  44:             });

  45:     </script>

  46:     

  47: </polymer-element>

Vamos a analizar cada una de las partes de nuestro componente.

1. Carga de las dependencias necesarias.

image

En la primera línea cargamos polymer.html necesario para poder implementar cada uno de los componentes que realicemos.

En la segunda línea cargamos core-ajax.html necesario para cargar los archivos json desde nuestro servidor.

image

En esta línea describimos el nombre de nuestro componente(requerido) así como los atributos:

culture: para asignar la cultura a nuestro componente.

url : Le indicará a nuestro componente cual es la url desde donde vamos a cargar nuestro json con los resources traducidos.

Para ello necesitamos el tag “polymer-element”, que a su vez no es otra cosa que un componente global descrito en polymer y que nos sirve para crear cada uno de nuestros componentes.

image

En esta parte estamos describiendo en un template de Polymer por una parte el estilo de nuestro componente que en este caso lo defino como un componente no visual y es por eso por lo que utilizamos :host con display a none. Te recomiendo revisar cada una de las posibilidades que tienes con respecto a estilos en Polymer.

Styling elements 

Lo segundo es la utilización de core-ajax a la que le asignamos los siguientes atributos y control de eventos.

auto: Carga automáticamente el resuorce sin la iteración del usuario.

url: Url donde se encuentra nuestro resource y que obtiene el valor del atributo url de nuestro componente con binding.

handleAs: Le estamos indicando a nuestro componente que el recurso es del tipo application/json aunque lo envía como application/x-www-form-urlencoded, quizá algún error en core-ajax, pero tampoco he entrado a analizar más puesto que funciona correctamente.

on-core-response: Esto es la suscripción a un evento que expone core-ajax y que se encarga de obtener la respuesta del servidor en caso que sea correcto, también puedes observar que esta bindeado en este caso a la función de nuestro componente handleResponse.

id: Si en cualquiera de los elementos de una template dentro de un componente se especifica un id, este será accesible desde el componente y desde fuera de este a través del objeto $, con lo cual para acceder a este componente puedes utilizar desde dentro de tu componente this.$.resource y desde fuera document.querySelector(‘my-translate’).$.resource.

Por último vamos con el JavaScript necesario en nuestro componente.

image

En primer lugar la asignación de los valores por defecto tanto para culture como url, en este caso “en” para culture y “../customelements/translate.json” quizá te preguntes el porque tengo que subir un nivel en la url y después descender a customelements cuando este resource se encuentra en la misma carpeta de mi componente, pues sencillo, piensa que este se va a consumir desde nuestra página index.html que se encuentra en un nivel superior. De esta forma puedes deducir que quizá una buena practica sería poner todos los resources en una carpeta especifica, pero como siempre es algo que debes de decidir tu.

handleResponse. Esta función es la suscripción al evento core-response del componente core-ajax que recibe tres argumentos, el primero de ellos es un customEvent, el segundo es la respuesta del servidor y en response tenemos el json y es por eso que se lo asignamos a data.

this.data = response.response.

El último de los argumentos y que no estamos utilizando es en sí el componente core-ajax.

Una vez que asignamos la respuesta a nuestra variable data, llamamos a la función SetModel. Lo ideal que yo no he hecho hubiese sido encapsular este valor, puesto que yo lo estoy exponiendo como público al  asignarlo a this.data,  se supone que tienes claro como poder encapsular datos en JavaScript y en el caso de Polymer lo puedes hacer con una función anónima y autoejecutable.

cultureChanged: Es una función que observa el cambio del atributo/propiedad culture y que llama a setModel. recibe dos parametros oldValue y newValue, como puedes observar siempre que utilices una función que observe el cambio de una propiedad por convención debe de terminar en Changed y precedida del nombre del atributo, otra cosa que deberías es siempre preguntar si oldValue no es igual a newValue, para ejecutar tu lógica.

urlChanged: Observa el cambio de url y en este caso si oldValue es distinto de newValue llama a la función go nuestro componente core-ajax para volver a cargar los datos.

En Polymer a diferencia de Angular no se utiliza dirty checking sino Object.Observe y que si quieres conocer más te recomiendo que analices el siguiente repositorio que es el que utiliza Polymer.

https://github.com/Polymer/observe-js

setModel: Está función lo que hace es seleccionar todas las templates de nuestra página que contengan el atributo is con valor igual a auto-binding y observa que le asigno a la propiedad model de la template el valor de data en la propiedad res y porque esto? Sencillo por separar en nuestra template los datos con una cierta lógica, es decir que en nuestro caso siempre accederemos a estos via binding de la siguiente forma.

{{res.hello}} para acceder al valor del resource hello de nuestro json, así como para acceder a world lo haremos con {{res.world}}.

Una vez analizado nuestro componente vamos con la parte de consumir este, aunque vamos a ser más breves.

image

1. Cargamos platform.js necesario para cualquier app Polymer.

2.Utilizamos link con rel igual a import para importar nuestro componente translate.

3. Establecemos un estilo con color a rojo para cada una de los tag hx con valor par, es decir h2,h4 y h6.

4. Creamos nuestra template con un header y todos los hx con el siguiente binding {{res.hello}}-{{res.world}} y el atributo is a valor auto-binding Advanced topics. Realmente es una cosa que me pregunto y es si en realidad esto afecta a SEO o por otra parte el esfuerzo que están haciendo todos por indexar páginas con JavaScript, convierta esto en un estandar, espero que sea así, puesto que sino poco sentido le veo al esfuerzo que se está realizando con WebComponents. En este caso una vez que se ha bindeado la template el contenido de esta se inserta justo detras de ella, como puedes observar.

image

5. Insertamos nuestro elemento “my-translate” en nuestra página y recuerda que no será visible, puesto que le indicamos en su estilo que la propiedad display estaba establecida a none.

6. Por último nos suscribimos al evento WebComponentsReady y cambiamos la cultura de nuestro componente, es evidente que esto lo podrías hacer desde un elemento select, etc,etc.

Si has montado todo el proyecto tal y como lo hemos descrito en el post, te invito a que ejecutes las siguientes líneas desde la consola del explorador.

document.querySelector(‘my-translate’).culture="en"

Resultado.

image

var response = document.querySelector(‘my-translate’).$.resources.go()

response.response

Resultado.

image

Conclusiones.

Como ya he comentado en el post esto no pretende ser una doctrina a la hora de aplicar localización en Polymer, pero si puede ser una aproximación de como hacerlo.

Lo que si que tienes que tener en cuenta cuando tu hagas un componente es exponer todas las propiedades localizables como atributos para poder hacer binding, al igual que lo hacen los Paper Elements de Polymer

Por último la url del repositorio, por si queréis jugar o proponer nuevas funcionalidades.

Polymer translate